From 0455f9e0eb4561cf84d58e1cba30ef84ca5b4c26 Mon Sep 17 00:00:00 2001 From: "Harol A. Reina H." Date: Thu, 24 Sep 2026 13:24:18 -0500 Subject: [PATCH] docs(guides): the migration guide #291 owed, and the index row #302 never got MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0028 obliges a minor that carries a breaking change to ship a migration guide. `[Unreleased]` holds six BREAKING entries and #291's two had none. The guide covers both halves of that change, because both move something a consumer can observe and neither changes a signature: - `AriClient.State` after a failed first connect. It stayed `Connecting` for the life of the instance; it now leaves a terminal state chosen by who ended the attempt — `Faulted` for a refused upgrade or nothing listening, `Disconnected` when the caller cancelled. The guide names the workarounds this makes deletable (a poll waiting for the state to settle), states that the exception is unchanged, and records that 2.5.3's entry told consumers to watch `State` and named a value that is no longer what they will see. `AriHealthCheck`'s status is unchanged and only its message text moves, which matters to anyone asserting on the string. - `AriOutboundListener` surviving an accept failure. The old loop ended silently and for good while `IsRunning` still reported true, so a watchdog that restarted the listener could not work — `StartAsync` refused while the flag was set. That watchdog can go, and a persistent failure is now noisy where it used to be invisible. Also here, found while writing the index row: `audiosocket-wire-format-migration.md` has existed since #302 and was never listed in `docs/guides/README.md`. Its row is added, so all three migration guides are now reachable from the index. The guide restates the accept backoff bounds and their per-minute consequence, which are quantitative figures in a document inside the claim registry's scope, so it carries its registry row in this commit (ADR-0042 D1). --- CHANGELOG.md | 6 ++ docs/claim-registry.md | 1 + docs/guides/README.md | 2 + ...nection-state-and-accept-loop-migration.md | 102 ++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 docs/guides/ari-connection-state-and-accept-loop-migration.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 18307993..03a2180d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -287,6 +287,9 @@ never from the exception (`Sdk/ADR-0056`). - No counter, gauge, activity or event changes, and no public API change — no new type, no new member, no changed signature, and `AriConnectionState` itself is untouched. +**Migration guide:** [`docs/guides/ari-connection-state-and-accept-loop-migration.md`](docs/guides/ari-connection-state-and-accept-loop-migration.md) +— required by ADR-0028 for a minor that carries a breaking change. + ### Changed — BREAKING: `AriOutboundListener` keeps accepting after an accept fails (#291) `AcceptLoopAsync` wrapped its whole `while` in a `try` whose last clause was `catch (SocketException) { }`, @@ -315,6 +318,9 @@ cap with each consecutive failure and starts over after a successful accept — - No public API change here either: the backoff bounds, the accept seam and the `TimeProvider` constructor the wait runs on are all `internal`. +**Migration guide:** [`docs/guides/ari-connection-state-and-accept-loop-migration.md`](docs/guides/ari-connection-state-and-accept-loop-migration.md) +— required by ADR-0028 for a minor that carries a breaking change. + ## [2.5.3] - 2026-09-13 ### Fixed — BREAKING: `VoiceAiPipeline` counted a synthesizer's own cancellation as a completed synthesis diff --git a/docs/claim-registry.md b/docs/claim-registry.md index 697cd883..25ad382d 100644 --- a/docs/claim-registry.md +++ b/docs/claim-registry.md @@ -182,6 +182,7 @@ Missed by the first sweep — tracked, public, and read as current by every cont | `high-load-tuning.md:13-18,20,251` | events/sec per agent tier; 200K/sec queue storm; VarSet 50%+ of volume | — | GAP — workload estimates about the reader's PBX; see *Unresolved* | | `high-load-tuning.md:138-140` | pauseWriter 1 MB / resumeWriter 512 KB / segment 4 KB "hardcoded" | ENFORCING | GAP | | `high-load-tuning.md:197` | EventPumpCapacity 20,000 | ENFORCING | OK — matches source; `README-technical.md:503` is the wrong one | +| `ari-connection-state-and-accept-loop-migration.md` | accept backoff 100 ms doubling to a 5 s cap; at most 12 Error lines a minute at the cap | ENFORCING | GAP — the bounds are `internal` constants in `AriOutboundListener`; the per-minute figure is 60/5 s, arithmetic over them. Both are restated from the `[Unreleased]` #291 entry, not newly derived | | `session-store-backends.md:5,56,70,76` | three backends, three overloads, three indexes, pageSize 500 | ENFORCING | GAP | | `session-store-backends.md:9-11,22,27,35` at `e250182e` | read latency <0.1 ms / <1 ms / 5-10 ms; the "sub-millisecond" InMemory and Redis bullets; "5-10 ms read latency is acceptable" | COHERENCE | **DELETED** — nothing measures InMemory, the record binds no read figure, and the only read the committed measurements time, `GetAsync` over loopback, put Postgres at p50 48 µs, not 5-10 ms; Postgres `GetAsync` measures under a millisecond too, so "sub-millisecond reads" did not tell Redis apart. This row cited `:26` until then; the bullets are `:22` and `:27`. The words that replaced these figures are the next two rows | | `session-store-backends.md:9-11,22,35` | what a store call costs, in words: InMemory in-process, with no I/O and no serialization; Redis one `GET` for `GetAsync` and two for `GetByLinkedIdAsync`; Postgres one `SELECT` per read, one WAL flush per single save under the default `synchronous_commit=on`, and one shared by a `SaveBatchAsync` | ENFORCING | GAP — this repository's own code: true against `InMemorySessionStore.cs`, `RedisSessionStore.cs` and `PostgresSessionStore.cs` as of 2026-09-12 (`GetByLinkedIdAsync` sends one `GET` when the linked index misses), and nothing counts the commands | diff --git a/docs/guides/README.md b/docs/guides/README.md index 251a0dba..8f56b1d3 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -4,8 +4,10 @@ Practical how-to guides for working with Verbara Sdk. | Guide | Description | |-------|-------------| +| [ari-connection-state-and-accept-loop-migration.md](ari-connection-state-and-accept-loop-migration.md) | Moving to the ARI connect attempt that leaves a terminal state and the outbound listener that survives an accept failure -- what `AriClient.State` reports now, which workarounds can be deleted, and the Error line a persistent accept failure produces. | | [asterisk-version-compatibility.md](asterisk-version-compatibility.md) | AMI event coverage matrix across Asterisk 18-23, listing typed classes and fallback behavior per version. | | [asterisk-version-matrix.md](asterisk-version-matrix.md) | Supported Asterisk versions (22 LTS primary, 23 Standard secondary), Docker test infrastructure, and known-divergent behavior. | +| [audiosocket-wire-format-migration.md](audiosocket-wire-format-migration.md) | Moving to the corrected AudioSocket frame header -- the three-byte shape Asterisk actually sends, the enum values that changed, and why a rebuild is what picks them up. | | [externalmedia-channel-id-migration.md](externalmedia-channel-id-migration.md) | Moving to the `CreateExternalMediaAsync` signature that takes a `channelId` -- why the break was bought, which callers have to be edited rather than rebuilt, and how to make an AudioSocket stream findable by the channel id the create returned. | | [high-load-tuning.md](high-load-tuning.md) | Configuration guidance for high-load scenarios (1K-100K+ agents), including EventPump sizing and buffer capacity recommendations. | | [log-analysis-prompt.md](log-analysis-prompt.md) | Ready-to-use LLM prompt for analyzing Verbara.Sdk structured log files with extract, classify, and diagnose phases. | diff --git a/docs/guides/ari-connection-state-and-accept-loop-migration.md b/docs/guides/ari-connection-state-and-accept-loop-migration.md new file mode 100644 index 00000000..dad6c632 --- /dev/null +++ b/docs/guides/ari-connection-state-and-accept-loop-migration.md @@ -0,0 +1,102 @@ +# Migrating to the ARI terminal connect state and the surviving accept loop + +Required by ADR-0028: a minor that carries a breaking change ships a migration guide. + +Two behaviours changed in `Verbara.Sdk.Ari`, both recorded in `Sdk/ADR-0056`. Neither changes an API +signature, so nothing stops compiling. What changes is what the SDK reports, and in one case it moves +an observable that a previous release explicitly told you to watch. + +## What you have to do + +**Update the package.** There is nothing to edit for most consumers. + +Two situations need a look, and both are about code you may have written *because* of the old +behaviour: + +1. You read `AriClient.State` after a connect attempt that failed, or you built a timeout around it + because the state never settled. See below — it settles now. +2. You restart `AriOutboundListener` when it stops accepting, or you watch for that condition. The + listener no longer stops. See below. + +## `AriClient.State` after a failed first connect + +**Before:** a first `ConnectAsync` that threw left `State` at `Connecting` for the life of the +instance. The state machine wrote `Connecting`, dialled the events socket, and wrote `Connected` on +the next statement — a throw from the dial skipped that statement, and nothing else could ever write +again, because the events loop starts after the dial and the reconnect loop is only reached from it. + +**Now:** the attempt leaves a terminal state, and which one is decided by **who ended the attempt**, +read from your own cancellation token rather than from the exception: + +| How the attempt ended | `State` before | `State` now | +|---|---|---| +| Refused upgrade (`401`, `503`), nothing listening, name does not resolve | `Connecting` | `Faulted` | +| You cancelled the token you passed | `Connecting` | `Disconnected` | + +**The exception you catch is unchanged** — same type, same message, same stack. The state is written +in a `finally` that catches nothing and runs before the exception becomes observable to the awaiting +caller, so a `catch` block of yours already reads the terminal value. + +### What this means for code you may have written + +- **A poll or timeout waiting for `State` to leave `Connecting`** can be deleted. It never left + before; it leaves immediately now. +- **A check that treats `Connecting` as "an attempt is still in progress"** is now correct, where + before it was permanently wrong after a failed first connect. +- **This amends what 2.5.3 told you.** That release's entry for the credential-refusal fix said an + initial `ConnectAsync` answered `401` *"still throws `WebSocketException` to the caller and leaves + `State` at `Connecting`"*, and pointed you at `State` or the health check because observers receive + no `OnError` when the loop stops. The throw is still exactly that. The state it named is not. +- **`AriHealthCheck` reports the same status.** `Connecting`, `Faulted` and `Disconnected` all fall to + its `Unhealthy` arm, so a failed first connect was Unhealthy before and is Unhealthy after. Only the + message text moves, from `"ARI state: Connecting"` to `"ARI state: Faulted"` or + `"ARI state: Disconnected"`. **If you assert on that string, it changes.** +- **`IsConnected` is unchanged.** It was false under `Connecting` and is false under both terminal + values. + +## `AriOutboundListener` after an accept failure + +**Before:** the accept loop wrapped its whole `while` in a `try` whose last clause was +`catch (SocketException) { }`, outside the loop. One transient accept failure while the listener was +meant to be running — `EMFILE`, `ENOBUFS`, a connection aborted in the backlog — ended the loop +silently and for good. `IsRunning` still reported `true`, the socket was still in `LISTEN`, and the +kernel kept completing handshakes nobody would ever read. An outbound connector hung instead of being +refused, and `StartAsync` could not restart the listener because it was still marked running. + +**Now:** such a failure is logged at Error, waited out, and the loop keeps accepting. The wait doubles +from 100 ms to a 5 s cap with each consecutive failure and resets after a successful accept. + +### What this means for code you may have written + +- **A watchdog that restarts the listener** when it notices connections are no longer arriving can be + removed. It could not work anyway: `StartAsync` refused while `IsRunning` was true. +- **You will see Error lines you did not see before**, under a condition that used to be silent. At + most twelve a minute once the wait reaches its cap: + `[AriOutbound] Accept failed — the listener stays bound and accepts again after a backoff`. + A persistent failure is now noisy on purpose; it was invisible before. +- **A connection that arrives during a wait** stays in the listen backlog until the wait ends, rather + than being accepted and dropped. +- **The stop path is unchanged.** `StopAsync` still ends the loop at once; it clears the running flag + before stopping the listener, so an accept aborted by that stop is told apart from a failure by + `IsRunning` and never by the token. + +## How to check which behaviour you are on + +Force a connect that cannot succeed — point `AriClientOptions` at a port with nothing listening — and +read `State` in your `catch`: + +```csharp +try +{ + await client.ConnectAsync(cancellationToken); +} +catch (Exception) +{ + // 2.5.3 and earlier: Connecting + // this release: Faulted + Console.WriteLine(client.State); +} +``` + +For the listener, there is nothing to probe safely from a consumer — the condition needs a real +accept failure. The Error line above is the signal that the new behaviour is in play.