test: create test for concurrency

This commit is contained in:
voltsrage
2026-06-23 05:17:22 +08:00
parent 383df0dde5
commit d8e142fffe
8 changed files with 2625 additions and 58 deletions
@@ -55,5 +55,9 @@ public class EncounterConfiguration : IEntityTypeConfiguration<Encounter>
builder.HasIndex(e => new { e.PatientId, e.AdmittedAt });
builder.HasIndex(e => new { e.Status, e.AdmittedAt })
.HasFilter("status = 'ACTIVE'");
builder.HasIndex(e => new { e.PatientId, e.EncounterType })
.IsUnique()
.HasDatabaseName("ix_encounters_patient_active_type")
.HasFilter("status = 'ACTIVE'");
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace VigilCareClinicalAPI.Migrations
{
/// <inheritdoc />
public partial class AddEncounterActiveTypeUniqueIndex : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "ix_encounters_patient_active_type",
table: "encounters",
columns: new[] { "patient_id", "encounter_type" },
unique: true,
filter: "status = 'ACTIVE'");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "ix_encounters_patient_active_type",
table: "encounters");
}
}
}
@@ -532,6 +532,11 @@ namespace VigilCareClinicalAPI.Migrations
b.HasIndex("PatientId", "AdmittedAt");
b.HasIndex("PatientId", "EncounterType")
.IsUnique()
.HasDatabaseName("ix_encounters_patient_active_type")
.HasFilter("status = 'ACTIVE'");
b.HasIndex("Status", "AdmittedAt")
.HasFilter("status = 'ACTIVE'");
@@ -232,7 +232,20 @@ public class PatientService : IPatientService
CreatedAt = DateTimeOffset.UtcNow
});
await _db.SaveChangesAsync();
try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateException ex)
when (ex.InnerException is Npgsql.PostgresException { SqlState: "23505" } pg
&& pg.ConstraintName == "ix_encounters_patient_active_type")
{
_db.ChangeTracker.Clear();
throw new ConflictException(
"Patient already has an active encounter of this type.",
"DUPLICATE_ACTIVE_ENCOUNTER");
}
return encounter;
}
@@ -20,7 +20,7 @@ public class SepsisBundleService : ISepsisBundleService
if (alertType != AlertType.SofaSepsis)
throw new InvalidOperationException(
$"Sepsis bundle can only be triggered by SOFA_SEPSIS, not {alertType.ToDbString()}.");
var exists = await _db.SepsisBundles
.AnyAsync(b => b.EncounterId == encounterId
&& b.ComplianceStatus == SepsisBundleComplianceStatus.InProgress, ct);
@@ -28,73 +28,75 @@ public class SepsisBundleService : ISepsisBundleService
if (exists)
return null;
var recognizedAt = DateTimeOffset.UtcNow;
var bundle = new SepsisBundle
{
Id = Guid.NewGuid(),
EncounterId = encounterId,
TriggeringAlertId = triggeringAlertId,
TriggeringAlertType = alertType.ToDbString(),
RecognizedAt = recognizedAt,
DeadlineAt = recognizedAt.AddHours(1),
ComplianceStatus = SepsisBundleComplianceStatus.InProgress
};
var elementDefs = new (SepsisBundleElementType Type, OrderType OrderType, string Description)[]
{
(SepsisBundleElementType.BloodCultures, OrderType.Lab, "SEP-1: Blood cultures"),
(SepsisBundleElementType.SerumLactate, OrderType.Lab, "SEP-1: Serum lactate"),
(SepsisBundleElementType.BroadSpectrumAntibiotics, OrderType.Medication, "SEP-1: Broad-spectrum antibiotics"),
(SepsisBundleElementType.IvFluidResuscitation, OrderType.Procedure, "SEP-1: IV fluid bolus"),
};
foreach (var (elementType, orderType, description) in elementDefs)
{
var order = await _orderService.CreateAsync(encounterId, new CreateOrderRequest(
orderType, description, "sepsis-bundle-engine"));
bundle.Elements.Add(new SepsisBundleElement
{
Id = Guid.NewGuid(),
ElementType = elementType,
Status = SepsisBundleElementStatus.Pending,
OrderId = order.Id
});
}
_db.SepsisBundles.Add(bundle);
_db.OutboxEvents.Add(new OutboxEvent
{
Id = Guid.NewGuid(),
Topic = "sepsis.bundle.created",
Payload = JsonSerializer.Serialize(new
{
bundleId = bundle.Id,
encounterId,
triggeringAlertId,
triggeringAlertType = alertType.ToDbString(),
recognizedAt,
deadlineAt = bundle.DeadlineAt,
partitionKey = encounterId.ToString()
}),
PartitionKey = encounterId.ToString(),
CreatedAt = DateTimeOffset.UtcNow
});
await using var tx = await _db.Database.BeginTransactionAsync(ct);
try
{
var recognizedAt = DateTimeOffset.UtcNow;
var bundle = new SepsisBundle
{
Id = Guid.NewGuid(),
EncounterId = encounterId,
TriggeringAlertId = triggeringAlertId,
TriggeringAlertType = alertType.ToDbString(),
RecognizedAt = recognizedAt,
DeadlineAt = recognizedAt.AddHours(1),
ComplianceStatus = SepsisBundleComplianceStatus.InProgress
};
var elementDefs = new (SepsisBundleElementType Type, OrderType OrderType, string Description)[]
{
(SepsisBundleElementType.BloodCultures, OrderType.Lab, "SEP-1: Blood cultures"),
(SepsisBundleElementType.SerumLactate, OrderType.Lab, "SEP-1: Serum lactate"),
(SepsisBundleElementType.BroadSpectrumAntibiotics, OrderType.Medication, "SEP-1: Broad-spectrum antibiotics"),
(SepsisBundleElementType.IvFluidResuscitation, OrderType.Procedure, "SEP-1: IV fluid bolus"),
};
foreach (var (elementType, orderType, description) in elementDefs)
{
var order = await _orderService.CreateAsync(encounterId, new CreateOrderRequest(
orderType, description, "sepsis-bundle-engine"));
bundle.Elements.Add(new SepsisBundleElement
{
Id = Guid.NewGuid(),
ElementType = elementType,
Status = SepsisBundleElementStatus.Pending,
OrderId = order.Id
});
}
_db.SepsisBundles.Add(bundle);
_db.OutboxEvents.Add(new OutboxEvent
{
Id = Guid.NewGuid(),
Topic = "sepsis.bundle.created",
Payload = JsonSerializer.Serialize(new
{
bundleId = bundle.Id,
encounterId,
triggeringAlertId,
triggeringAlertType = alertType.ToDbString(),
recognizedAt,
deadlineAt = bundle.DeadlineAt,
partitionKey = encounterId.ToString()
}),
PartitionKey = encounterId.ToString(),
CreatedAt = DateTimeOffset.UtcNow
});
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
return bundle;
}
catch (DbUpdateException ex)
when (ex.InnerException is Npgsql.PostgresException { SqlState: "23505" } pg
&& pg.ConstraintName == "ix_sepsis_bundles_encounter_in_progress")
{
await tx.RollbackAsync(ct);
_db.ChangeTracker.Clear();
return null;
}
return bundle;
}
public async Task<SepsisBundle?> GetCurrentByEncounterAsync(Guid encounterId)