fix(dcl): robust static-tag seeding — bounded-retry+log initial seed (#1) and re-seed on reconnect (#3)

STATIC tags (no further OnDataChange after advise) depend entirely on the
seed read. Pre-fix HandleSubscribe seeded only on Success && Value != null,
silently dropping a seed that raced the just-created advise (VT_EMPTY) — so a
static tag stayed Uncertain forever while the source read Good. ReSubscribeAll
did no seeding at all, so a static tag could not self-heal across reconnect.

- New SeedTagsAsync helper: per-tag ReadAsync (not a bulk read — some gateways
  time out on large batches) with round-based bounded retry
  (SeedReadMaxAttempts/SeedReadRetryDelay), logging any tag that never yields a
  value (named — previously zero log trace).
- HandleSubscribe seed loop delegates to SeedTagsAsync.
- ReSubscribeAll re-seeds re-advised tags after reconnect via the
  generation-guarded TagValueReceived path (fan-out keys off
  _subscriptionsByInstance, preserved across reconnect).

Diagnosed live on wonder-app-vd03 2026-06-17 (see scadabridge-dcl-static-tag-false-bad).
Mechanism #2 (single transient-bad push) left as a follow-up.
This commit is contained in:
Joseph Doherty
2026-06-18 09:23:23 -04:00
parent 3782ebdadb
commit 72aec3b4d4
3 changed files with 229 additions and 9 deletions
@@ -20,4 +20,24 @@ public class DataConnectionOptions
/// disconnect toward the failover retry count (DataConnectionLayer-009).
/// </summary>
public TimeSpan StableConnectionThreshold { get; set; } = TimeSpan.FromSeconds(60);
/// <summary>
/// DataConnectionLayer-027: total number of attempts the seed read makes per tag
/// before giving up. The initial subscribe seed (and the reconnect re-seed) reads
/// each just-advised tag to capture its current value; on a cold/fresh advise the
/// read can race the subscription and return an empty/failed result. A STATIC tag
/// (one that never fires another OnDataChange) would then stay Uncertain forever,
/// because its bridge value depends entirely on that seed. Re-reading the still-empty
/// subset a bounded number of times lets the advise warm up. Minimum 1 (1 = single
/// read, no retry).
/// </summary>
public int SeedReadMaxAttempts { get; set; } = 3;
/// <summary>
/// DataConnectionLayer-027: delay between seed-read attempts for the tags that have
/// not yet returned a usable value. Applied once per round across the whole pending
/// subset, so the total added latency is bounded to
/// <c>(SeedReadMaxAttempts - 1) × SeedReadRetryDelay</c> regardless of tag count.
/// </summary>
public TimeSpan SeedReadRetryDelay { get; set; } = TimeSpan.FromMilliseconds(250);
}