Summary
ISubscription has 42 members. Eleven of them are publish-pipeline collaboration protocol with SubscriptionManager and SessionPublishQueue — internal machinery published on a public interface, where any holder of an ISubscription can call them and corrupt the publishing state machine.
Evidence
Subscription/ISubscription.cs — 42 members over Subscription (3,471 lines).
The 11 protocol members:
| Member |
Purpose |
ItemReadyToPublish |
monitored item signals data available |
ItemNotificationsAvailable |
monitored item signals queued notifications |
QueueOverflowHandler |
queue overflow callback |
PublishTimerExpired |
publish timer tick |
PublishTimeout |
timeout notification message |
SubscriptionTransferred |
transfer notification message |
AvailableSequenceNumbersForRetransmission |
retransmission bookkeeping |
Acknowledge |
sequence number acknowledgement |
ResendData |
resend request |
SessionClosed() |
session teardown |
SessionClosed(ISession) |
session teardown, overload |
Why this matters
These are called by exactly two collaborators — SubscriptionManager and SessionPublishQueue — but they sit on the public interface. A node manager reaching through IServerInternal.SubscriptionManager.GetSubscriptions() can call PublishTimerExpired() or QueueOverflowHandler() directly and drive the publishing state machine into an inconsistent state. Nothing in the type system prevents it.
The interface is also the test surface, so tests of subscription behaviour are invited to drive the protocol directly rather than asserting observable outcomes.
The behaviour behind the seam is real
This is not a pass-through — the publishing machinery is substantial:
| Module |
Lines |
Subscription.cs |
3,471 |
SessionPublishQueue.cs |
1,071 |
SentMessageQueue.cs |
449 |
| Total |
~5,000 |
Deletion test
Delete the 11 members from the public interface: complexity reappears, but only inside SubscriptionManager and SessionPublishQueue, which are the only legitimate callers. That is relocation into an internal seam, not duplication across callers.
Proposed direction
Move the publish-pipeline protocol to an internal seam consumed only by SubscriptionManager and SessionPublishQueue. Leave the service-set operations (CreateMonitoredItemsAsync, ModifyMonitoredItemsAsync, DeleteMonitoredItemsAsync, SetMonitoringModeAsync, SetPublishingMode, Publish, Republish, Modify, ConditionRefreshAsync, SetTriggering, TransferSessionAsync, …) on ISubscription.
Expected result: 42 → ~25 members, no behaviour change, no plugin-API break.
Open question
Does durable-subscription restore require external access to any of the 11? ToStorableSubscription(), SetSubscriptionDurable() and AvailableSequenceNumbersForRetransmission() interact with the persistence path (Subscription/Persistence/), so the internal seam must still serve ISubscriptionStore and ISubscriptionRetransmissionStore. See docs/DurableSubscription.md.
Constraints
- 1.5.378 compatibility — additive; mark the moved members
[Obsolete] on ISubscription and forward to the internal seam during transition.
- Must continue to support
TransferSubscriptions (docs/TransferSubscription.md) and durable subscriptions.
- Tests that currently drive the protocol directly should be replaced with tests asserting observable outcomes through the service-set operations — replace, don't layer.
Dependency category
In-process. Pure computation and in-memory state; no port required. The persistence seams (ISubscriptionStore, IMonitoredItemQueueFactory, ISubscriptionRetransmissionStore) already exist and are reused unchanged.
Verified against commit e73e71184.
Summary
ISubscriptionhas 42 members. Eleven of them are publish-pipeline collaboration protocol withSubscriptionManagerandSessionPublishQueue— internal machinery published on a public interface, where any holder of anISubscriptioncan call them and corrupt the publishing state machine.Evidence
Subscription/ISubscription.cs— 42 members overSubscription(3,471 lines).The 11 protocol members:
ItemReadyToPublishItemNotificationsAvailableQueueOverflowHandlerPublishTimerExpiredPublishTimeoutSubscriptionTransferredAvailableSequenceNumbersForRetransmissionAcknowledgeResendDataSessionClosed()SessionClosed(ISession)Why this matters
These are called by exactly two collaborators —
SubscriptionManagerandSessionPublishQueue— but they sit on the public interface. A node manager reaching throughIServerInternal.SubscriptionManager.GetSubscriptions()can callPublishTimerExpired()orQueueOverflowHandler()directly and drive the publishing state machine into an inconsistent state. Nothing in the type system prevents it.The interface is also the test surface, so tests of subscription behaviour are invited to drive the protocol directly rather than asserting observable outcomes.
The behaviour behind the seam is real
This is not a pass-through — the publishing machinery is substantial:
Subscription.csSessionPublishQueue.csSentMessageQueue.csDeletion test
Delete the 11 members from the public interface: complexity reappears, but only inside
SubscriptionManagerandSessionPublishQueue, which are the only legitimate callers. That is relocation into an internal seam, not duplication across callers.Proposed direction
Move the publish-pipeline protocol to an internal seam consumed only by
SubscriptionManagerandSessionPublishQueue. Leave the service-set operations (CreateMonitoredItemsAsync,ModifyMonitoredItemsAsync,DeleteMonitoredItemsAsync,SetMonitoringModeAsync,SetPublishingMode,Publish,Republish,Modify,ConditionRefreshAsync,SetTriggering,TransferSessionAsync, …) onISubscription.Expected result: 42 → ~25 members, no behaviour change, no plugin-API break.
Open question
Does durable-subscription restore require external access to any of the 11?
ToStorableSubscription(),SetSubscriptionDurable()andAvailableSequenceNumbersForRetransmission()interact with the persistence path (Subscription/Persistence/), so the internal seam must still serveISubscriptionStoreandISubscriptionRetransmissionStore. Seedocs/DurableSubscription.md.Constraints
[Obsolete]onISubscriptionand forward to the internal seam during transition.TransferSubscriptions(docs/TransferSubscription.md) and durable subscriptions.Dependency category
In-process. Pure computation and in-memory state; no port required. The persistence seams (
ISubscriptionStore,IMonitoredItemQueueFactory,ISubscriptionRetransmissionStore) already exist and are reused unchanged.Verified against commit
e73e71184.