feat(auditlog): thread ParentExecutionId through S&F for retry-loop cached rows
The store-and-forward retry loop emits the per-attempt and terminal cached audit rows (ApiCallCached/DbWriteCached Attempted, CachedResolve) via CachedCallLifecycleBridge from a CachedCallAttemptContext, not from the script context. The ExecutionId rollout (Task 4) already threaded ExecutionId and SourceScript through this path; ParentExecutionId — the spawning inbound-API request's ExecutionId — was not, so those retry-loop rows had ParentExecutionId = null even for an inbound-API-routed run. Thread it additively as a sibling at every carry point ExecutionId passes through: - StoreAndForwardMessage gains ParentExecutionId (Guid?). - StoreAndForwardStorage adds a nullable parent_execution_id column via the same idempotent PRAGMA-probed ALTER TABLE migration; rows persisted by an older build read back null (back-compat). The defensive Guid.TryParse read helper (ParseExecutionId) is renamed ParseGuidColumn and reused for both columns so a corrupt value cannot abort the retry sweep. - StoreAndForwardService.EnqueueAsync gains an optional parentExecutionId param, stamped onto the buffered message and surfaced on the CachedCallAttemptContext built in the retry loop. - CachedCallAttemptContext gains ParentExecutionId. - CachedCallLifecycleBridge.BuildPacket sets AuditEvent.ParentExecutionId from the context, beside the existing ExecutionId. - IExternalSystemClient.CachedCallAsync / IDatabaseGateway.CachedWriteAsync gain an optional parentExecutionId param; ScriptRuntimeContext's CachedCall / CachedWrite helpers pass _parentExecutionId. All threading is additive — ParentExecutionId is Guid? everywhere, null for non-routed runs, and old buffered S&F rows still deserialize with the new field null.
This commit is contained in:
@@ -75,7 +75,7 @@ public class DatabaseCachedWriteEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
@@ -116,7 +116,7 @@ public class DatabaseCachedWriteEmissionTests
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
@@ -145,7 +145,7 @@ public class DatabaseCachedWriteEmissionTests
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
@@ -167,7 +167,7 @@ public class DatabaseCachedWriteEmissionTests
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
@@ -181,7 +181,7 @@ public class DatabaseCachedWriteEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
trackedId,
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ public class DatabaseCachedWriteEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
@@ -221,7 +221,79 @@ public class DatabaseCachedWriteEmissionTests
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.Is<Guid?>(id => id == TestExecutionId),
|
||||
It.Is<string?>(s => s == SourceScript)),
|
||||
It.Is<string?>(s => s == SourceScript),
|
||||
It.IsAny<Guid?>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId Task 6): the helper → gateway hop for
|
||||
/// <c>ParentExecutionId</c>. A cached write enqueued from an inbound-API-
|
||||
/// routed script run must forward the runtime context's
|
||||
/// <c>ParentExecutionId</c> verbatim into
|
||||
/// <see cref="IDatabaseGateway.CachedWriteAsync"/> so the buffered retry
|
||||
/// loop later stamps it onto its audit rows.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CachedWrite_ThreadsParentExecutionId_IntoGateway()
|
||||
{
|
||||
var parentExecutionId = Guid.NewGuid();
|
||||
var gateway = new Mock<IDatabaseGateway>();
|
||||
gateway
|
||||
.Setup(g => g.CachedWriteAsync(
|
||||
"myDb", "INSERT INTO t VALUES (1)",
|
||||
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
var helper = CreateHelper(gateway.Object, forwarder, parentExecutionId);
|
||||
await helper.CachedWrite("myDb", "INSERT INTO t VALUES (1)");
|
||||
|
||||
gateway.Verify(g => g.CachedWriteAsync(
|
||||
"myDb", "INSERT INTO t VALUES (1)",
|
||||
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(),
|
||||
It.Is<Guid?>(id => id == parentExecutionId)),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId Task 6): a non-routed run threads a
|
||||
/// <c>null</c> ParentExecutionId into the gateway — the additive default.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CachedWrite_NonRoutedRun_ThreadsNullParentExecutionId_IntoGateway()
|
||||
{
|
||||
var gateway = new Mock<IDatabaseGateway>();
|
||||
gateway
|
||||
.Setup(g => g.CachedWriteAsync(
|
||||
"myDb", "INSERT INTO t VALUES (1)",
|
||||
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
var helper = CreateHelper(gateway.Object, forwarder);
|
||||
await helper.CachedWrite("myDb", "INSERT INTO t VALUES (1)");
|
||||
|
||||
gateway.Verify(g => g.CachedWriteAsync(
|
||||
"myDb", "INSERT INTO t VALUES (1)",
|
||||
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(),
|
||||
It.Is<Guid?>(id => id == null)),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -236,7 +308,7 @@ public class DatabaseCachedWriteEmissionTests
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
var forwarder = new CapturingForwarder
|
||||
{
|
||||
@@ -253,7 +325,7 @@ public class DatabaseCachedWriteEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
trackedId,
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()),
|
||||
Times.Once);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.ReturnsAsync(new ExternalCallResult(true, null, null, WasBuffered: true));
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
@@ -121,7 +121,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.ReturnsAsync(new ExternalCallResult(true, "{\"ok\":true}", null, WasBuffered: false));
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
@@ -158,7 +158,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.ReturnsAsync(new ExternalCallResult(true, null, null, WasBuffered: true));
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
@@ -178,7 +178,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
id1,
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()),
|
||||
Times.Once);
|
||||
client.Verify(c => c.CachedCallAsync(
|
||||
"ERP", "GetOrder",
|
||||
@@ -186,7 +186,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
id2,
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.ReturnsAsync(new ExternalCallResult(true, null, null, WasBuffered: true));
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
@@ -226,7 +226,79 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.Is<Guid?>(id => id == TestExecutionId),
|
||||
It.Is<string?>(s => s == SourceScript)),
|
||||
It.Is<string?>(s => s == SourceScript),
|
||||
It.IsAny<Guid?>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId Task 6): the helper → gateway hop for
|
||||
/// <c>ParentExecutionId</c>. A cached call enqueued from an inbound-API-
|
||||
/// routed script run must forward the runtime context's
|
||||
/// <c>ParentExecutionId</c> verbatim into
|
||||
/// <see cref="IExternalSystemClient.CachedCallAsync"/> so the buffered
|
||||
/// retry loop later stamps it onto its audit rows.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CachedCall_ThreadsParentExecutionId_IntoClient()
|
||||
{
|
||||
var parentExecutionId = Guid.NewGuid();
|
||||
var client = new Mock<IExternalSystemClient>();
|
||||
client
|
||||
.Setup(c => c.CachedCallAsync(
|
||||
"ERP", "GetOrder",
|
||||
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.ReturnsAsync(new ExternalCallResult(true, null, null, WasBuffered: true));
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
var helper = CreateHelper(client.Object, forwarder, parentExecutionId);
|
||||
await helper.CachedCall("ERP", "GetOrder");
|
||||
|
||||
client.Verify(c => c.CachedCallAsync(
|
||||
"ERP", "GetOrder",
|
||||
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(),
|
||||
It.Is<Guid?>(id => id == parentExecutionId)),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId Task 6): a non-routed run threads a
|
||||
/// <c>null</c> ParentExecutionId into the client — the additive default.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CachedCall_NonRoutedRun_ThreadsNullParentExecutionId_IntoClient()
|
||||
{
|
||||
var client = new Mock<IExternalSystemClient>();
|
||||
client
|
||||
.Setup(c => c.CachedCallAsync(
|
||||
"ERP", "GetOrder",
|
||||
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.ReturnsAsync(new ExternalCallResult(true, null, null, WasBuffered: true));
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
var helper = CreateHelper(client.Object, forwarder);
|
||||
await helper.CachedCall("ERP", "GetOrder");
|
||||
|
||||
client.Verify(c => c.CachedCallAsync(
|
||||
"ERP", "GetOrder",
|
||||
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(),
|
||||
It.Is<Guid?>(id => id == null)),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -241,7 +313,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.ReturnsAsync(new ExternalCallResult(true, null, null, WasBuffered: true));
|
||||
var forwarder = new CapturingForwarder
|
||||
{
|
||||
@@ -261,7 +333,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
trackedId,
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -276,7 +348,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.ReturnsAsync(new ExternalCallResult(true, null, null, WasBuffered: true));
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
@@ -303,7 +375,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.ReturnsAsync(new ExternalCallResult(true, null, null, WasBuffered: true));
|
||||
|
||||
var helper = CreateHelper(client.Object, forwarder: null);
|
||||
@@ -316,7 +388,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
trackedId,
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -346,7 +418,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
// WasBuffered=false — the immediate HTTP attempt succeeded; S&F
|
||||
// is bypassed entirely.
|
||||
.ReturnsAsync(new ExternalCallResult(true, "{\"ok\":true}", null, WasBuffered: false));
|
||||
@@ -412,7 +484,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.ReturnsAsync(new ExternalCallResult(true, "{\"ok\":true}", null, WasBuffered: false));
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
@@ -442,7 +514,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
.ReturnsAsync(new ExternalCallResult(
|
||||
false, null, "Permanent error: HTTP 422 bad payload", WasBuffered: false));
|
||||
var forwarder = new CapturingForwarder();
|
||||
@@ -485,7 +557,7 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>(),
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>()))
|
||||
It.IsAny<Guid?>(), It.IsAny<string?>(), It.IsAny<Guid?>()))
|
||||
// S&F took ownership — Attempted + Resolve come from the
|
||||
// CachedCallLifecycleBridge driven by the retry loop, not the helper.
|
||||
.ReturnsAsync(new ExternalCallResult(true, null, null, WasBuffered: true));
|
||||
|
||||
Reference in New Issue
Block a user