feat(abcip): final review I1 — node-level Drivers:AbCip:DefaultTransport

The old AdminUI cannot author the per-driver transport key (#576), so the
'pin before upgrading' recipe had no working path. A node-level appsetting
(env Drivers__AbCip__DefaultTransport) now supplies the default for AbCip
drivers with no key: explicit key > node default > compiled Native. Bound
and validated unconditionally (bad value fails host start); the driver
factory and the Test Connect probe both read it so they agree. AbCip.md
upgrade recipe rewritten around the key; Configuration.md documents it.
Also documents first-wins for AllowPacking/probe and host-alias throttles
(final review M6) and moves the status-bit note out of AbCip.md (M5).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-07 21:34:48 -04:00
parent b66972b6f5
commit b75e013d0a
13 changed files with 447 additions and 44 deletions
+38 -18
View File
@@ -89,23 +89,28 @@ The driver has two interchangeable wire transports, selected by one `DriverConfi
| Key | Values | Default | Serialization |
|---|---|---|---|
| `transport` | absent \| `Native` \| `Libplctag` | absent ⇒ `Native` | string, case-insensitive on read, written as `"Libplctag"` / `"Native"`; "use the default" is encoded by OMITTING the key |
| `transport` | absent \| `Native` \| `Libplctag` | absent ⇒ the node's `Drivers:AbCip:DefaultTransport` (itself default `Native`) | string, case-insensitive on read, written as `"Libplctag"` / `"Native"`; "use the default" is encoded by OMITTING the key |
- **`Native`** (the default since Task 22) is a managed in-process EtherNet/IP + CIP client
(`Driver.Ab.Transport`).
- **`Libplctag`** (fallback) drives the libplctag native library, as before.
- An absent key means "use the default". It is resolved in one place, `AbCipTransportResolver`, which the
driver, the Test Connect probe and the CLI all go through. An explicit value always wins.
driver, the Test Connect probe and the CLI all go through. **Precedence:** an explicit per-driver key →
the node-level appsetting **`Drivers:AbCip:DefaultTransport`** (`Native` | `Libplctag`, env
`Drivers__AbCip__DefaultTransport`; a bad value fails host start; see
[Configuration.md](../Configuration.md#driversabcip-node-level-abcip-defaults)) → compiled `Native`. The
Test Connect probe uses the same node value, so it probes the stack the driver would run. The CLI has no
node config and uses compiled `Native`.
- A bad value fails like a bad `PlcFamily`; the Test Connect probe reports "Config JSON is invalid".
- A change takes effect when the driver instance is **constructed**: on every process start (so the upgrade
restart moves no-key drivers to Native — see the release note) and when a deploy that changes `DriverConfig`
does this (`DriverHostActor` respawns the child, #516). `ReinitializeAsync` alone keeps the transport
it was constructed with.
- AdminUI: the **Transport** dropdown in the advanced section of the AbCip driver form ("Default (Native)" /
"Native (managed)" / "libplctag (fallback)"). **Default omits the key on save**, so a defaulted driver stays
- AdminUI: the **Transport** dropdown in the advanced section of the AbCip driver form ("Default (node
setting; Native unless pinned)" / "Native (managed)" / "libplctag (fallback)"). **Default omits the key on save**, so a defaulted driver stays
defaulted; saving another field preserves an explicit value and preserves absence.
- The driver logs `transport=<effective> (default|configured)` at Information on every init, e.g.
`transport=Native (default)`, including reconnect re-inits. AbCip
`transport=Native (default)` or `transport=Libplctag (node default)`, including reconnect re-inits. AbCip
drivers now receive the host logger (they previously ran on a null logger, so none of their warnings
reached the node log); expect previously invisible warnings, e.g. a probe enabled with no probe tag path.
- CLI: `--transport default|native|libplctag` (default `default`, which goes through the same resolver).
@@ -119,19 +124,32 @@ The driver has two interchangeable wire transports, selected by one `DriverConfi
- ⚠️ **Client-visible:** native reports **`BadTypeMismatch`** for a mis-authored same-width type (for example a
tag authored `DINT` where the controller holds a `REAL`), where libplctag returned Good garbage. Correct the
authored type; see [Native transport behaviour](#native-transport-behaviour).
- **To fall back**, set `"transport": "Libplctag"` in the driver config (or pick "libplctag (fallback)" in
the AdminUI dropdown).
- **To fall back** one driver, set `"transport": "Libplctag"` in its driver config (or pick "libplctag
(fallback)" in the AdminUI dropdown). To fall back a whole node, set `Drivers:AbCip:DefaultTransport` to
`Libplctag` (below).
- ⚠️ **When it takes effect: the upgrade restart itself.** Every process start constructs the drivers
(including boot-from-cache on a driver-only node), so **every driver with no `transport` key moves to Native
the moment the upgraded node starts** — no deploy is needed. To keep a driver on libplctag, add
`"transport": "Libplctag"` and deploy it **before upgrading**: older builds ignore the unknown key. After the
upgrade, a transport change takes effect when a deploy recreates the driver (not on `ReinitializeAsync`).
(including boot-from-cache on a driver-only node), so **every driver with no `transport` key moves to the
node default — `Native` unless pinned — the moment the upgraded node starts**; no deploy is needed. After the
upgrade, a per-driver transport change takes effect when a deploy recreates the driver (not on
`ReinitializeAsync`).
- **To stay on libplctag across the upgrade (pin before upgrading):** set the node-level key on each node
you upgrade, in the same change that installs the new build — `appsettings.json`
`"Drivers": { "AbCip": { "DefaultTransport": "Libplctag" } }`, or the environment variable
`Drivers__AbCip__DefaultTransport=Libplctag` on the service. Older builds ignore the section, so it can be
staged ahead of the binaries. Every AbCip driver with no `transport` key then starts on libplctag; the init
log shows `transport=Libplctag (node default)`. Move to Native later per driver (explicit
`"transport": "Native"` in the new AdminUI) or for the node (remove the key / set `Native`, then restart).
⚠️ Do **not** rely on adding `"transport": "Libplctag"` to driver configs through the **old** AdminUI before
upgrading: its typed AbCip form has no Transport field and drops unknown keys on save (#576), and on a fused
`admin,driver` node the AdminUI and drivers upgrade together, so there is no window. The node key works on
every node shape, including fused nodes and driver-only nodes booting from cache.
- **Evidence, and its limits:** AbCip **reads** (plus browse and templates) were proven on a production
CompactLogix 5069-L320ERS2 (**Z-35162**, read-only, 2026-10-08; see
[gate results](../plans/2026-10-07-native-ab-transport-gate-results.md) §Real press). AbCip **writes** are
simulator-proven only (`ab_server`), as is all of AbLegacy.
- AbLegacy has its own exclusion: a defaulted AbLegacy driver with any PLC-5 or `logixpccc` device stays on
libplctag (see [AbLegacy.md §Transport](AbLegacy.md#transport-transport-option)).
simulator-proven only (`ab_server`).
- **AbLegacy is different: its default stays libplctag** and Native is opt-in per driver; the native PCCC
path has wire evidence (on `ab_server`) only for N-file reads/writes and one F8 read (see
[AbLegacy.md §Transport](AbLegacy.md#transport-transport-option)).
## Native transport behaviour
@@ -140,8 +158,12 @@ The driver has two interchangeable wire transports, selected by one `DriverConfi
request in flight; large reads (listing pages, Read Template) are sent unpacked and exclusive.
- **Forward Open throttle:** at most one attempt in flight, at least 1 s between starts, bounded by the
open budget.
- `ConnectionSize` / `Timeout` are honoured per pooled connection, and the **first** driver/device to open
a given device key wins. ControlLogix, GuardLogix and (since T21) CompactLogix default to 4002, a Large
- `ConnectionSize` / `Timeout` / `AllowPacking` are honoured per pooled connection, and the **first**
driver/device to open a given device key wins — including a Test Connect probe on a fused `admin,driver`
node (its connection carries the probe timeout). A later opener with different options is ignored and
logged once as a Warning (`DeviceSessionPool`). ⚠️ The device key compares the **host string**, so the
same controller addressed by DNS name in one driver and by IP in another gets two connections and two
Forward Open throttles (up to two opens per second); address a controller the same way everywhere. ControlLogix, GuardLogix and (since T21) CompactLogix default to 4002, a Large
Forward Open (0x5B), the same as libplctag. A target that refuses Large gets one regular open at
min(size, 504) inside the same throttled attempt. An explicit `ConnectionSize` in the options or on the
device overrides the family default.
@@ -157,8 +179,6 @@ The driver has two interchangeable wire transports, selected by one `DriverConfi
writes are gated on a mismatch too.
- Malformed tag names put the device into backoff, as under libplctag (#570). STRING capacity includes the
alignment pad (84, not 82) (#570).
- Status-bit sub-element writes (e.g. `T4:0.DN`) are Unsupported natively (they would clobber the control
word); nothing goes on the wire. Sub-element arrays are Unsupported natively.
- **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.