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
@@ -611,6 +611,84 @@ public class SiteStorageService
await command.ExecuteNonQueryAsync();
}
/// <summary>
/// Applies a coalesced set of native-alarm mirror changes for one instance/source — upserts AND
/// deletes — in a SINGLE connection + transaction (review 2026-10-21 01/SR-P2). Deletes used
/// to be one connection + autocommit (one WAL fsync + one CDC row) per dropped condition; a
/// snapshot swap dropping N stale conditions now costs one commit. The caller guarantees each
/// source reference appears at most once across <paramref name="upserts"/> and
/// <paramref name="deletes"/> (the actor coalesces per reference), so statement order is irrelevant.
/// </summary>
/// <param name="instanceName">Unique name of the instance owning the alarms.</param>
/// <param name="sourceCanonicalName">Canonical name of the source binding.</param>
/// <param name="upserts">The conditions to upsert.</param>
/// <param name="deletes">Source references whose rows are removed.</param>
/// <returns>A task that completes when the whole batch has been committed.</returns>
public async Task ApplyNativeAlarmChangesAsync(
string instanceName, string sourceCanonicalName,
IReadOnlyList<(string SourceReference, string ConditionJson, string? MetadataJson, DateTimeOffset LastTransitionAt)> upserts,
IReadOnlyList<string> deletes)
{
if (upserts.Count == 0 && deletes.Count == 0)
return;
await using var connection = OpenConnection();
await using var transaction = (SqliteTransaction)await connection.BeginTransactionAsync();
if (deletes.Count > 0)
{
await using var del = connection.CreateCommand();
del.Transaction = transaction;
del.CommandText = @"
DELETE FROM native_alarm_state
WHERE instance_unique_name = @name
AND source_canonical_name = @source
AND source_reference = @ref";
del.Parameters.AddWithValue("@name", instanceName);
del.Parameters.AddWithValue("@source", sourceCanonicalName);
var dRef = del.Parameters.Add("@ref", SqliteType.Text);
foreach (var reference in deletes)
{
dRef.Value = reference;
await del.ExecuteNonQueryAsync();
}
}
if (upserts.Count > 0)
{
await using var command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = @"
INSERT INTO native_alarm_state
(instance_unique_name, source_canonical_name, source_reference, condition_json, last_transition_at, metadata_json)
VALUES (@name, @source, @ref, @json, @at, @meta)
ON CONFLICT(instance_unique_name, source_canonical_name, source_reference) DO UPDATE SET
condition_json = excluded.condition_json,
last_transition_at = excluded.last_transition_at,
metadata_json = excluded.metadata_json";
var pName = command.Parameters.Add("@name", SqliteType.Text);
var pSource = command.Parameters.Add("@source", SqliteType.Text);
var pRef = command.Parameters.Add("@ref", SqliteType.Text);
var pJson = command.Parameters.Add("@json", SqliteType.Text);
var pAt = command.Parameters.Add("@at", SqliteType.Text);
var pMeta = command.Parameters.Add("@meta", SqliteType.Text);
pName.Value = instanceName;
pSource.Value = sourceCanonicalName;
foreach (var row in upserts)
{
pRef.Value = row.SourceReference;
pJson.Value = row.ConditionJson;
pAt.Value = row.LastTransitionAt.ToString("O");
pMeta.Value = (object?)row.MetadataJson ?? DBNull.Value;
await command.ExecuteNonQueryAsync();
}
}
await transaction.CommitAsync();
}
/// <summary>
/// Returns all mirrored native alarm conditions for an instance's source binding,
/// used to rehydrate a NativeAlarmActor on (re)start.