Commit Graph

2 Commits

Author SHA1 Message Date
Joseph Doherty fd64fed217 fix(focas): a stalled device fails fast for the rest of its batch; generation-stamp health (#740 review round 1)
I1: Task.WhenAll waits for the slowest device, so concurrency alone still let a device
that accepts TCP and then stalls cost (its tag count x call timeout) per batch. After the
first timeout, transport failure or comms-class status on a device, its remaining tags in
the batch now fail BadCommunicationError immediately, the same as the coalescer's
FailMembers, which now also stops after a failed range. FIXED: tags are still served
from the cache. A malformed address is BadNodeIdUnknown and does not count as a link
failure. Test: a 10-tag stalled device beside a healthy one completes in under 3 call
timeouts with the healthy value Good. The P9 test now also asserts per-client read
counts and that two reads overlapped across devices.

Minors:
- 1: a superseded init releases its table only if it was never published (flag).
- 2: a fixed-tree refusal credits LastSuccessfulRead; a value-path refusal after a comms
  fault restores Healthy.
- 5: alarm/fixed/refusal health is reset inside the publish lock, after the generation
  check.
- 6: FocasFixedTreeHealthTests wait on served-failure/refusal counters, not wall-clock.
- 8: data-path health writes (reads, coalescer, writes, poll errors, refusals) carry the
  generation they started under and are dropped once a lifecycle call has moved on. Each
  write is a check-and-set under the lifecycle lock.
Also adds a unit case where 20 vendor records with type 0 and empty messages stay vendor.

Refs #740
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 14:44:42 -04:00
Joseph Doherty 29b566d51b fix(focas): disambiguate the cnc_rdalmmsg2 record layout at 1,520-byte multiples (R07:H20)
ParseAlarms took the 76-byte vendor layout whenever Length % 76 == 0, so a legacy 80-byte
reply of exactly 19 records (1,520 B = 20 x 76) decoded as 20 garbage vendor records.

The response block carries no record count (its 16-byte header is length/class/path/
command/rc/payload length), so the layout is chosen from what is known: the record count
the request asked for (a reply never holds more), then each layout's field invariants on
every record. For legacy records, the int32 type/axis/length must fit a short and the
length must be at most 64. For vendor records, the i16 length must be at most 64. If both
still fit, the layout whose declared lengths match the decoded text wins. Vendor stays the
default when both readings are equally plausible, and unambiguous lengths are unchanged.

Refs #740
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 14:24:05 -04:00