fix(configuration): resolve Low code-review findings (Configuration-004,005,007,010,011)
- Configuration-004: NodePermissions stored as int to match the EF HasConversion<int>() in OtOpcUaConfigDbContext.ConfigureNodeAcl. - Configuration-005: serialise LiteDbConfigCache.PutAsync so concurrent Put for the same (ClusterId, GenerationId) cannot duplicate rows. - Configuration-007: rethrow OperationCanceledException from GenerationApplier.ApplyPass when the caller's token is cancelled. - Configuration-010: scrub secrets and drop the full exception object from the ResilientConfigReader fallback warning log. - Configuration-011: pin the previously-uncovered GenerationApplier cancellation and path-length / publish-validation paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,38 @@ public sealed class LiteDbConfigCacheTests : IDisposable
|
||||
(await cache.GetMostRecentAsync("c-1"))!.PayloadJson.ShouldBe("{\"v\":2}");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Configuration-005 — concurrent PutAsync for the same (ClusterId, GenerationId) must
|
||||
// not produce duplicate rows. The original find-then-insert was non-atomic so two racing
|
||||
// callers could both observe `existing is null` and both Insert.
|
||||
// ------------------------------------------------------------------------------------
|
||||
[Fact]
|
||||
public async Task PutAsync_concurrent_for_same_cluster_and_generation_does_not_duplicate()
|
||||
{
|
||||
using var cache = new LiteDbConfigCache(_dbPath);
|
||||
// Pre-seed gen=99 so prune keepLatest:1 has a sentinel that survives independent of
|
||||
// any potential duplicate (gen=42) row count.
|
||||
await cache.PutAsync(Snapshot("c-1", 99));
|
||||
|
||||
// Many parallel writes for the same key. Without serialization, racing find-then-insert
|
||||
// would Insert multiple rows for the same (ClusterId, GenerationId=42).
|
||||
var tasks = Enumerable.Range(0, 64).Select(_ => Task.Run(async () =>
|
||||
{
|
||||
var s = Snapshot("c-1", 42);
|
||||
await cache.PutAsync(s);
|
||||
})).ToArray();
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// Count rows for gen=42 directly by inspecting the LiteDB file via a fresh handle.
|
||||
cache.Dispose();
|
||||
using var verify = new LiteDB.LiteDatabase(_dbPath);
|
||||
var col = verify.GetCollection<GenerationSnapshot>("generations");
|
||||
var gen42Count = col.Find(s => s.ClusterId == "c-1" && s.GenerationId == 42).Count();
|
||||
gen42Count.ShouldBe(1,
|
||||
$"PutAsync must upsert atomically — found {gen42Count} rows for (c-1, gen=42) after 64 concurrent puts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Corrupt_file_surfaces_as_LocalConfigCacheCorruptException()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user