Fix: FHIR bundle processing has no rollback on partial failure

This commit is contained in:
voltsrage
2026-06-21 17:01:16 +08:00
parent 3eb937ef98
commit a91d79f3cd
3 changed files with 22 additions and 13 deletions
@@ -66,7 +66,10 @@ public class ObservationService : IObservationService
{
using var timer = _metrics.ObservationIngestDuration.NewTimer();
await using var tx = await _db.Database.BeginTransactionAsync();
// Re-use an outer transaction (e.g. FhirBundleProcessor) when one is already active.
await using var tx = _db.Database.CurrentTransaction is null
? await _db.Database.BeginTransactionAsync()
: null;
try
{
// Step 4 — insert observation
@@ -96,10 +99,6 @@ public class ObservationService : IObservationService
ClinicalAlert? alert = null;
// Step 6 — critical threshold detection (synchronous)
// WARNING detection is intentionally deferred to the Kafka consumer.
// A critical potassium of 2.1 mEq/L is immediately life-threatening — the alert
// must exist before this API call returns. A warning heart rate of 95 bpm warrants
// attention but not an emergency page; the additional Kafka latency is clinically safe.
if (IsCriticalBreach(req.Value, threshold))
{
alert = new ClinicalAlert
@@ -116,7 +115,6 @@ public class ObservationService : IObservationService
};
_db.ClinicalAlerts.Add(alert);
// Step 6b — outbox event for the alert (relay picks this up in Phase 3)
_db.OutboxEvents.Add(BuildOutboxEvent("alert.generated", new
{
alertId = alert.Id,
@@ -149,7 +147,7 @@ public class ObservationService : IObservationService
// Step 8 — COMMIT
await _db.SaveChangesAsync();
await tx.CommitAsync();
if (tx is not null) await tx.CommitAsync();
_metrics.ObservationsIngestedTotal
.WithLabels(req.ObservationCode, req.Source.ToDbString())
@@ -173,9 +171,7 @@ public class ObservationService : IObservationService
}
catch (DbUpdateException ex) when (IsUniqueViolation(ex))
{
// Race condition: two concurrent retries both passed the pre-check above.
// The unique partial index caught it. Roll back and return the existing row.
await tx.RollbackAsync();
if (tx is not null) await tx.RollbackAsync();
var existing = await _db.Observations
.AsNoTracking()
.FirstOrDefaultAsync(o => o.IdempotencyKey == req.IdempotencyKey);
@@ -185,7 +181,7 @@ public class ObservationService : IObservationService
}
catch
{
await tx.RollbackAsync();
if (tx is not null) await tx.RollbackAsync();
throw;
}
}
@@ -113,9 +113,14 @@ public class OrderService : IOrderService
order.ResultedAt = DateTimeOffset.UtcNow;
order.ResultSummary = req.ResultSummary;
await using var tx = _db.Database.CurrentTransaction is null
? await _db.Database.BeginTransactionAsync()
: null;
await _db.SaveChangesAsync();
var bundleService = _serviceProvider.GetRequiredService<ISepsisBundleService>();
await bundleService.OnOrderResultedAsync(order.Id);
if (tx is not null) await tx.CommitAsync();
return order;
}
}