fix(site-runtime): batch native-alarm deletes into the coalesced flush transaction; docs for scripts/cache/scheduler (review 2026-10-21 01/SR-P2)

This commit is contained in:
Joseph Doherty
2026-10-22 08:33:30 -04:00
parent 010d2ea438
commit 37c25db2f1
5 changed files with 148 additions and 18 deletions
@@ -60,10 +60,12 @@ public class NativeAlarmActor : ReceiveActor
/// source reference keeps only its LATEST transition, flushed as one batched SQLite write on
/// a short single-shot timer rather than a write per transition. A crash loses at most one
/// flush interval of mirror writes; the (re)subscribe snapshot swap reconciles the mirror, so
/// the durability trade is safe. A <see cref="PersistDelete"/> removes the ref here so a
/// deleted condition is not resurrected by a still-pending upsert.
/// the durability trade is safe. A <see cref="PersistDelete"/> records a <c>null</c> entry for
/// the ref (upsert/delete discriminator), replacing any pending upsert so a deleted condition
/// is not resurrected; a later upsert for the same ref replaces the delete. Both kinds are
/// applied in the one batched flush transaction (review 2026-10-21 01/SR-P2).
/// </summary>
private readonly Dictionary<string, NativeAlarmTransition> _dirtyUpserts = new();
private readonly Dictionary<string, NativeAlarmTransition?> _dirtyUpserts = new();
private ICancelable? _retryTimer;
private ICancelable? _flushTimer;
@@ -556,6 +558,11 @@ public class NativeAlarmActor : ReceiveActor
private void MarkDirtyUpsert(NativeAlarmTransition t)
{
_dirtyUpserts[t.SourceReference] = t;
ArmFlushTimer();
}
private void ArmFlushTimer()
{
if (_flushTimer == null)
{
_flushTimer = Context.System.Scheduler.ScheduleTellOnceCancelable(
@@ -575,6 +582,8 @@ public class NativeAlarmActor : ReceiveActor
return;
var batch = _dirtyUpserts.Values
.Where(t => t is not null)
.Select(t => t!)
.Select(t => (
t.SourceReference,
JsonSerializer.Serialize(t.Condition),
@@ -582,26 +591,24 @@ public class NativeAlarmActor : ReceiveActor
t.AlarmTypeName, t.Category, t.Message, t.CurrentValue, t.LimitValue, t.AckTime)),
t.TransitionTime))
.ToList();
var deletes = _dirtyUpserts.Where(kv => kv.Value is null).Select(kv => kv.Key).ToList();
_dirtyUpserts.Clear();
_storage.UpsertNativeAlarmsAsync(_instanceName, _source.CanonicalName, batch)
_storage.ApplyNativeAlarmChangesAsync(_instanceName, _source.CanonicalName, batch, deletes)
.ContinueWith(
task => _logger.LogWarning(task.Exception,
"Failed to persist {Count} native alarms on {Instance}", batch.Count, _instanceName),
"Failed to persist {Count} native alarm changes on {Instance}",
batch.Count + deletes.Count, _instanceName),
TaskContinuationOptions.OnlyOnFaulted);
}
private void PersistDelete(string sourceReference)
{
// Drop any pending coalesced upsert for this ref so the delete is not resurrected by a
// still-buffered upsert on the next flush (P4).
_dirtyUpserts.Remove(sourceReference);
_storage.DeleteNativeAlarmAsync(_instanceName, _source.CanonicalName, sourceReference)
.ContinueWith(
task => _logger.LogWarning(task.Exception,
"Failed to delete native alarm {Ref} on {Instance}", sourceReference, _instanceName),
TaskContinuationOptions.OnlyOnFaulted);
// Queue the delete in the same dirty set as upserts (a null entry replaces any pending
// upsert, so the delete is not resurrected) and let the coalesced flush apply it in the
// batched transaction — one commit for N dropped conditions instead of N.
_dirtyUpserts[sourceReference] = null;
ArmFlushTimer();
}
// ── Internal messages ──