fix(driver-galaxy): resolve High code-review findings (Driver.Galaxy-002, Driver.Galaxy-008)
Driver.Galaxy-002 — DataTypeMap.Map had no Int64 arm though MxValueDecoder/ MxValueEncoder both fully support Int64. Galaxy attributes with the Int64 mx_data_type code fell through to the String default, creating a String address-space node while runtime reads decoded a boxed long. Added `6 => DriverDataType.Int64`, extending the contiguous 0..5 scheme so the type map agrees with the decoder/encoder on all seven Galaxy data types. Driver.Galaxy-008 — after a stream fault the EventPump's StreamEvents consumer loop exited and its channel completed; EventPump.Start() is a no-op on a completed-but-non-null loop, so a replayed subscription had no consumer and ReplayAsync never re-registered the post-reconnect item handles. ReplayAsync now recreates the EventPump (RestartEventPumpForReplay) and rebinds the SubscriptionRegistry per subscription with the fresh item handles returned by the post-reconnect SubscribeBulkAsync, via new SubscriptionRegistry.SnapshotEntries and Rebind APIs. Regression tests: DataTypeMapTests (every code incl. Int64), SubscriptionRegistry Tests (Rebind/SnapshotEntries), EventPumpStreamFaultTests (faulted pump dead, fresh pump resumes dispatch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -95,6 +95,47 @@ internal sealed class SubscriptionRegistry
|
||||
public IReadOnlyList<TagBinding> SnapshotAllBindings() =>
|
||||
[.. _bySubscriptionId.Values.SelectMany(entry => entry.Bindings)];
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot every active subscription with its bindings, grouped by subscription id.
|
||||
/// Used by the reconnect replay path so it can re-issue SubscribeBulk per subscription
|
||||
/// and then <see cref="Rebind"/> each one with the post-reconnect item handles.
|
||||
/// </summary>
|
||||
public IReadOnlyList<(long SubscriptionId, IReadOnlyList<TagBinding> Bindings)> SnapshotEntries() =>
|
||||
[.. _bySubscriptionId.Values.Select(entry => (entry.SubscriptionId, entry.Bindings))];
|
||||
|
||||
/// <summary>
|
||||
/// Replace an existing subscription's bindings with the item handles a post-reconnect
|
||||
/// SubscribeBulk returned, rebuilding the reverse fan-out map so events on the new
|
||||
/// handles dispatch and the now-dead pre-reconnect handles are dropped. No-op when the
|
||||
/// subscription id is unknown (it was unsubscribed during the reconnect window).
|
||||
/// </summary>
|
||||
public void Rebind(long subscriptionId, IReadOnlyList<TagBinding> newBindings)
|
||||
{
|
||||
if (!_bySubscriptionId.TryGetValue(subscriptionId, out var oldEntry)) return;
|
||||
|
||||
// Drop this subscription from every reverse-map bag it currently appears in. The
|
||||
// pre-reconnect item handles are stale once the gw re-issues fresh ones.
|
||||
foreach (var binding in oldEntry.Bindings)
|
||||
{
|
||||
if (binding.ItemHandle <= 0) continue;
|
||||
if (!_subscribersByItemHandle.TryGetValue(binding.ItemHandle, out var bag)) continue;
|
||||
|
||||
var remaining = new ConcurrentBag<long>(bag.Where(id => id != subscriptionId));
|
||||
if (remaining.IsEmpty) _subscribersByItemHandle.TryRemove(binding.ItemHandle, out _);
|
||||
else _subscribersByItemHandle[binding.ItemHandle] = remaining;
|
||||
}
|
||||
|
||||
_bySubscriptionId[subscriptionId] = new SubscriptionEntry(subscriptionId, newBindings);
|
||||
foreach (var binding in newBindings)
|
||||
{
|
||||
if (binding.ItemHandle <= 0) continue; // failed gw subscribe — no events expected
|
||||
_subscribersByItemHandle.AddOrUpdate(
|
||||
binding.ItemHandle,
|
||||
_ => [subscriptionId],
|
||||
(_, bag) => { bag.Add(subscriptionId); return bag; });
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record SubscriptionEntry(long SubscriptionId, IReadOnlyList<TagBinding> Bindings);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user