LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL adapter files, and related docs to deprecated/. Removed LmxProxy registration from DataConnectionFactory, project reference from DCL, protocol option from UI, and cleaned up all requirement docs.
5.2 KiB
Component: SubscriptionManager
Purpose
Manages the lifecycle of tag value subscriptions, multiplexing multiple client subscriptions onto shared MXAccess tag subscriptions and delivering updates via per-client bounded channels with configurable backpressure.
Location
src/ZB.MOM.WW.LmxProxy.Host/Subscriptions/SubscriptionManager.cs
Responsibilities
- Create per-client subscription channels with bounded capacity.
- Share underlying MXAccess tag subscriptions across multiple clients subscribing to the same tags.
- Deliver tag value updates from MXAccess callbacks to all subscribed clients.
- Handle backpressure when client channels are full (DropOldest, DropNewest, or Wait).
- Clean up subscriptions on client disconnect.
- Notify all subscribed clients with bad quality when MXAccess disconnects.
1. Architecture
1.1 Per-Client Channels
Each subscribing client gets a bounded System.Threading.Channel<(string address, Vtq vtq)>:
- Capacity: configurable (default 1000 messages).
- Full mode: configurable (default
DropOldest). SingleReader = true,SingleWriter = false.
1.2 Shared Tag Subscriptions
Tag subscriptions to MXAccess are shared across clients:
- When the first client subscribes to a tag, a new MXAccess subscription is created.
- When additional clients subscribe to the same tag, they are added to the existing tag subscription's client set.
- When the last client unsubscribes from a tag, the MXAccess subscription is disposed.
1.3 Thread Safety
ReaderWriterLockSlimprotects tag subscription updates.ConcurrentDictionaryfor client subscription tracking.
2. Subscription Flow
2.1 Subscribe
SubscribeAsync(clientId, addresses, ct):
- Creates a bounded channel with configured capacity and full mode.
- Creates a
ClientSubscriptionrecord (clientId, channel, address set, CancellationTokenSource, counters). - For each tag address:
- If the tag already has a subscription, adds the client to the existing
TagSubscription.clientIdsset. - Otherwise, creates a new
TagSubscriptionand calls_scadaClient.SubscribeAsync()to register with MXAccess (outside the lock to avoid blocking).
- If the tag already has a subscription, adds the client to the existing
- Registers a cancellation token callback to automatically call
UnsubscribeClienton disconnect. - Returns the channel reader for the GrpcServer to stream from.
2.2 Value Updates
OnTagValueChanged(address, Vtq) — called from MxAccessClient's COM event handler:
- Looks up the tag subscription to find all subscribed clients.
- For each client, calls
channel.Writer.TryWrite((address, vtq)). - If the channel is full:
- DropOldest: Logs a warning, increments
DroppedMessageCount. The oldest message is automatically discarded by the channel. - DropNewest: Drops the incoming message.
- Wait: Blocks the writer until space is available (not recommended for gRPC streaming).
- DropOldest: Logs a warning, increments
- On channel closed (client disconnected), schedules
UnsubscribeClientcleanup.
2.3 Unsubscribe
UnsubscribeClient(clientId):
- Removes the client from the client dictionary.
- For each tag the client was subscribed to, removes the client from the tag's subscriber set.
- If a tag has no remaining subscribers, disposes the MXAccess subscription handle.
- Completes the client's channel writer (signals end of stream).
3. Backpressure
| Mode | Behavior | Use Case |
|---|---|---|
| DropOldest | Silently discards oldest message when channel is full | Default. Fire-and-forget semantics. No client blocking. |
| DropNewest | Drops the incoming message when channel is full | Preserves history, drops latest updates. |
| Wait | Blocks the writer until space is available | Not recommended for gRPC streaming (blocks callback thread). |
Per-client statistics track DeliveredMessageCount and DroppedMessageCount for monitoring via the status dashboard.
4. Disconnection Handling
4.1 Client Disconnect
When a client's gRPC stream ends (cancellation or error), the cancellation token callback triggers UnsubscribeClient, which cleans up all tag subscriptions for that client.
4.2 MxAccess Disconnect
OnConnectionStateChanged — when the MxAccess connection drops:
- Sends a bad-quality Vtq to all subscribed clients via their channels.
- Each client receives an async notification of the connection loss.
- Tag subscriptions are retained in memory for reconnection (via MxAccessClient's
_storedSubscriptions).
5. Statistics
GetSubscriptionStats() returns:
TotalClients— number of active client subscriptions.TotalTags— number of unique tags with active MXAccess subscriptions.ActiveSubscriptions— total client-tag subscription count.
Dependencies
- MxAccessClient (IScadaClient) — creates and disposes MXAccess tag subscriptions.
- Configuration —
SubscriptionConfigurationfor channel capacity and full mode.
Interactions
- GrpcServer calls
SubscribeAsyncon Subscribe RPC and reads from the returned channel. - MxAccessClient delivers value updates via the
OnTagValueChangedcallback. - HealthAndMetrics reads subscription statistics for health checks and status reports.
- ServiceHost disposes the SubscriptionManager at shutdown.