Skip to content

Commit 7064964

Browse files
committed
SubscriptionOnlineManager: skip TryAddOrderTransaction when statusInfo is a linked view
Reproducer: client subscribes to OrderStatus(Tx=N), registers an order(Tx=R), broker replies with an Active execution carrying OrigTxId=R, TxId=R+1. The execution-side lookup hits the per-order TxId entry created by the prior register and returns a linked SubscriptionInfo whose Linked/State accessors throw via CheckOnLinked. TryAddOrderTransaction then called statusInfo.Linked.Add and threw — the connector logged the CheckOnLinked exception and the order flow then surfaced a phantom Cancel. Add ISubscriptionOnlineInfo.IsLinked and bail out of TryAddOrderTransaction when the resolved info is already a linked view: the order is tracked through the main subscription, the message is forwarded by the caller, no extra alias state is needed. Test reproduces the exact stack and asserts the execution passes through.
1 parent a26ce59 commit 7064964

4 files changed

Lines changed: 92 additions & 0 deletions

File tree

Algo/ISubscriptionOnlineManagerState.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,14 @@ public interface ISubscriptionOnlineInfo
130130
/// Linked subscription IDs.
131131
/// </summary>
132132
List<long> Linked { get; }
133+
134+
/// <summary>
135+
/// True when this info is a linked view onto a main subscription
136+
/// (created via <see cref="ISubscriptionOnlineManagerState.CreateLinkedSubscriptionInfo"/>),
137+
/// not the main subscription itself. Mutating <see cref="State"/> or
138+
/// reading <see cref="Linked"/> on a linked view throws — callers that
139+
/// reach an info through a per-order TxId lookup must check this flag
140+
/// before treating the info as a main subscription.
141+
/// </summary>
142+
bool IsLinked { get; }
133143
}

Algo/SubscriptionOnlineManager.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,15 @@ private bool ChangeState(ISubscriptionOnlineInfo info, long transId, Subscriptio
300300

301301
private void TryAddOrderTransaction(ISubscriptionOnlineInfo statusInfo, long transactionId, bool warnOnDuplicate = true)
302302
{
303+
// statusInfo can arrive as a linked view (looked up via an existing
304+
// per-order TxId entry in _subscriptionsById). The order is then
305+
// already tracked through the main OrderStatus subscription — adding
306+
// another linked alias keyed by another TxId duplicates state, and
307+
// statusInfo.Linked.Add below would throw CheckOnLinked anyway. Bail
308+
// silently — the message will still be forwarded by the caller.
309+
if (statusInfo.IsLinked)
310+
return;
311+
303312
if (!_state.ContainsSubscriptionById(transactionId))
304313
{
305314
var orderSubscription = _state.CreateLinkedSubscriptionInfo(statusInfo);

Algo/SubscriptionOnlineManagerState.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ public List<long> Linked
6060
}
6161
}
6262

63+
public bool IsLinked => _main != null;
64+
6365
public override string ToString() => (_main != null ? "Linked: " : string.Empty) + Subscription.ToString();
6466
}
6567

Tests/SubscriptionOnlineManagerTests.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,77 @@ public async Task OrderStatus_CreatesSubscriptionWithDefaultSecurityId()
10221022
AreEqual(100, info.Subscription.TransactionId, "Subscription TransactionId should match");
10231023
}
10241024

1025+
/// <summary>
1026+
/// Reproducer for the linkage break observed in MultiConnect/client1 logs:
1027+
/// Subscribe OrderStatus(Tx=N) → RegisterOrder(Tx=R) → broker sends Active
1028+
/// ExecutionMessage{ OrigTxId=R, TxId!=0, DataType=Transactions }.
1029+
///
1030+
/// On the outgoing OrderRegister, ProcessInMessageAsync calls
1031+
/// TryAddOrderSubscription → TryAddOrderTransaction(MAIN, R), which adds a
1032+
/// LINKED copy at <c>_subscriptionsById[R]</c>.
1033+
///
1034+
/// Then the incoming Active is looked up by OrigTxId=R and returns the
1035+
/// LINKED copy (not MAIN). Inside the existing TryAddOrderTransaction call
1036+
/// at ProcessOutMessageAsync (line 223), <c>statusInfo.Linked.Add</c> is
1037+
/// called — and SubscriptionInfo.CheckOnLinked() throws because info has
1038+
/// _main != null.
1039+
///
1040+
/// Symptom in MultiConnect log: Order stays Pending despite broker
1041+
/// confirming Active. After cancel, Order shows as new Done — the user
1042+
/// reads it as "filled".
1043+
/// </summary>
1044+
[TestMethod]
1045+
public async Task OrderRegister_FollowedByActive_OrigTxIdEqualsRegisterTxId_DoesNotThrow()
1046+
{
1047+
var logReceiver = new TestReceiver();
1048+
var manager = new SubscriptionOnlineManager(logReceiver, _ => true, new SubscriptionOnlineManagerState());
1049+
var token = CancellationToken;
1050+
1051+
// 1. Subscribe to OrderStatus.
1052+
await manager.ProcessInMessageAsync(new OrderStatusMessage
1053+
{
1054+
IsSubscribe = true,
1055+
TransactionId = 100,
1056+
}, token);
1057+
await manager.ProcessOutMessageAsync(new SubscriptionResponseMessage { OriginalTransactionId = 100 }, token);
1058+
await manager.ProcessOutMessageAsync(new SubscriptionOnlineMessage { OriginalTransactionId = 100 }, token);
1059+
1060+
// 2. Send OrderRegister with TxId=R. ProcessInMessageAsync's switch on
1061+
// MessageTypes.OrderRegister calls TryAddOrderSubscription → adds a
1062+
// LINKED entry at _subscriptionsById[R].
1063+
const long R = 27738195;
1064+
await manager.ProcessInMessageAsync(new OrderRegisterMessage
1065+
{
1066+
TransactionId = R,
1067+
SecurityId = Helper.CreateSecurityId(),
1068+
Side = Sides.Buy,
1069+
Volume = 200m,
1070+
Price = 0.4m,
1071+
OrderType = OrderTypes.Limit,
1072+
}, token);
1073+
1074+
// 3. Broker responds Active. With OrigTxId=R the lookup at line 216 in
1075+
// ProcessOutMessageAsync returns the LINKED copy. With TxId!=0, the
1076+
// branch at line 223 calls TryAddOrderTransaction(LINKED, X), which
1077+
// then hits statusInfo.Linked.Add → CheckOnLinked → throws.
1078+
var active = new ExecutionMessage
1079+
{
1080+
DataTypeEx = DataType.Transactions,
1081+
HasOrderInfo = true,
1082+
OrderState = OrderStates.Active,
1083+
OrderId = 4991775493L,
1084+
OriginalTransactionId = R,
1085+
TransactionId = R + 1, // upstream-translated id, anything non-zero
1086+
SecurityId = Helper.CreateSecurityId(),
1087+
ServerTime = logReceiver.CurrentTime,
1088+
};
1089+
1090+
// Currently throws InvalidOperationException via CheckOnLinked.
1091+
// After the fix it must pass through and forward the message.
1092+
var (forward, _) = await manager.ProcessOutMessageAsync(active, token);
1093+
forward.AssertNotNull("Active execution must pass through, not be dropped or throw.");
1094+
}
1095+
10251096
#endregion
10261097

10271098
#region History+Live Subscription Tests

0 commit comments

Comments
 (0)