Fix: Unify Outbox Pattern with IOutboxWriter (Architecture Consolidation)

**Issue Found & Resolved:**
- Discovered parallel Outbox/Inbox systems: building_blocks (pre-existing, ModelOperations/SignalEngine using) vs outbox (newly added)
- VIOLATION: IOutboxWriter registered singleton; multiple modules injected and actively using building_blocks.outbox_message
- ShadowRunJob was writing to separate outbox.outbox schema, breaking existing Outbox/Inbox pattern

**Architecture Fix:**
- ShadowRunJob now uses IOutboxWriter (injected) → building_blocks.outbox_message
- Eliminated: custom outbox.outbox insert logic (InsertOutboxEventAsync)
- Eliminated: parallel schema (outbox.outbox DDL migration 0007)
- Result: Single unified Outbox pattern via IOutboxWriter/IInboxStore interfaces

**Implementation:**
- ShadowRunJob: Added IDbConnectionFactory + IOutboxWriter dependencies
- Persist + Event: Single transaction (shadow_run + outbox_message inserted atomically)
- OutboxMessage: EventType="ShadowRunCompleted", SchemaVersion=1
- PayloadHash: SHA256.HashData (per CA1850 rule)
- Fallback: If AddAsync fails, transaction rolls back (no partial success)

**Downstream Consumers:**
- Existing OutboxPollerJob (unchanged): reads building_blocks.outbox_message → inbox_message
- ApprovalQueueConsumer: retains DB insert implementation (ready for Hangfire wiring later)
- AuditLogConsumer: retains Serilog structured logging (compliance audit via logs)

**Cleaned Up:**
- Removed: 0007_CreateOutboxTable.sql (separate schema not needed)
- Removed: ShadowRunOutboxPollerJob (existing OutboxPollerJob handles all events)
- Removed: ShadowRunCompletedInboxConsumerJob, ApprovalQueueInboxConsumerJob, AuditLogInboxConsumerJob (will integrate via existing consumer interfaces)
- Program.cs: Removed all new RecurringJob registrations

**AGENTS.md v16.0 Compliance:**
✓ Architecture: Unified via verified interface pattern (IOutboxWriter)
✓ Necessity: Grounded in existing code (ModelOperations, SignalEngine already using)
✓ Normalization: 3NF writes (atomic transaction)
✓ Idempotent: OutboxMessage deduplication via existing patterns
✓ Traceability: CorrelationId preserved end-to-end
✓ Safety: No partial success (transaction-wrapped)
✓ Debt: Consolidation (zero new parallel systems)

**Tests:** 84/84 passing (0 regressions)

**Next:** Integrate Consumers with Hangfire using unified Outbox pattern.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 12:48:04 +09:00
parent 121a6b35d8
commit 258bb17f3c
5 changed files with 112 additions and 84 deletions
@@ -118,51 +118,4 @@ public sealed class ShadowRunQueries(IDbConnectionFactory connectionFactory)
private static string SerializeValidationGates(ValidationGates gates)
=> System.Text.Json.JsonSerializer.Serialize(gates);
/// <summary>
/// Emit event to outbox (transactional with shadow run insert).
/// Used for async event-driven downstream consumers.
/// </summary>
public async Task InsertOutboxEventAsync(
Guid runId,
Guid modelId,
Guid correlationId,
bool allGatesPassed,
ShadowRunMetrics metrics,
CancellationToken cancellationToken)
{
const string sql = """
insert into outbox.outbox
(aggregate_id, event_type, payload)
values (
@RunId,
@EventType,
cast(@Payload as jsonb)
)
""";
var eventPayload = System.Text.Json.JsonSerializer.Serialize(new
{
RunId = runId,
ModelId = modelId,
CorrelationId = correlationId,
AllGatesPassed = allGatesPassed,
TotalReturn = metrics.TotalReturn,
SharpeRatio = metrics.SharpeRatio,
ProbOfBacktestOverfit = metrics.ProbOfBacktestOverfit,
DailySharePercentile = metrics.DailySharePercentile,
CompletedAt = DateTimeOffset.UtcNow
});
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
await connection.ExecuteAsync(
new CommandDefinition(
sql,
new
{
RunId = runId,
EventType = "ShadowRunCompleted",
Payload = eventPayload
},
cancellationToken: cancellationToken));
}
}