docs(v3): implementation plans — master orchestration + 4 batch plans
- v3 design finalized: all 7 open items resolved (live historian probe:
255-char tagname limit; SDK multi-notifier spike: native AddNotifier,
never duplicate ReportEvent; NamespaceKind.Simulated retired; CSV column
dictionaries; ScadaBridge re-bind sized; rename-warning scan decided),
reconciled with the pre-v3 universal discovery browser (its new §11
v3 forward note re-targets the browse commit contract).
- Calculation pseudo-driver mini-design (IDependencyConsumer capability,
mux-fed change/timer triggers, Tarjan cycle deploy gate, VT-parity
error semantics).
- Implementation plans for Opus-agent execution, grounded against exact
files/symbols: master plan (batch DAG, worktree-isolated parallel
waves, contract-first fan-out, global gotcha list) + one plan per
batch (1 schema+RawPath identity, 2 /raw UI+Calculation, 3 UNS
reference-only+{{equip}}, 4 dual-namespace address space = v3.0),
each with per-wave file ownership and a mandatory docker-dev live gate.
This commit is contained in:
@@ -127,12 +127,57 @@ narrow the driver config or use manual entry" rather than OOM on a 100 k-node ba
|
||||
capture on a short interval until the node set is non-empty and stable across two passes,
|
||||
bounded by the open-timeout — the same contract `DriverInstanceActor` honours at deploy.
|
||||
5. `await driver.ShutdownAsync(...)` in a `finally` — the tree is fully captured; hold nothing
|
||||
live. (No lazy expand ⇒ no need to keep the connection.)
|
||||
live. (No lazy expand ⇒ no need to keep the connection.) **The shutdown itself is bounded**
|
||||
(the R2-01 per-op-deadline rule, program doc §3.1 — no unbounded waits anywhere): it runs
|
||||
under its **own 10 s linked CTS**, because a driver wedged during discovery may be equally
|
||||
wedged in shutdown, and cleanup must not extend the open beyond open-timeout + that bound.
|
||||
On shutdown timeout: log a warning, **abandon the instance** (drop the reference — never
|
||||
block on it or rethrow), and still return / fail the open on the **discovery outcome** —
|
||||
a hung shutdown neither fails a successful capture nor masks the real discovery error.
|
||||
6. return `new CapturedTreeBrowseSession(capture.Root)`.
|
||||
|
||||
`CanBrowse(driverType, configJson)` = `TryCreate` (cheap, no connect) succeeds **and** the
|
||||
instance is `ITagDiscovery { SupportsOnlineDiscovery: true }`. Used by the AdminUI to decide
|
||||
whether to render the **Browse** button vs. manual entry, before any connect.
|
||||
#### Capture concurrency — coalescing + global cap
|
||||
|
||||
Every `OpenAsync` is heavy: it constructs, connects, and fully enumerates a **real driver
|
||||
against the live device**. Repeated Browse clicks (or two operators picking against the same
|
||||
controller) must not stack parallel symbol walks on one PLC. Two controls, both inside
|
||||
`DiscoveryDriverBrowser`:
|
||||
|
||||
- **In-flight coalescing**, keyed on `(driverType, hash of configJson)` (e.g. SHA-256 of the
|
||||
caller-supplied JSON; `PatchForBrowse` is a pure function of `driverType`, so hashing the
|
||||
pre-patch config is equivalent per key). A second open for the same key **awaits the same
|
||||
capture task** rather than starting another walk; each awaiting caller then receives its
|
||||
**own** `CapturedTreeBrowseSession` (own token, own TTL in the `BrowseSessionRegistry`)
|
||||
over the **shared, immutable captured tree**. A failed capture fails every coalesced
|
||||
awaiter with the same error. The coalescing map holds **in-flight captures only** — no
|
||||
result caching: the entry is removed when the capture completes (success or failure), so
|
||||
the next open for that key (including a Refresh, §4.3) triggers a fresh capture.
|
||||
- **A global cap on simultaneous captures** — `MaxConcurrentCaptures = 4` (a const, like
|
||||
`BrowserSessionService.PerCallTimeout`). Excess opens **queue** (semaphore, FIFO) and still
|
||||
honour the caller's cancellation token while queued. Coalesced awaiters don't consume a
|
||||
slot — only distinct in-flight captures count against the cap.
|
||||
|
||||
`CanBrowse(driverType, configJson)` = `TryCreate` (cheap, no connect) yields a non-null
|
||||
instance **and** that instance is `ITagDiscovery { SupportsOnlineDiscovery: true }`. Used by
|
||||
the AdminUI to decide whether to render the **Browse** button vs. manual entry, before any
|
||||
connect. Two hardening rules:
|
||||
|
||||
- **`TryCreate` can throw, not just return null.** Factories parse `configJson` *inside*
|
||||
`TryCreate`, so malformed/half-typed JSON (exactly what a picker holds mid-authoring) can
|
||||
surface as an exception. `CanBrowse` must catch **any** exception → `false` — it is a UI
|
||||
affordance gate and must never throw into the page.
|
||||
- **Defensive teardown of the throwaway instance.** Whatever the outcome (not discovery-capable,
|
||||
flag false, or an exception after construction), `CanBrowse` best-effort
|
||||
`ShutdownAsync`s/disposes any instance it *did* create — bounded, swallow-all — so probing
|
||||
never leaks driver instances.
|
||||
|
||||
**Invariant the throwaway-instance pattern depends on:** driver constructors are
|
||||
**connection-free** today — every driver in the tree connects in `InitializeAsync`, never in
|
||||
its ctor. `CanBrowse` (construct → inspect → discard, potentially on every picker render) and
|
||||
the capture path's construct-then-patch flow both rely on this. A future driver that opens a
|
||||
device connection in its constructor would silently turn `CanBrowse` into a connect storm —
|
||||
treat "no I/O in driver ctors" as a driver-authoring rule this browser now depends on (worth a
|
||||
line in the program doc's cross-cutting rules when P1 lands).
|
||||
|
||||
### 4.3 `CapturedTreeBrowseSession : IBrowseSession`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user