refactor(r2-04): internal IMxWriteOps seam under GatewayGalaxyDataWriter (sealed SDK session untestable)
This commit is contained in:
@@ -129,23 +129,39 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
|
||||
"GalaxyMxSession is not connected. Call ConnectAsync before issuing writes.");
|
||||
var serverHandle = _session.ServerHandle;
|
||||
|
||||
// Bind the per-batch MX write operations behind the internal IMxWriteOps seam (see below) so the
|
||||
// whole write pipeline is unit-testable without the sealed SDK session (archreview 06/S-1).
|
||||
var ops = new SessionMxWriteOps(session, serverHandle);
|
||||
|
||||
var results = new WriteResult[writes.Count];
|
||||
for (var i = 0; i < writes.Count; i++)
|
||||
{
|
||||
results[i] = await WriteOneAsync(session, serverHandle, writes[i],
|
||||
results[i] = await WriteOneAsync(ops, writes[i],
|
||||
securityResolver(writes[i].FullReference), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>Test seam: drive the real single-write pipeline against a fake <see cref="IMxWriteOps"/>
|
||||
/// so the fail-closed / unconfirmed behaviour (archreview 06/S-1) is covered offline. Behaviourally
|
||||
/// identical to the per-entry path <see cref="WriteAsync"/> runs.</summary>
|
||||
/// <param name="ops">The (fake) MX write operations.</param>
|
||||
/// <param name="request">The write request.</param>
|
||||
/// <param name="classification">The tag's security classification.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>The translated write result.</returns>
|
||||
internal Task<WriteResult> WriteOneForTestAsync(
|
||||
IMxWriteOps ops, WriteRequest request, SecurityClassification classification, CancellationToken ct)
|
||||
=> WriteOneAsync(ops, request, classification, ct);
|
||||
|
||||
private async Task<WriteResult> WriteOneAsync(
|
||||
MxGatewaySession session, int serverHandle, WriteRequest request,
|
||||
IMxWriteOps ops, WriteRequest request,
|
||||
SecurityClassification classification, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var itemHandle = await EnsureItemHandleAsync(session, serverHandle, request.FullReference, ct)
|
||||
var itemHandle = await EnsureItemHandleAsync(ops, request.FullReference, ct)
|
||||
.ConfigureAwait(false);
|
||||
var mxValue = MxValueEncoder.Encode(request.Value);
|
||||
|
||||
@@ -154,7 +170,7 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
|
||||
{
|
||||
// SecuredWrite/VerifiedWrite tags carry their own ArchestrA user identity
|
||||
// (current/verifier user), so they don't use the supervisory path.
|
||||
reply = await InvokeWriteSecuredAsync(session, serverHandle, itemHandle, mxValue, ct)
|
||||
reply = await InvokeWriteSecuredAsync(ops, itemHandle, mxValue, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
@@ -163,8 +179,8 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
|
||||
// only COMMITS the value when the item is advised in SUPERVISORY mode. Without it
|
||||
// the gateway's Write call doesn't throw (reply looks OK) but the value never
|
||||
// reaches the galaxy. AdviseSupervisory once per handle, then Write.
|
||||
await EnsureSupervisoryAdvisedAsync(session, serverHandle, itemHandle, ct).ConfigureAwait(false);
|
||||
reply = await session.WriteRawAsync(serverHandle, itemHandle, mxValue, _writeUserId, ct)
|
||||
await EnsureSupervisoryAdvisedAsync(ops, itemHandle, ct).ConfigureAwait(false);
|
||||
reply = await ops.WriteRawAsync(itemHandle, mxValue, _writeUserId, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -189,10 +205,10 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
|
||||
classification is SecurityClassification.SecuredWrite or SecurityClassification.VerifiedWrite;
|
||||
|
||||
private async Task<int> EnsureItemHandleAsync(
|
||||
MxGatewaySession session, int serverHandle, string fullRef, CancellationToken ct)
|
||||
IMxWriteOps ops, string fullRef, CancellationToken ct)
|
||||
{
|
||||
if (TryResolveCachedOrBorrowed(fullRef) is int resolved) return resolved;
|
||||
var handle = await session.AddItemAsync(serverHandle, fullRef, ct).ConfigureAwait(false);
|
||||
var handle = await ops.AddItemAsync(fullRef, ct).ConfigureAwait(false);
|
||||
Interlocked.Increment(ref _addItemCallCount);
|
||||
_itemHandles[fullRef] = handle;
|
||||
return handle;
|
||||
@@ -211,26 +227,26 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
|
||||
/// re-advises — the supervisory state never outlives the handle it was taken against.
|
||||
/// </summary>
|
||||
private async Task EnsureSupervisoryAdvisedAsync(
|
||||
MxGatewaySession session, int serverHandle, int itemHandle, CancellationToken ct)
|
||||
IMxWriteOps ops, int itemHandle, CancellationToken ct)
|
||||
{
|
||||
if (!_supervisedHandles.TryAdd(itemHandle, 0)) return;
|
||||
|
||||
var request = new MxCommandRequest
|
||||
{
|
||||
SessionId = session.SessionId,
|
||||
SessionId = ops.SessionId,
|
||||
ClientCorrelationId = Guid.NewGuid().ToString("N"),
|
||||
Command = new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.AdviseSupervisory,
|
||||
AdviseSupervisory = new AdviseSupervisoryCommand
|
||||
{
|
||||
ServerHandle = serverHandle,
|
||||
ServerHandle = ops.ServerHandle,
|
||||
ItemHandle = itemHandle,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var reply = await session.InvokeAsync(request, ct).ConfigureAwait(false);
|
||||
var reply = await ops.InvokeAsync(request, ct).ConfigureAwait(false);
|
||||
if (reply.ProtocolStatus is { } proto && proto.Code != ProtocolStatusCode.Ok)
|
||||
{
|
||||
// Supervisory advise failed — forget it so the next write retries, and let the
|
||||
@@ -250,14 +266,14 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
|
||||
/// interprets the underlying MXAccess command kind.
|
||||
/// </summary>
|
||||
private static Task<MxCommandReply> InvokeWriteSecuredAsync(
|
||||
MxGatewaySession session, int serverHandle, int itemHandle, MxValue value, CancellationToken ct)
|
||||
IMxWriteOps ops, int itemHandle, MxValue value, CancellationToken ct)
|
||||
{
|
||||
var command = new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.WriteSecured,
|
||||
WriteSecured = new WriteSecuredCommand
|
||||
{
|
||||
ServerHandle = serverHandle,
|
||||
ServerHandle = ops.ServerHandle,
|
||||
ItemHandle = itemHandle,
|
||||
Value = value,
|
||||
CurrentUserId = 0,
|
||||
@@ -266,11 +282,11 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
|
||||
};
|
||||
var request = new MxCommandRequest
|
||||
{
|
||||
SessionId = session.SessionId,
|
||||
SessionId = ops.SessionId,
|
||||
ClientCorrelationId = Guid.NewGuid().ToString("N"),
|
||||
Command = command,
|
||||
};
|
||||
return session.InvokeAsync(request, ct);
|
||||
return ops.InvokeAsync(request, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -301,3 +317,75 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
|
||||
return new WriteResult(StatusCodeMap.Good);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The per-write MXAccess gateway operations the writer needs, extracted behind an internal seam so the
|
||||
/// fail-closed write pipeline (archreview 06/S-1) is unit-testable — the SDK <c>MxGatewaySession</c>
|
||||
/// types are sealed with internal ctors and cannot be faked. Production binds
|
||||
/// <see cref="SessionMxWriteOps"/> over the real <c>(MxGatewaySession, serverHandle)</c>; tests inject a
|
||||
/// fake that records calls and returns canned replies.
|
||||
/// </summary>
|
||||
internal interface IMxWriteOps
|
||||
{
|
||||
/// <summary>The gateway session id stamped onto every <see cref="MxCommandRequest"/>.</summary>
|
||||
string SessionId { get; }
|
||||
|
||||
/// <summary>The MXAccess server handle carried in each item/advise/write command.</summary>
|
||||
int ServerHandle { get; }
|
||||
|
||||
/// <summary>Add an MXAccess item and return its item handle.</summary>
|
||||
/// <param name="fullRef">The dotted tag full reference.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>The MXAccess item handle.</returns>
|
||||
Task<int> AddItemAsync(string fullRef, CancellationToken ct);
|
||||
|
||||
/// <summary>Invoke a raw <see cref="MxCommand"/> (AdviseSupervisory / WriteSecured) and return the reply.</summary>
|
||||
/// <param name="request">The command request.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>The gateway reply.</returns>
|
||||
Task<MxCommandReply> InvokeAsync(MxCommandRequest request, CancellationToken ct);
|
||||
|
||||
/// <summary>Issue a raw (non-secured) Write for the given item handle.</summary>
|
||||
/// <param name="itemHandle">The MXAccess item handle to write.</param>
|
||||
/// <param name="value">The encoded MX value.</param>
|
||||
/// <param name="userId">The write user id (typically 0 — the supervisory path commits it).</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>The gateway reply.</returns>
|
||||
Task<MxCommandReply> WriteRawAsync(int itemHandle, MxValue value, int userId, CancellationToken ct);
|
||||
}
|
||||
|
||||
/// <summary>Production <see cref="IMxWriteOps"/> — a thin adapter over a live
|
||||
/// <see cref="MxGatewaySession"/> and its server handle. Behaviour is bit-identical to the previous
|
||||
/// inline <c>(session, serverHandle)</c> calls.</summary>
|
||||
internal sealed class SessionMxWriteOps : IMxWriteOps
|
||||
{
|
||||
private readonly MxGatewaySession _session;
|
||||
private readonly int _serverHandle;
|
||||
|
||||
/// <summary>Initializes the adapter over a connected session + server handle.</summary>
|
||||
/// <param name="session">The connected gateway session.</param>
|
||||
/// <param name="serverHandle">The MXAccess server handle for this session.</param>
|
||||
public SessionMxWriteOps(MxGatewaySession session, int serverHandle)
|
||||
{
|
||||
_session = session;
|
||||
_serverHandle = serverHandle;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SessionId => _session.SessionId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int ServerHandle => _serverHandle;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> AddItemAsync(string fullRef, CancellationToken ct)
|
||||
=> _session.AddItemAsync(_serverHandle, fullRef, ct);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<MxCommandReply> InvokeAsync(MxCommandRequest request, CancellationToken ct)
|
||||
=> _session.InvokeAsync(request, ct);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<MxCommandReply> WriteRawAsync(int itemHandle, MxValue value, int userId, CancellationToken ct)
|
||||
=> _session.WriteRawAsync(_serverHandle, itemHandle, value, userId, ct);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user