perf(dcl): batch subscribe/read/write seam, bounded reconnect, sharded subscriptions
This commit is contained in:
@@ -26,6 +26,7 @@ public sealed class RealMxGatewayClient : IMxGatewayClient
|
||||
private int _serverHandle;
|
||||
private int _writeUserId;
|
||||
private int _readTimeoutMs;
|
||||
private int _supervisoryAdviseParallelism = 16;
|
||||
private ulong _lastSeq;
|
||||
|
||||
// tag ↔ MXAccess item handle, maintained across subscribe/write.
|
||||
@@ -65,6 +66,7 @@ public sealed class RealMxGatewayClient : IMxGatewayClient
|
||||
{
|
||||
_writeUserId = options.WriteUserId;
|
||||
_readTimeoutMs = options.ReadTimeoutMs;
|
||||
_supervisoryAdviseParallelism = Math.Max(1, options.SupervisoryAdviseParallelism);
|
||||
|
||||
var clientOptions = new MxGatewayClientOptions
|
||||
{
|
||||
@@ -102,6 +104,122 @@ public sealed class RealMxGatewayClient : IMxGatewayClient
|
||||
return handle.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<MxSubscribeOutcome>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> tagPaths, CancellationToken ct = default)
|
||||
{
|
||||
if (tagPaths.Count == 0)
|
||||
return [];
|
||||
|
||||
// Plain-advise mode (a configured write user): the worker's SubscribeBulk does
|
||||
// AddItem + Advise for every tag in ONE gateway command, collapsing the historical
|
||||
// 2 RPCs per tag to 1 RPC per chunk.
|
||||
if (!UseSupervisoryAdvise)
|
||||
{
|
||||
var replies = await _session!.SubscribeBulkAsync(_serverHandle, tagPaths, ct).ConfigureAwait(false);
|
||||
return MapSubscribeResults(tagPaths, replies);
|
||||
}
|
||||
|
||||
// Supervisory mode (WriteUserId == 0): the worker has no BULK supervisory advise
|
||||
// (SubscribeBulk issues plain Advise), so bulk-add the items in one command and
|
||||
// pipeline the per-item AdviseSupervisory commands with a bounded window instead
|
||||
// of issuing them serially. Cross-repo follow-up: an additive `supervisory` flag on
|
||||
// SubscribeBulkCommand in mxaccessgw would collapse this to one RPC as well.
|
||||
var added = await _session!.AddItemBulkAsync(_serverHandle, tagPaths, ct).ConfigureAwait(false);
|
||||
var outcomes = MapSubscribeResults(tagPaths, added);
|
||||
|
||||
var advisable = outcomes
|
||||
.Select((o, i) => (Outcome: o, Index: i))
|
||||
.Where(x => x.Outcome.Success)
|
||||
.ToList();
|
||||
|
||||
var advised = await BulkPipeline.RunAsync(
|
||||
advisable,
|
||||
_supervisoryAdviseParallelism,
|
||||
async (x, token) =>
|
||||
{
|
||||
var handle = int.Parse(x.Outcome.SubscriptionId!, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
await EnsureSupervisoryAdvisedAsync(handle, token).ConfigureAwait(false);
|
||||
return x.Outcome;
|
||||
},
|
||||
(x, ex) => x.Outcome with { Success = false, SubscriptionId = null, Error = ex.Message },
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
var result = outcomes.ToArray();
|
||||
for (var i = 0; i < advisable.Count; i++)
|
||||
result[advisable[i].Index] = advised[i];
|
||||
|
||||
// A failed advise leaves an added-but-unadvised item; drop our handle mapping so a
|
||||
// later retry re-adds cleanly rather than reusing a half-subscribed handle.
|
||||
foreach (var failed in result.Where(r => !r.Success))
|
||||
{
|
||||
if (_tagToHandle.TryRemove(failed.TagPath, out var staleHandle))
|
||||
_handleToTag.TryRemove(staleHandle, out _);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps the gateway's per-item bulk results back onto the requested tag paths. Results
|
||||
/// are returned in request order; the tag address on the reply is preferred when the
|
||||
/// gateway echoes it. Successful rows populate the tag ↔ item-handle maps that the
|
||||
/// event loop and the write path key off.
|
||||
/// </summary>
|
||||
private MxSubscribeOutcome[] MapSubscribeResults(
|
||||
IReadOnlyList<string> tagPaths, IReadOnlyList<SubscribeResult> replies)
|
||||
{
|
||||
var outcomes = new MxSubscribeOutcome[tagPaths.Count];
|
||||
for (var i = 0; i < tagPaths.Count; i++)
|
||||
{
|
||||
var tag = tagPaths[i];
|
||||
if (i >= replies.Count)
|
||||
{
|
||||
outcomes[i] = new MxSubscribeOutcome(tag, false, null, "Gateway returned no result for this tag.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var reply = replies[i];
|
||||
if (!reply.WasSuccessful)
|
||||
{
|
||||
outcomes[i] = new MxSubscribeOutcome(tag, false, null,
|
||||
string.IsNullOrEmpty(reply.ErrorMessage) ? "Bulk subscribe failed." : reply.ErrorMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
_tagToHandle[tag] = reply.ItemHandle;
|
||||
_handleToTag[reply.ItemHandle] = tag;
|
||||
outcomes[i] = new MxSubscribeOutcome(
|
||||
tag, true, reply.ItemHandle.ToString(CultureInfo.InvariantCulture), null);
|
||||
}
|
||||
|
||||
return outcomes;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UnsubscribeBulkAsync(IReadOnlyList<string> subscriptionIds, CancellationToken ct = default)
|
||||
{
|
||||
var handles = new List<int>(subscriptionIds.Count);
|
||||
foreach (var id in subscriptionIds)
|
||||
{
|
||||
if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var handle))
|
||||
handles.Add(handle);
|
||||
}
|
||||
|
||||
if (handles.Count == 0)
|
||||
return;
|
||||
|
||||
await _session!.UnsubscribeBulkAsync(_serverHandle, handles, ct).ConfigureAwait(false);
|
||||
|
||||
foreach (var handle in handles)
|
||||
{
|
||||
// UnsubscribeBulk unadvises AND removes the item, releasing every advice kind.
|
||||
_supervisoryAdvised.TryRemove(handle, out _);
|
||||
if (_handleToTag.TryRemove(handle, out var tag))
|
||||
_tagToHandle.TryRemove(tag, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UnsubscribeAsync(string subscriptionId, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user