m-1: a poll that captured a reader a rebind then disposed retries once on the current
reader; with no newer reader the typed SqlPollReaderDisposedException is marked
classified, so HandlePollError no longer degrades for it. Groups report a disposed
reader as a sentinel so WhenAll surfaces the typed exception, not a bare cancel.
m-2: TryAddRef refuses new groups once Dispose ran, matching the doc comment.
m-4: a rebind / tag-table failure in InitializeAsync now writes Faulted with a
sanitized reason (generation-checked) and host Stopped instead of latching
Initializing with no LastError.
m-6: SecurityClass-as-descriptor is documented at the Sql site (Commons field is a
follow-up). m-7: a position-less JsonException renders '?' instead of blanks.
Refs #744
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
SqlPollReader's SemaphoreSlim was never disposed and a new reader is built per
rebind. The reader is now IDisposable with a reference count (owner + one per
in-flight group): the gate is disposed only when the last group - including a
timed-out one still wedged in the provider - releases it, so nothing ever releases
into a disposed semaphore. ApplyBinding disposes the replaced reader; the final
DisposeAsync disposes the live one (Shutdown does not, since a re-init without a
rebind keeps polling through it).
Refs #744
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
The first #516 pass deliberately left these two out, because each derives more
than options from config: Sql its ISqlDialect and resolved connection string,
FOCAS the IFocasClientFactory its Backend key selects — all injected at
construction. Adopting new options alone would poll a NEW tag set through the
OLD database/backend, and a half-applied config change is worse than a
discarded one because it looks like it worked.
Rather than accept that exclusion, this fixes the blocker. Each factory now
exposes a ParseBinding returning EVERY config-derived dependency as one value,
and the driver adopts them atomically:
- Sql: ApplyBinding takes (options, dialect, connectionString) and rebuilds
everything downstream — provider factory, Endpoint, and the SqlPollReader
that captures all three. It runs BEFORE BuildTagTable, so the tag table and
the connection it is polled over always come from the same revision. A
test-injected DbProviderFactory survives a rebind (_explicitFactory), so a
re-derived dialect cannot displace what a test passed in.
- FOCAS: options and backend move as a pair in InitializeAsync.
Re-resolving Sql's connection string on reinit is a side benefit: a rotated
credential is picked up without a process restart.
Drivers constructed directly get no rebinder and keep their constructor
binding, so every existing "{}"-passing lifecycle test is unaffected by
construction rather than by luck.
The tests pin ATOMICITY, not merely that a re-parse happened — a test checking
only options would have passed against the broken version. Confirmed by
simulating the half-fix (adopt options, skip the backend): 2 of 3 FOCAS tests
go red. Reverting Sql's rebind turns its connection-string test red. Sql also
asserts that a reinit which cannot resolve its new connection leaves the
previous binding whole rather than half-adopting.
All 12 drivers now honour a changed config in place. Sql.Tests 226, FOCAS.Tests
275 pass.