fix(abcip): native runtime STRING capacity, mistype churn, malformed-name config error, LWORD type check; non-blocking lease release (#570, #579)

#570
- STRING: the built-in 88-byte Logix STRING holds 82 characters (LEN + DATA[82] + 2 pad), not 84; the
  write still sends the whole 88-byte structure and a read never decodes the pad as characters.
- Mistype churn: a type refusal (CIP 0xFF/0x2107) on the existence read no longer throws from
  InitializeAsync, so the runtime is not disposed/re-created (and re-Forward-Opened) every poll; the driver
  also keeps a runtime that throws TypeMismatch from init and records the device as reachable.
- Malformed tag name: a TagPath that does not parse, or a name the CIP symbolic encoder refuses (now also
  non-ASCII, empty member, >255-char member, unterminated subscript), is AbCipTagConfigurationException ->
  per-tag BadConfigurationError (named const in AbCipStatusMapper), warned once, never feeding the device
  backoff or health; siblings keep reading. A malformed probe tag no longer feeds the backoff either.
- Micro800 BYTE/WORD/LWORD (0xD1/0xD2/0xD4) are type-checked as integers of their width: REAL/LREAL or a
  narrower integer over LWORD is TypeMismatch, not decoded garbage.

#579
- ConnectionLease.ReleaseInBackground: one non-blocking release shape for both native AB runtimes. The
  AbCip runtime's Dispose no longer waits up to 2 s under the per-tag lock; the pool still registers the
  close synchronously so a re-create cannot overlap it. Offline soak tests (25 cycles, AbCip + AbLegacy)
  prove no leaked connection; FakeEipServer gains OpenConnections.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-07 23:05:49 -04:00
parent 04b6563286
commit 4e6eed7454
17 changed files with 799 additions and 46 deletions
+27 -2
View File
@@ -181,8 +181,33 @@ The driver has two interchangeable wire transports, selected by one `DriverConfi
an Unsupported refusal (final review I4): the tag's handle is kept (no re-create, no reconnect), driver
health is unchanged (no `Degraded`, nothing fed to the comms-loss watchdog), and the Warning is logged
**once per tag** per driver instance, not every poll. The same applies to the libplctag-path width check.
- Malformed tag names put the device into backoff, as under libplctag (#570). STRING capacity includes the
alignment pad (84, not 82) (#570).
The same holds when the controller refuses the **existence read itself** with a type error (CIP
`0xFF`/`0x2107`): initialization completes with the status instead of throwing, so the runtime is not
disposed and re-created (nor, for a sole lease holder, re-Forward-Opened) on every poll, and the device's
connect backoff is not opened (#570). Micro800 `BYTE`/`WORD`/`LWORD` (`0xD1`/`0xD2`/`0xD4`) are checked as
integers of their width: a `REAL`/`LREAL` or a narrower integer authored over an `LWORD` is
`BadTypeMismatch`, not decoded garbage (#570).
- **A malformed tag name is a per-tag configuration error** (#570). A `TagPath` that does not parse, or a
name the CIP symbolic encoder refuses (non-ASCII, an empty member such as `A..B`, a member longer than
255 characters, an unterminated or non-numeric subscript), reads and writes **`BadConfigurationError`**
with one Warning per tag. Nothing reaches the device, so it never opens the device's connect backoff and
never degrades driver health — healthy tags on the same device keep reading. (libplctag still reports its
own `ErrorBadParam` for a name it rejects.) A malformed **probe** tag does not feed the backoff either.
- **STRING capacity:** the built-in Logix `STRING` is 88 bytes on the wire — `LEN` + `DATA[82]` + 2 bytes of
alignment padding — so a write holds at most **82** characters (it used to allow 84) and a read never
returns the padding as characters. The write still sends the whole 88-byte structure, padding zeroed, as
libplctag does. A user string type of any other size keeps the buffer-derived capacity (`size − 4`): the
read alone cannot tell its `DATA` length from up to 3 bytes of padding, so a write may accept up to 3
characters more than the type holds (#570).
- **Releasing a tag never blocks** (#579). Disposing a native tag runtime — the driver does it under the
tag's lock on an evict, a reinitialize or a shutdown — releases its connection lease in the background
(`ConnectionLease.ReleaseInBackground`, the same shape the native AbLegacy runtime uses). It used to wait
up to 2 s for the last lease's Forward Close. Nothing is lost by not waiting: the pool records the close
synchronously, so a re-create the next instant still does not open a second session until that Forward
Close + UnRegisterSession have finished, and the close is bounded by the connection's own budgets (the
open budget for an open still in flight, 1 s for the Forward Close), so a connection is always released.
Offline soak: `NativeAbCipLeaseReleaseTests` (25 create/release cycles, no leaked connection); a soak
against `ab_server` has not been run.
- **A refused operation is not a comms fault.** Any operation a runtime refuses as Unsupported (in AbCip, e.g.
a native write whose type cannot be determined) reaches the client as `BadNotSupported`, keeps the tag
handle, and leaves driver health unchanged; only real transport failures evict and degrade.
+3
View File
@@ -150,6 +150,9 @@ Native routing: SLC500/MicroLogix/PLC-5 are direct; `logixpccc` uses Unconnected
- **ST (string) byte order is unverified** (no capture); string values may be byte-swapped.
- Unlike AbCip, the native AbLegacy runtime has **no element-type check**, so a mis-authored same-width
type reads as reinterpreted data (as on libplctag), not `BadTypeMismatch`.
- Releasing a native tag (evict, reinitialize, shutdown) never blocks on the connection close: the lease
is released in the background (`ConnectionLease.ReleaseInBackground`), the same shape as native AbCip
(#579). A re-create cannot overlap the close, because the pool holds the successor connection behind it.
### Wire evidence (native PCCC)