From 947999100b8447870fcee80c671296efba842ceb Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 18:35:54 +0100 Subject: [PATCH 01/21] =?UTF-8?q?spec:=20017=20phase=204=20task=204.1=20?= =?UTF-8?q?=E2=80=94=20phase=204=20predicted;=20a=20verdict=20per=20hard?= =?UTF-8?q?=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From master d8633b1, all nine gates at tools/README.md's figures. The probe reproduces the phase 4 table (40 FAILED, 18 reachable, 22 hard, 6 BUILT). blockcheck BUILT 189 -> 204..230; pagelint 658 -> 640..622; pages with nothing BUILT 74 -> 66..58. The pin grows by three (Npgsql EF Core, two TickerQ packages); SqlServer, Sqlite and Pomelo are already transitive. 22 hard blocks read: 15 parse, 7 other. Six parse blocks carry a defect beside the placeholder; their recurrences grepped (the unopened ConfigureServices lambda: 13 on 8 pages, 7 off-tranche). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- spec/017-compile_repairs/tasks.md | 126 +++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/spec/017-compile_repairs/tasks.md b/spec/017-compile_repairs/tasks.md index fa948ef..23a90a2 100644 --- a/spec/017-compile_repairs/tasks.md +++ b/spec/017-compile_repairs/tasks.md @@ -1245,7 +1245,7 @@ one. **Goal:** every 1-hard-block page in *Outbox and Inbox* leaves whole, its one hard block repaired, skipped with an accepted reason, or listed. Page list: § *The tranches*, phase 4 table. -- [ ] **Task 4.1:** Predict phase 4's movement +- [x] **Task 4.1:** Predict phase 4's movement - Input: § *The tranches*, phase 4 table; § *Phase 3 as executed* - Output: § *Phase 4 as executed* opens with a prediction per gate, **and a verdict per hard block before it is touched**: parse (placeholder / fragment / not code) or other (defect / wrapper @@ -1273,6 +1273,130 @@ skipped with an accepted reason, or listed. Page list: § *The tranches*, phase - Input: 4.1's prediction; the PR's diff - Output: as 2.6, for phase 4 +### Phase 4 as executed + +**Prediction, 2026-09-27, from `master` `d8633b1`**, the merge of phase 3 (PR #192). The *Now* +column is this task's own run at `d8633b1`, every gate bare, exit code read before its output; all +nine exit 0 and read at `tools/README.md`'s figures. Each prediction names its mechanism: + +| # | Gate | Now | Predicted after phase 4, and why | +|---:|---|---|---| +| 1 | `linkcheck` | 165 files, 0 broken | **none**. No file is added | +| 2 | `pagelint` | 0 errors, 658 warnings, 162 pages | **errors 0; warnings 658 → between 640 and 622.** On the 22 pages, rule 6 warns on **36** blocks, every one FAILED: the **18** reachable blocks and **18** of the 22 hard ones. The four hard blocks that do not warn already carry `using`s (`DapperOutbox.md` #2, `DynamoInbox.md` #1, `DynamoOutbox.md` #2, `ReplayOnSeenReference.md` #1). A reachable repair gives its block `using`s whether or not it then builds (−18, to 640); each hard block repaired rather than listed takes one more, to 622 if all 18 are. Recurrence repairs off the tranche lower it further, and are explained if so | +| 3, 4, 7 | shape, redirects, `--verify` | 161 / 77 / 161 | **none**. No `SUMMARY.md` change | +| 5 | `versioncheck` | 0 stale of **18, across 5 pages** | **none, scope held at 18 across 5.** Its five pages (`tools/versioncheck.py:80`) are the tutorials and `GetStarted.md`, none in phase 4 | +| 6 | `optioncheck` | 0 mismatches, 59 tables, 519 rows | **none.** Five phase 4 pages carry a table it reads — `AzureBlobDistributedLock.md`, `DynamoInbox.md`, `DynamoOutbox.md`, `InMemoryOutbox.md`, `PostgresDistributedLock.md`, 5 tables, 16 rows (`dotnet run --project tools/optioncheck -- `, per page). A row changes only if a repair finds a documented default wrong, and is then a defect in § *Defect ledger* | +| 8 | `symbolcheck` | 0 findings, 22 entries, 161 pages, 3 silenced | **none**. No repair here names a watchlisted symbol | +| 9 | `blockcheck` | 983: 189 BUILT, 778 FAILED, 16 SKIPPED; 538 reference assemblies; 30 units | **BUILT 189 → at least 204, at most 230; SKIPPED 16 plus accepted reasons only; reference assemblies up with the pin; exit 0; units 30 plus the new ones, 0 violations.** Mechanism below | +| — | `attr_mismatch.py` | **7**, exit 1 | **held at 7.** Its hits are on five pages, none in phase 4. `InMemoryInbox.md` #2's defect is an attribute on a class, not on a handler of the other kind, so the script does not read it | + +**Gate 9, by source.** The probe re-run at `d8633b1` (§ *The tranches*' recipe, `Order` excluded) +reads the phase 4 table unchanged: **40 FAILED, 18 reachable, 0 same-page, 22 hard, 6 BUILT**. +The 18 reachable are **9** with a `using` alone (`AzureBlobDistributedLock.md` #1, +`DynamoOutbox.md` #3, `FirestoreDistributedLock.md` #1, `MongoDbDistributedLock.md` #1, +`MsSqlDistributedLock.md` #1, `MySQLOutbox.md` #1, `MySqlDistributedLock.md` #1, +`PostgresDistributedLock.md` #1, `SqliteOutbox.md` #1), **6** with an empty stub (`services`: +`MSSQLOutbox.md` #2, `MySQLOutbox.md` #2, `PostgresOutbox.md` #2, `SqliteOutbox.md` #2, +`UsingSweeperCircuitBreaking.md` #2, #3), **2** needing a stub with members (`InMemoryOutbox.md` #2, +`UsingSweeperCircuitBreaking.md` #4) and **1** a typed value (`InMemoryInbox.md` #1, `services`). + +- **Floor 204** = 189 + 9 + 6: only the `using` and empty-stub blocks +- **Ceiling 230** = 189 + 18 + 21 + 2: every reachable block; every hard block but + `ReplayOnSeenReference.md` #1, which stays FAILED by P2-2; and `TickerQScheduler.md` #2, #3, off + the tranche, once the pin carries the two TickerQ packages. Each hard block that is listed rather + than repaired, and each defect a stub surfaces, lowers it by one +- **The pin grows by three, and is measured alone first**, as 1.8 was: + `Npgsql.EntityFrameworkCore.PostgreSQL` for `PostgresOutbox.md` #3, and `TickerQ.EntityFrameworkCore` + and `TickerQ.Dashboard` at 9.0.2 for `TickerQScheduler.md` #2, #3 (§ *Blocks that stay FAILED*, + where both built in scratch against the released packages). The pin carries **95** + `PackageReference`s (`grep -c`). Predicted alone: **+2 BUILT** (the TickerQ blocks), no other + verdict moves, because `UseNpgsql` sits behind `PostgresOutbox.md` #3's shape error. The other + three EF Core providers are already in the reference set, transitively + (`Microsoft.EntityFrameworkCore.SqlServer.dll`, `.Sqlite.dll`, `Pomelo.EntityFrameworkCore.MySql.dll` + in `refs.txt`), so `MSSQLOutbox.md`, `MySQLOutbox.md` and `SqliteOutbox.md` #3 need no ask +- **Pages with nothing BUILT: 74 → at most 66, as low as 58.** 17 phase 4 pages have no BUILT + block. **8** reach one by a `using` or an empty stub alone (`AzureBlobDistributedLock.md`, + `FirestoreDistributedLock.md`, `MongoDbDistributedLock.md`, `MsSqlDistributedLock.md`, + `MySQLOutbox.md`, `MySqlDistributedLock.md`, `PostgresDistributedLock.md`, `SqliteOutbox.md`); + **2** by a stub with members or a typed value (`InMemoryInbox.md`, `InMemoryOutbox.md`); **6** + only by repairing their one hard block (`AzureBlobArchiveProvider.md`, `DynamoInbox.md`, + `MSSQLInbox.md`, `MySQLInbox.md`, `PostgresInbox.md`, `SqliteInbox.md`); and + `ReplayOnSeenReference.md` by none. 74 − 16 = **58** +- **The ≤ 60 target, read ahead.** Phases 4 and 5 must land 14 of tranche 2's 19 (§ *Phase 3 as + executed*). Phase 4 holds **10** of the 19 — its pages with nothing BUILT and ≥ 1 reachable block, + the first ten above — and phase 5 the other 9. So phase 4 must land **at least 5** of the 10 if + phase 5 lands all nine. The six hard-only pages are not among the 19 but count toward ≤ 60 all the + same: every one landed is one phase 5 need not + +**A verdict per hard block, before it is touched.** From `--classify` (on the page's own text) and +`--explain` (on the probe's `stageP`, `using`s supplied). *Parse* is placeholder, fragment or not +code; *other* is defect or wrapper artefact. **Seven parse blocks carry a defect in the text +beside the placeholder**; a placeholder repair that does not repair it leaves the block FAILED. + +| Page | # | `--classify` / probe | Verdict | What the diagnostics and the text show | +|---|---:|---|---|---| +| `AzureBlobArchiveProvider.md` | 1 | parse / PARSE | **parse — placeholder, and defects** | `{ ... }` and a trailing `...` (`CS8635`); `.ConfigureServices(hostContext, services) =>` opens no lambda (`CS1519`, `CS1001`). Behind them, read at 10.7.0: `New AzCliCredential();` inside an initialiser; `AzureBlobArchiveProviderOptions` has no parameterless constructor (a primary constructor taking `blobContainerUri`, `tokenCredential`, `accessTier`, `tagBlobs`) and its properties are `init`; `UseOutboxArchiver` cannot infer `TTransaction`; `BatchSize` is `ArchiveBatchSize`; `MinimumAge` is a `TimeSpan`, not `744`; the option assignments name no `options.` | +| `AzureBlobDistributedLock.md` | 2 | parse / PARSE | **parse — placeholder** | `opt.Outbox = /* your external Outbox */;` (`CS1525`), alone | +| `DapperOutbox.md` | 2 | other / DEFECT | **other — wrapper artefact** | A `public override` `HandleAsync` outside its handler class: the `members` wrapper derives from `object` (`CS0117` on `base.HandleAsync`), and `_transactionProvider`, `_postBox`, `_logger` are the unshown class's fields | +| `DynamoInbox.md` | 1 | parse / PARSE | **parse — placeholder, and defects** | `...` twice and the same unopened `ConfigureServices` lambda; `{ ServiceURL = "…"; }`, a `;` inside an object initialiser; `credentials` is never shown | +| `DynamoOutbox.md` | 2 | other / DEFECT | **other — wrapper artefact** | As `DapperOutbox.md` #2: an `override` outside its class, `CS0117`, the same three fields | +| `FirestoreDistributedLock.md` | 2 | parse / PARSE | **parse — placeholder** | `/* your external Outbox */;`, alone | +| `InMemoryInbox.md` | 2 | import / DEFECT | **other — defect** | `[UseInboxAsync(...)]` on the class: `CS0592`, *"only valid on 'method' declarations"* (`RequestHandlerAttribute`'s usage at 10.7.0). The same attribute is repeated, correctly, on `HandleAsync`. `grep -rn -A1 '^\s*\[UseInbox' contents/ \| grep class` → **1**, this page | +| `InMemoryOutbox.md` | 1 | parse / PARSE | **parse — placeholder** | `/* your producer registry */;`, alone | +| `MSSQLInbox.md` | 1 | parse / PARSE | **parse — placeholder, and a defect** | `...` twice and the unopened `ConfigureServices` lambda | +| `MSSQLOutbox.md` | 3 | import / DEFECT | **other — wrapper artefact** | `CS0116`: a free `public void ConfigureServices` beside a class; no wrapper hosts both. `UseSqlServer` resolves | +| `MongoDbDistributedLock.md` | 2 | parse / PARSE | **parse — placeholder** | `/* your MongoDB Outbox */;`, alone | +| `MsSqlDistributedLock.md` | 2 | parse / PARSE | **parse — placeholder** | `/* your MS SQL Outbox */;`, alone | +| `MySQLInbox.md` | 1 | parse / PARSE | **parse — placeholder, and defects** | As `MSSQLInbox.md` #1, and `opt.InboxConfiguration` inside a lambda whose parameter is `options` | +| `MySQLOutbox.md` | 3 | parse / PARSE | **parse — placeholder** | `....` (`CS8635`, then `CS0029` reading it as a range), inside the same free-method shape as `MSSQLOutbox.md` #3 | +| `MySqlDistributedLock.md` | 2 | parse / PARSE | **parse — placeholder** | `/* your MySQL Outbox */;`, alone | +| `PostgresDistributedLock.md` | 2 | parse / PARSE | **parse — placeholder** | `/* your Postgres Outbox */;`, alone | +| `PostgresInbox.md` | 1 | parse / PARSE | **parse — placeholder, and defects** | As `MySQLInbox.md` #1 | +| `PostgresOutbox.md` | 3 | import / DEFECT | **other — wrapper artefact, and the pin** | `CS0116` as `MSSQLOutbox.md` #3, and `CS1061` `UseNpgsql`: `Npgsql.EntityFrameworkCore.PostgreSQL` is not in the pin | +| `ReplayOnSeenReference.md` | 1 | other / DEFECT | **other — API not in 10.7.0** | `CS0117`: `RequestContextBagNames` at 10.7.0 has `CloudEventsAdditionalProperties`, `JobId`, `PartitionKey`, `Headers`, `WorkflowId` — no `CausationId`. Stays FAILED by P2-2 (task 4.4) | +| `SqliteInbox.md` | 1 | parse / PARSE | **parse — placeholder, and defects** | As `MySQLInbox.md` #1 | +| `SqliteOutbox.md` | 3 | import / DEFECT | **other — wrapper artefact** | `CS0116` as `MSSQLOutbox.md` #3. `UseSqlite` resolves | +| `UsingSweeperCircuitBreaking.md` | 5 | import / DEFECT | **parse — fragment** | `CS0535` for `CoolDown()` and `TrippedTopics`, under the block's own `// Implement other methods using distributed cache`. `TripTopic(RoutingKey)` matches the 10.7.0 interface. The probe calls it DEFECT; read, it is a partial implementation that says so | + +**By this reading, 15 parse and 7 other.** `--classify` reads 14 *parse*, 3 *other* and 5 *import*; +the probe, 14 PARSE and 8 DEFECT. The one block whose kind differs is `UsingSweeperCircuitBreaking.md` +#5, a fragment the probe calls a DEFECT. The 7 *other*: + +- **Four wrapper artefacts, two shapes.** An `override` outside its class (`DapperOutbox.md`, + `DynamoOutbox.md` #2) and a free method beside a class (`MSSQLOutbox.md`, `SqliteOutbox.md` #3). + `PostgresOutbox.md` #3 is the second shape **and** waits on the pin, and `MySQLOutbox.md` #3, a + *parse* block for its `....`, is the second shape behind it +- **One defect**, `InMemoryInbox.md` #2, and **one API not in 10.7.0**, `ReplayOnSeenReference.md` #1 + +**Six parse blocks carry a defect beside the placeholder**, so a repair that removes only the +placeholder leaves them FAILED: `AzureBlobArchiveProvider.md` #1 and the five inbox pages' +`#1`s. Their recurrences, run now so that 4.2 opens with them: + +| Defect | Grep | Hits | Pages | +|---|---|---:|---| +| `.ConfigureServices(hostContext, services) =>` opens no lambda | `grep -rn 'ConfigureServices(hostContext, services) =>' contents/` | **13** | **8**: the six tranche pages once each, `BrighterBasicConfiguration.md` ×2, `DispatcherConfigurationReference.md` ×5. Both off-tranche pages have **nothing BUILT** (5 FAILED blocks each), so a recurrence repair there can move the ≤ 60 count too | +| `opt.` inside a lambda whose parameter is `options` | a Python scan of every `(p => { … })` for an assignment through another common builder name: **10** hits, read one by one | **3** | `MySQLInbox.md`, `PostgresInbox.md`, `SqliteInbox.md`. The other 7 are nested lambdas (`AddProducers(configure =>` inside `AddBrighter(options =>`, `AddOtlpExporter(o =>`, `UseMisfireHandler(options =>`), which is correct code | +| `[UseInbox…]` on a class | `grep -rn -A1 '^\s*\[UseInbox' contents/ \| grep class` | **1** | `InMemoryInbox.md` | +| `New AzCliCredential();` | `grep -rn 'New Az' contents/` | **1** | `AzureBlobArchiveProvider.md` | +| `UseOutboxArchiver` without its type argument | `grep -rn 'UseOutboxArchiver' contents/` → 7 lines on 2 pages | **1** | `AzureBlobArchiveProvider.md`; `OutboxArchiver.md`'s three calls all name `` | + +**Carried into the repair tasks, not the prediction:** + +- **`Order`**: no phase 4 block names it (`grep -cw Order` over each of the 40 FAILED blocks' + `--show` output → 0). The 1.10 ruling has nothing to act on here +- **Shared worlds.** The four EF Core outbox pages (`MSSQLOutbox.md`, `MySQLOutbox.md`, + `PostgresOutbox.md`, `SqliteOutbox.md`) repeat one block shape, and the five `*DistributedLock.md` + pages another; a unit or a repair on one is tried on its siblings before the next page is opened. + `InMemoryInbox.md` #1 wants `services` and `subscriptions`, which `InMemoryTransportContext.cs` + already supplies +- **Found off the tranche:** `QuartzScheduler.md:359` carries a U+200B zero-width space between + `UseMisfire` and `Handler` (`od -c`; `grep -rlP '\x{200B}' contents/` → that page only, 1 line). + **It compiles**: C# removes formatting characters before comparing identifiers. Run in a scratch + net10.0 console app, a call spelled with the U+200B (written by `printf '\u200b'`) resolved to + `UseMisfireHandler` and printed `called`; the control, `C.UseMisfireHandlr()`, is `CS0117`. So it is not a compile defect but a + text one — a search of the page for `UseMisfireHandler` misses the line. The page is in no + tranche; recorded for phase 5 to remove + --- ## Phase 5 — Tranche 2b and the recorded falsehoods *(6 tasks, one PR, CHANGES THE SITE)* From bd2b1ed5d1bd3ef120692d270125f0fb06f04302 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 19:29:08 +0100 Subject: [PATCH 02/21] blockcheck: grow the pin by the Npgsql EF provider and two TickerQ packages 98 PackageReferences, 542 reference assemblies. Measured alone: one block moves, TickerQScheduler.md #3, FAILED -> BUILT. #2 now fails only on Program, the statements wrapper's limitation (Q2), not on the pin. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- tools/blockcheck/refs/refs.csproj | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tools/blockcheck/refs/refs.csproj b/tools/blockcheck/refs/refs.csproj index 97aebeb..8b3033f 100644 --- a/tools/blockcheck/refs/refs.csproj +++ b/tools/blockcheck/refs/refs.csproj @@ -206,6 +206,16 @@ net9.0. Below it the restore fails NU1605, a package downgrade. --> + + + + + + + From fc65dce65b4e846b6b643cd2840072e9084e5b00 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 19:29:28 +0100 Subject: [PATCH 03/21] blockcheck: baseline TickerQScheduler.md #3, built by the grown pin Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- tools/blockcheck/baseline.tsv | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/blockcheck/baseline.tsv b/tools/blockcheck/baseline.tsv index 8b3fb95..9f1a40f 100644 --- a/tools/blockcheck/baseline.tsv +++ b/tools/blockcheck/baseline.tsv @@ -200,6 +200,7 @@ contents/TestingQueryHandlers.md 1 TestingQueryHandlersContext.cs 3462412 contents/TestingQueryHandlers.md 2 TestingQueryHandlersContext.cs 3462412 contents/TestingQueryHandlers.md 3 TestingQueryHandlersContext.cs 3462412 contents/TickerQScheduler.md 1 TickerQSchedulerContext.cs 3509d3e +contents/TickerQScheduler.md 3 TickerQSchedulerContext.cs bd2b1ed contents/TickerQScheduler.md 4 TickerQSchedulerContext.cs 3509d3e contents/TickerQScheduler.md 5 TickerQSchedulerContext.cs 280d1b7 contents/TickerQScheduler.md 6 TickerQSchedulerContext.cs 280d1b7 From a8ede7c97c2a89c0ee2ecee1c851be999aa74868 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 19:49:46 +0100 Subject: [PATCH 04/21] docs: outbox and inbox pages compile; the InMemory Inbox's window and #4335 stated as run The thirteen outbox and inbox pages of tranche 2a build whole: using directives, four units, the unopened ConfigureServices lambda repaired at all 13 recurrences on 8 pages, the free ConfigureServices put in a Startup class, the handler bodies given the class the samples declare. Defects, each verified at 10.7.0: [UseInboxAsync] on a class (CS0592); Post awaited with a cancellationToken it does not take; a ';' inside an object initialiser; the InMemory Inbox said to keep entries until restart, where they expire after EntryTimeToLive; the page's Warn configuration beside an attribute whose default Throw wins. A global InboxConfiguration is ignored without AddProducers (BrighterCommand/Brighter#4335, fixed on master, in no release): stated once on Inbox Support, linked from the nine inbox pages. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- contents/AzureBlobArchiveProvider.md | 13 ++- contents/BrighterBasicConfiguration.md | 25 +++-- contents/BrighterInboxSupport.md | 6 + contents/DapperOutbox.md | 104 ++++++++++-------- contents/DispatcherConfigurationReference.md | 20 ++-- contents/DynamoInbox.md | 14 ++- contents/DynamoOutbox.md | 91 +++++++++------ contents/FirestoreInbox.md | 2 + contents/InMemoryInbox.md | 26 ++++- contents/InMemoryOutbox.md | 17 ++- contents/MSSQLInbox.md | 20 ++-- contents/MSSQLOutbox.md | 73 +++++++----- contents/MongoDBInbox.md | 2 + contents/MySQLInbox.md | 20 +++- contents/MySQLOutbox.md | 71 +++++++----- contents/PostgresInbox.md | 21 ++-- contents/PostgresOutbox.md | 69 +++++++----- contents/SpannerInbox.md | 2 + contents/SqliteInbox.md | 22 ++-- contents/SqliteOutbox.md | 71 +++++++----- tools/blockcheck/scaffold/pages.tsv | 11 ++ .../scaffold/units/DynamoInboxContext.cs | 15 +++ .../scaffold/units/DynamoOutboxContext.cs | 25 ++++- .../scaffold/units/InMemoryBoxContext.cs | 52 +++++++++ .../blockcheck/scaffold/units/PageContext.cs | 33 +++++- .../scaffold/units/RelationalInboxContext.cs | 12 ++ .../scaffold/units/RelationalOutboxContext.cs | 16 +++ 27 files changed, 596 insertions(+), 257 deletions(-) create mode 100644 tools/blockcheck/scaffold/units/DynamoInboxContext.cs create mode 100644 tools/blockcheck/scaffold/units/InMemoryBoxContext.cs create mode 100644 tools/blockcheck/scaffold/units/RelationalInboxContext.cs create mode 100644 tools/blockcheck/scaffold/units/RelationalOutboxContext.cs diff --git a/contents/AzureBlobArchiveProvider.md b/contents/AzureBlobArchiveProvider.md index 0e73a3c..628a16a 100644 --- a/contents/AzureBlobArchiveProvider.md +++ b/contents/AzureBlobArchiveProvider.md @@ -16,13 +16,20 @@ For this we will need the *Archive* packages for the Azure *Archive Provider*. * **Paramore.Brighter.Archive.Azure** -``` csharp +```csharp +using Azure.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Paramore.Brighter.Storage.Azure; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Outbox.Hosting; + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices(hostContext, services) => + .ConfigureServices((hostContext, services) => { ConfigureBrighter(hostContext, services); - } + }); private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { diff --git a/contents/BrighterBasicConfiguration.md b/contents/BrighterBasicConfiguration.md index c4ae6cb..ca69f16 100644 --- a/contents/BrighterBasicConfiguration.md +++ b/contents/BrighterBasicConfiguration.md @@ -136,17 +136,21 @@ We provide the class `ServiceActivatorHostedService` for this in the NuGet packa The `ServiceActivatorHostedService` calls the **Dispatcher.Receive** method which starts message pumps for the configured *Subscriptions*. -``` csharp +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Paramore.Brighter.ServiceActivator.Extensions.Hosting; + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices(hostContext, services) => + .ConfigureServices((hostContext, services) => { ConfigureBrighter(hostContext, services); - } + }); private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { - ... + // ... services.AddHostedService(); } @@ -154,21 +158,26 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo On shutdown Brighter will allow the current *Request Handler* to complete, then end the message pump loop and exit. If you have long-running handlers it is possible that they will not complete in the default 5s for graceful shutdown of the MS Generic Host. In this case, you need to [increase the timeout](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-6.0#shutdowntimeout) of the host shutdown. -``` csharp +```csharp +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Paramore.Brighter.ServiceActivator.Extensions.Hosting; + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices(hostContext, services) => + .ConfigureServices((hostContext, services) => { services.Configure(options => { options.ShutdownTimeout = TimeSpan.FromSeconds(20); }); ConfigureBrighter(hostContext, services); - } + }); private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { - ... + // ... services.AddHostedService(); } diff --git a/contents/BrighterInboxSupport.md b/contents/BrighterInboxSupport.md index dd48a7c..08afce1 100644 --- a/contents/BrighterInboxSupport.md +++ b/contents/BrighterInboxSupport.md @@ -81,6 +81,12 @@ There are two versions of the attribute: sync and async. Ensure that you choose Your inbox is configured as part of the Brighter extensions to `IServiceCollection`. See [Inbox Configuration](/contents/DispatcherConfigurationReference.md#inbox) for more. +### Global Inbox Configuration in a Consumer-Only Application + +In Brighter 10.7.0, the `InboxConfiguration` you set in `AddConsumers` reaches your handlers' pipelines only when the application also calls `AddProducers`. In an application that only consumes, Brighter adds no Inbox to any handler, so a duplicate is handled again, with no error. This is [BrighterCommand/Brighter#4335](https://github.com/BrighterCommand/Brighter/issues/4335); the fix is on Brighter's development branch and in no release yet. + +Until a release carries it, put the attribute on each handler that must not see a duplicate, as in [Adding an Inbox to a Handler](#adding-an-inbox-to-a-handler). The attribute takes effect whether or not the application registers producers. Its own `onceOnlyAction` decides what a duplicate does, whatever `actionOnExists` the configuration sets, so give it the action you want. An application that calls `AddProducers` gets the global Inbox as configured. + ### Provisioning the Inbox Table If your Inbox runs on a relational database (MSSQL, PostgreSQL, MySQL, SQLite, or Spanner), Brighter can create and migrate the table for you at application startup — see [Database Provisioning](/contents/BoxProvisioning.md). The **Inbox Builder** section below describes the alternative: managing the DDL yourself. diff --git a/contents/DapperOutbox.md b/contents/DapperOutbox.md index 0306367..c976784 100644 --- a/contents/DapperOutbox.md +++ b/contents/DapperOutbox.md @@ -63,6 +63,8 @@ public void ConfigureServices(IServiceCollection services) ``` +The handler below is `AddGreetingHandlerAsync` from the Brighter sample at `Brighter/samples/WebAPI/WebAPI_Dapper/`, which also declares the `AddGreeting` request, the `Person` and `Greeting` entities and the `GreetingMade` event it uses. + In our handler we take a dependency on Brighter's **IAmATransactionConnectionProvider**. We explicitly start a transaction within the handler on the Database within the provider. Dapper provides extension methods on a DbConnection for typical CRUD operations. Our provider wraps that DbConnection, and allows you to create a DB transaction associated with that DbConnection. You must use our method, and not create the transaction directly via the connection, because we cannot obtain that transaction. Sharing that transaction allows us to insert a message into the Outbox within the same transaction. We call **DepositPostAsync** within that transaction to write the message to the Outbox. Once the transaction has closed we can call **ClearOutboxAsync** to immediately clear, or we can rely on the Outbox Sweeper, if we have configured one to clear for us. (There are equivalent synchronous versions of these APIs). @@ -78,56 +80,72 @@ using Dapper; using Microsoft.Extensions.Logging; using Paramore.Brighter; -public override async Task HandleAsync(AddGreeting addGreeting, CancellationToken cancellationToken = default) +public class AddGreetingHandlerAsync : RequestHandlerAsync { - var posts = new List(); + private readonly IAmATransactionConnectionProvider _transactionProvider; + private readonly IAmACommandProcessor _postBox; + private readonly ILogger _logger; - //We use the transaction provider to grab connection and transaction, because Outbox needs - //to share them 'behind the scenes' - DbConnection conn = await _transactionProvider.GetConnectionAsync(cancellationToken); - DbTransaction tx = await _transactionProvider.GetTransactionAsync(cancellationToken); - try - { - var people = await conn.QueryAsync( - "select * from Person where name = @name", - new { name = addGreeting.Name }, - tx); - var person = people.Single(); - - var greeting = new Greeting(addGreeting.Greeting, person); - - //write the added child entity to the Db - await conn.ExecuteAsync( - "insert into Greeting (Message, Recipient_Id) values (@Message, @RecipientId)", - new { greeting.Message, greeting.RecipientId }, - tx); - - //Now write the message we want to send to the Db in the same transaction. - posts.Add(await _postBox.DepositPostAsync( - new GreetingMade(greeting.Greet()), - _transactionProvider, - cancellationToken: cancellationToken)); - - //commit both new greeting and outgoing message - await _transactionProvider.CommitAsync(cancellationToken); - } - catch (Exception e) + public AddGreetingHandlerAsync(IAmATransactionConnectionProvider transactionProvider, + IAmACommandProcessor postBox, + ILogger logger) { - _logger.LogError(e, "Exception thrown handling Add Greeting request"); - //it went wrong, rollback the entity change and the downstream message - await _transactionProvider.RollbackAsync(cancellationToken); - return await base.HandleAsync(addGreeting, cancellationToken); + _transactionProvider = transactionProvider; + _postBox = postBox; + _logger = logger; } - finally + + public override async Task HandleAsync(AddGreeting addGreeting, CancellationToken cancellationToken = default) { - _transactionProvider.Close(); - } + var posts = new List(); - //Send this message via a transport. We need the ids to send just the messages here, not all outstanding ones. - //Alternatively, you can let the Sweeper do this, but at the cost of increased latency - await _postBox.ClearOutboxAsync(posts, cancellationToken: cancellationToken); + //We use the transaction provider to grab connection and transaction, because Outbox needs + //to share them 'behind the scenes' + DbConnection conn = await _transactionProvider.GetConnectionAsync(cancellationToken); + DbTransaction tx = await _transactionProvider.GetTransactionAsync(cancellationToken); + try + { + var people = await conn.QueryAsync( + "select * from Person where name = @name", + new { name = addGreeting.Name }, + tx); + var person = people.Single(); + + var greeting = new Greeting(addGreeting.Greeting, person); + + //write the added child entity to the Db + await conn.ExecuteAsync( + "insert into Greeting (Message, Recipient_Id) values (@Message, @RecipientId)", + new { greeting.Message, greeting.RecipientId }, + tx); + + //Now write the message we want to send to the Db in the same transaction. + posts.Add(await _postBox.DepositPostAsync( + new GreetingMade(greeting.Greet()), + _transactionProvider, + cancellationToken: cancellationToken)); + + //commit both new greeting and outgoing message + await _transactionProvider.CommitAsync(cancellationToken); + } + catch (Exception e) + { + _logger.LogError(e, "Exception thrown handling Add Greeting request"); + //it went wrong, rollback the entity change and the downstream message + await _transactionProvider.RollbackAsync(cancellationToken); + return await base.HandleAsync(addGreeting, cancellationToken); + } + finally + { + _transactionProvider.Close(); + } - return await base.HandleAsync(addGreeting, cancellationToken); + //Send this message via a transport. We need the ids to send just the messages here, not all outstanding ones. + //Alternatively, you can let the Sweeper do this, but at the cost of increased latency + await _postBox.ClearOutboxAsync(posts, cancellationToken: cancellationToken); + + return await base.HandleAsync(addGreeting, cancellationToken); + } } ``` diff --git a/contents/DispatcherConfigurationReference.md b/contents/DispatcherConfigurationReference.md index 382fa65..a992ae1 100644 --- a/contents/DispatcherConfigurationReference.md +++ b/contents/DispatcherConfigurationReference.md @@ -36,10 +36,10 @@ If you are using a **HostBuilder** class's **ConfigureServices** method call th // ... private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices(hostContext, services) => + .ConfigureServices((hostContext, services) => { services.AddConsumers(...) - } + }); ``` @@ -81,10 +81,10 @@ For RabbitMQ for example, this would look like this: // ... private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices(hostContext, services) => + .ConfigureServices((hostContext, services) => { ConfigureBrighter(hostContext, services); - } + }); private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { @@ -122,10 +122,10 @@ For RabbitMQ, this would look like: // ... private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices(hostContext, services) => + .ConfigureServices((hostContext, services) => { ConfigureBrighter(hostContext, services); - } + }); private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { @@ -156,10 +156,10 @@ Under the hood your Dispatcher uses a *Command Processor* and you will need to c // ... private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices(hostContext, services) => + .ConfigureServices((hostContext, services) => { ConfigureBrighter(hostContext, services); - } + }); private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { @@ -271,10 +271,10 @@ A typical *Inbox* configuration for MySQL would be: // ... private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices(hostContext, services) => + .ConfigureServices((hostContext, services) => { ConfigureBrighter(hostContext, services); - } + }); private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { diff --git a/contents/DynamoInbox.md b/contents/DynamoInbox.md index 7384f91..f06827b 100644 --- a/contents/DynamoInbox.md +++ b/contents/DynamoInbox.md @@ -43,7 +43,7 @@ The type also declares `Credentials` and `Region`. **Neither does anything**: th only `TableName`, and in the AWS SDK v3 package the two are get-only and never assigned, so they are always null. The v4 package declares them settable and still reads neither. -``` csharp +```csharp using Amazon.DynamoDBv2; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -53,21 +53,23 @@ using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices(hostContext, services) => + .ConfigureServices((hostContext, services) => { ConfigureBrighter(hostContext, services); - } + }); private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { - var dynamoDb = new AmazonDynamoDBClient(credentials, new AmazonDynamoDBConfig { ServiceURL = "http://dynamodb.us-east-1.amazonaws.com"; }); + var dynamoDb = new AmazonDynamoDBClient(credentials, new AmazonDynamoDBConfig { ServiceURL = "http://dynamodb.us-east-1.amazonaws.com" }); services.AddConsumers(opt => { opt.InboxConfiguration = new InboxConfiguration(new DynamoDbInbox(dynamoDb, new DynamoDbInboxConfiguration())); - ... + // ... }); } -... +// ... ``` + +In Brighter 10.7.0 this configuration takes effect only in an application that also calls `AddProducers`; see [Global Inbox Configuration in a Consumer-Only Application](/contents/BrighterInboxSupport.md#global-inbox-configuration-in-a-consumer-only-application). diff --git a/contents/DynamoOutbox.md b/contents/DynamoOutbox.md index 85ec634..1a21b1e 100644 --- a/contents/DynamoOutbox.md +++ b/contents/DynamoOutbox.md @@ -63,6 +63,8 @@ public void ConfigureServices(IServiceCollection services) ``` +The handler below is `AddGreetingHandlerAsync` from the Brighter sample at `Brighter/samples/WebAPI/WebAPI_Dynamo/`, which also declares the `AddGreeting` request, the `Person` entity and the `GreetingMade` event it uses. + In our handler we take a dependency on Brighter's **IAmADynamoDbTransactionProvider** interface, which **DynamoDbUnitOfWork** implements. We explicitly start a transaction within the handler on the Database within that provider. We call **DepositPostAsync** within that transaction to write the message to the Outbox. Once the transaction has closed we can call **ClearOutboxAsync** to immediately clear, or we can rely on the Outbox Sweeper, if we have configured one to clear for us. (There are equivalent synchronous versions of these APIs). @@ -78,50 +80,67 @@ using Amazon.DynamoDBv2.DataModel; using Amazon.DynamoDBv2.Model; using Microsoft.Extensions.Logging; using Paramore.Brighter; +using Paramore.Brighter.DynamoDb; -public override async Task HandleAsync(AddGreeting addGreeting, CancellationToken cancellationToken = default) +public class AddGreetingHandlerAsync : RequestHandlerAsync { - var posts = new List(); + private readonly IAmADynamoDbTransactionProvider _transactionProvider; + private readonly IAmACommandProcessor _postBox; + private readonly ILogger _logger; - //We use the transaction provider to grab connection and transaction, because Outbox needs - //to share them 'behind the scenes' - var context = new DynamoDBContext(_transactionProvider.DynamoDb); - var transaction = await _transactionProvider.GetTransactionAsync(cancellationToken); - try + public AddGreetingHandlerAsync(IAmADynamoDbTransactionProvider transactionProvider, + IAmACommandProcessor postBox, + ILogger logger) { - var person = await context.LoadAsync(addGreeting.Name, cancellationToken); + _transactionProvider = transactionProvider; + _postBox = postBox; + _logger = logger; + } - person.Greetings.Add(addGreeting.Greeting); + public override async Task HandleAsync(AddGreeting addGreeting, CancellationToken cancellationToken = default) + { + var posts = new List(); - var document = context.ToDocument(person); - var attributeValues = document.ToAttributeMap(); + //We use the transaction provider to grab connection and transaction, because Outbox needs + //to share them 'behind the scenes' + var context = new DynamoDBContext(_transactionProvider.DynamoDb); + var transaction = await _transactionProvider.GetTransactionAsync(cancellationToken); + try + { + var person = await context.LoadAsync(addGreeting.Name, cancellationToken); - //write the added child entity to the Db - just replace the whole entity as we grabbed the original - //in production code, an update expression would be faster - transaction.TransactItems.Add(new TransactWriteItem{Put = new Put{TableName = "People", Item = attributeValues}}); + person.Greetings.Add(addGreeting.Greeting); - //Now write the message we want to send to the Db in the same transaction. - posts.Add(await _postBox.DepositPostAsync( - new GreetingMade(addGreeting.Greeting), - _transactionProvider, - cancellationToken: cancellationToken)); + var document = context.ToDocument(person); + var attributeValues = document.ToAttributeMap(); - //commit both new greeting and outgoing message - await _transactionProvider.CommitAsync(cancellationToken); - } - catch (Exception e) - { - _logger.LogError(e, "Exception thrown handling Add Greeting request"); - //it went wrong, rollback the entity change and the downstream message - _transactionProvider.Rollback(); - return await base.HandleAsync(addGreeting, cancellationToken); - } + //write the added child entity to the Db - just replace the whole entity as we grabbed the original + //in production code, an update expression would be faster + transaction.TransactItems.Add(new TransactWriteItem{Put = new Put{TableName = "People", Item = attributeValues}}); + + //Now write the message we want to send to the Db in the same transaction. + posts.Add(await _postBox.DepositPostAsync( + new GreetingMade(addGreeting.Greeting), + _transactionProvider, + cancellationToken: cancellationToken)); + + //commit both new greeting and outgoing message + await _transactionProvider.CommitAsync(cancellationToken); + } + catch (Exception e) + { + _logger.LogError(e, "Exception thrown handling Add Greeting request"); + //it went wrong, rollback the entity change and the downstream message + _transactionProvider.Rollback(); + return await base.HandleAsync(addGreeting, cancellationToken); + } - //Send this message via a transport. We need the ids to send just the messages here, not all outstanding ones. - //Alternatively, you can let the Sweeper do this, but at the cost of increased latency - await _postBox.ClearOutboxAsync(posts, cancellationToken: cancellationToken); + //Send this message via a transport. We need the ids to send just the messages here, not all outstanding ones. + //Alternatively, you can let the Sweeper do this, but at the cost of increased latency + await _postBox.ClearOutboxAsync(posts, cancellationToken: cancellationToken); - return await base.HandleAsync(addGreeting, cancellationToken); + return await base.HandleAsync(addGreeting, cancellationToken); + } } ``` @@ -169,6 +188,12 @@ This applies to both `Paramore.Brighter.Outbox.DynamoDB` and `.V4`. The DynamoDB `MessageItem.CausationId` is decorated with `[DynamoDBGlobalSecondaryIndexHashKey(indexName: "Causation")]`, and `DynamoDbTableFactory` reflects over those attributes when it builds the request. So a table you create through the factory already has the index — you only add a throughput entry for it: ```csharp +using System.Collections.Generic; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using Paramore.Brighter.DynamoDb; +using Paramore.Brighter.Outbox.DynamoDB; + var createTableRequest = new DynamoDbTableFactory().GenerateCreateTableRequest( new DynamoDbCreateProvisionedThroughput( new ProvisionedThroughput { ReadCapacityUnits = 10, WriteCapacityUnits = 10 }, diff --git a/contents/FirestoreInbox.md b/contents/FirestoreInbox.md index 64787f2..8932071 100644 --- a/contents/FirestoreInbox.md +++ b/contents/FirestoreInbox.md @@ -57,6 +57,8 @@ public static class FirestoreInboxRegistration } ``` +In Brighter 10.7.0 this configuration takes effect only in an application that also calls `AddProducers`; see [Global Inbox Configuration in a Consumer-Only Application](/contents/BrighterInboxSupport.md#global-inbox-configuration-in-a-consumer-only-application). + `FirestoreInbox` also has a constructor taking an `IAmAFirestoreConnectionProvider` alongside the configuration, so one client can serve the Inbox, the Outbox and the lock. diff --git a/contents/InMemoryInbox.md b/contents/InMemoryInbox.md index 0a2fd19..4d8ba00 100644 --- a/contents/InMemoryInbox.md +++ b/contents/InMemoryInbox.md @@ -30,7 +30,13 @@ The InMemory Inbox provides message deduplication without requiring a database. ## InMemory Inbox Configuration ```csharp -// ... +using System; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Inbox; +using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; + var bus = new InternalBus(); services.AddConsumers(options => @@ -46,16 +52,22 @@ services.AddConsumers(options => .AutoFromAssemblies(); ``` +In Brighter 10.7.0 this configuration takes effect only in an application that also calls `AddProducers`; see [Global Inbox Configuration in a Consumer-Only Application](/contents/BrighterInboxSupport.md#global-inbox-configuration-in-a-consumer-only-application). + ## InMemory Inbox Example Usage ```csharp -// ... -[UseInboxAsync(step: 0, contextKey: typeof(PersonCreatedHandler), onceOnly: true)] +using System.Threading; +using System.Threading.Tasks; +using Paramore.Brighter; +using Paramore.Brighter.Inbox; +using Paramore.Brighter.Inbox.Attributes; + public class PersonCreatedHandler : RequestHandlerAsync { private readonly PersonRepository _repository; - [UseInboxAsync(0, typeof(PersonCreatedHandler), true)] + [UseInboxAsync(0, typeof(PersonCreatedHandler), true, onceOnlyAction: OnceOnlyAction.Warn)] public override async Task HandleAsync( PersonCreated @event, CancellationToken cancellationToken = default) @@ -70,12 +82,14 @@ public class PersonCreatedHandler : RequestHandlerAsync } ``` +The attribute decides what happens to a duplicate for this handler, whatever the configuration says. `onceOnlyAction` defaults to `Throw`, so without it a duplicate raises `OnceOnlyException` even though the configuration above asks for `Warn`. The configuration's `actionOnExists` applies only to handlers that carry no `[UseInboxAsync]` attribute of their own. + ## InMemory Inbox Limitations - **No persistence**: Deduplication state lost on restart - **Single process**: Cannot deduplicate across instances -- **Memory bound**: All seen message IDs held in memory -- **No cleanup**: Old entries remain until process restart +- **Memory bound**: Seen message IDs are held in memory. When adding an entry finds `EntryLimit` entries or more (2048 by default), the oldest are removed down to `CompactionPercentage` of the limit (half, by default), at most once per `ExpirationScanInterval` +- **A short deduplication window**: An entry expires `EntryTimeToLive` after it is written (5 minutes by default), and a scan removes it at most once per `ExpirationScanInterval` (10 minutes by default), started when an entry is added or read. A duplicate that arrives after its entry has gone is handled again ## Further Reading diff --git a/contents/InMemoryOutbox.md b/contents/InMemoryOutbox.md index 52c2e37..a2aa684 100644 --- a/contents/InMemoryOutbox.md +++ b/contents/InMemoryOutbox.md @@ -64,14 +64,19 @@ The InMemoryOutbox's capacity is constrained. You can configure the limit to the ## InMemory Outbox Configuration ```csharp -// ... +using System; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Outbox.Hosting; + services.AddBrighter(options => { options.HandlerLifetime = ServiceLifetime.Scoped; }) .AddProducers(options => { - options.ProducerRegistry = /* your producer registry */; + options.ProducerRegistry = producerRegistry; // your producer registry options.Outbox = new InMemoryOutbox(TimeProvider.System); }) .UseOutboxSweeper(); // Enable sweeper for reliability @@ -80,7 +85,11 @@ services.AddBrighter(options => ## InMemory Outbox Example of Post ```csharp -// ... +using System.Threading; +using System.Threading.Tasks; +using System.Transactions; +using Paramore.Brighter; + public class CreatePersonHandler : RequestHandlerAsync { private readonly IAmACommandProcessor _commandProcessor; @@ -96,7 +105,7 @@ public class CreatePersonHandler : RequestHandlerAsync await _repository.SaveAsync(person); // Deposit message to outbox (held in memory) - await _commandProcessor.Post(new PersonCreated { PersonId = person.Id }, cancellationToken: cancellationToken); + await _commandProcessor.PostAsync(new PersonCreated { PersonId = person.Id }, cancellationToken: cancellationToken); return await base.HandleAsync(command, cancellationToken); } diff --git a/contents/MSSQLInbox.md b/contents/MSSQLInbox.md index 66f3669..7d8c6e6 100644 --- a/contents/MSSQLInbox.md +++ b/contents/MSSQLInbox.md @@ -16,14 +16,19 @@ For this we will need the *Inbox* packages for the MsSQL *Inbox*. * **Paramore.Brighter.Inbox.MsSql** -``` csharp -// ... +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Paramore.Brighter; +using Paramore.Brighter.Inbox.MsSql; +using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices(hostContext, services) => + .ConfigureServices((hostContext, services) => { ConfigureBrighter(hostContext, services); - } + }); private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { @@ -31,14 +36,15 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo { var configuration = new RelationalDatabaseConfiguration(connectionString, "BrighterTests", inboxTableName: "InboxMessages"); options.InboxConfiguration = new InboxConfiguration(new MsSqlInbox(configuration)); - ... + // ... }); } -... - +// ... ``` +In Brighter 10.7.0 this configuration takes effect only in an application that also calls `AddProducers`; see [Global Inbox Configuration in a Consumer-Only Application](/contents/BrighterInboxSupport.md#global-inbox-configuration-in-a-consumer-only-application). + ## Provisioning the MSSQL Inbox Table You have two equally valid options for creating and maintaining the Inbox table: diff --git a/contents/MSSQLOutbox.md b/contents/MSSQLOutbox.md index ed38170..005ea3c 100644 --- a/contents/MSSQLOutbox.md +++ b/contents/MSSQLOutbox.md @@ -106,6 +106,9 @@ To configure the MSSQL Outbox, you need to provide an outbox implementation in t First, define the configuration for your SQL Server database connection. We recommend retrieving the connection string from your application's configuration (e.g., `appsettings.json`) rather than hardcoding it. ```csharp +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; + // Get connection string from configuration var connectionString = "Server=localhost;Database=brighter;User Id=sa;Password=your_password;TrustServerCertificate=True;"; @@ -134,6 +137,15 @@ For more detailed information on integrating with Entity Framework Core, please Here is a complete example of configuring the MSSQL Outbox with EF Core. ```csharp +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.MsSql; +using Paramore.Brighter.MsSql.EntityFrameworkCore; +using Paramore.Brighter.Outbox.MsSql; +using Paramore.Brighter.Outbox.Hosting; + // In your DbContext, you would have your entities public class MyDbContext : DbContext { @@ -141,38 +153,41 @@ public class MyDbContext : DbContext public MyDbContext(DbContextOptions options) : base(options) {} } -// In ConfigureServices or Program.cs -public void ConfigureServices(IServiceCollection services) +// In your Startup class; in Program.cs, the same calls go on builder.Services +public class Startup { - var connectionString = "Server=localhost;Database=brighter;User Id=sa;Password=your_password;TrustServerCertificate=True;"; - - // 1. Add your DbContext - services.AddDbContext(options => - options.UseSqlServer(connectionString) - ); - - // 2. Configure the Outbox - var outboxConfiguration = new RelationalDatabaseConfiguration(connectionString, outBoxTableName: "Outbox"); - services.AddSingleton(outboxConfiguration); - - // 3. Configure Brighter - services.AddBrighter(options => + public void ConfigureServices(IServiceCollection services) { - // ... other Brighter options - }) - .AddProducers(producers => - { - producers.Outbox = new MsSqlOutbox(outboxConfiguration); - producers.ConnectionProvider = typeof(MsSqlConnectionProvider); - // Use the EF Core transaction provider with your DbContext. Note the "Core": MSSQL is - // the one provider that spells it MsSqlEntityFrameworkCoreTransactionProvider, where - // MySQL, PostgreSQL, SQLite and MongoDB all use EntityFrameworkTransactionProvider. - producers.TransactionProvider = typeof(MsSqlEntityFrameworkCoreTransactionProvider); + var connectionString = "Server=localhost;Database=brighter;User Id=sa;Password=your_password;TrustServerCertificate=True;"; + + // 1. Add your DbContext + services.AddDbContext(options => + options.UseSqlServer(connectionString) + ); + + // 2. Configure the Outbox + var outboxConfiguration = new RelationalDatabaseConfiguration(connectionString, outBoxTableName: "Outbox"); + services.AddSingleton(outboxConfiguration); + + // 3. Configure Brighter + services.AddBrighter(options => + { + // ... other Brighter options + }) + .AddProducers(producers => + { + producers.Outbox = new MsSqlOutbox(outboxConfiguration); + producers.ConnectionProvider = typeof(MsSqlConnectionProvider); + // Use the EF Core transaction provider with your DbContext. Note the "Core": MSSQL is + // the one provider that spells it MsSqlEntityFrameworkCoreTransactionProvider, where + // MySQL, PostgreSQL, SQLite and MongoDB all use EntityFrameworkTransactionProvider. + producers.TransactionProvider = typeof(MsSqlEntityFrameworkCoreTransactionProvider); - // ... configure your producers (e.g., for RabbitMQ, Kafka) - }) - .UseOutboxSweeper() // Optionally add the background sweeper service - .AutoFromAssemblies(); // Scan for handlers and mappers + // ... configure your producers (e.g., for RabbitMQ, Kafka) + }) + .UseOutboxSweeper() // Optionally add the background sweeper service + .AutoFromAssemblies(); // Scan for handlers and mappers + } } ``` diff --git a/contents/MongoDBInbox.md b/contents/MongoDBInbox.md index 36d12fe..d3f8b17 100644 --- a/contents/MongoDBInbox.md +++ b/contents/MongoDBInbox.md @@ -58,6 +58,8 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo } ``` +In Brighter 10.7.0 this configuration takes effect only in an application that also calls `AddProducers`; see [Global Inbox Configuration in a Consumer-Only Application](/contents/BrighterInboxSupport.md#global-inbox-configuration-in-a-consumer-only-application). + ### Advanced Configuration For more advanced scenarios, you can provide custom MongoDB client settings and collection configurations: diff --git a/contents/MySQLInbox.md b/contents/MySQLInbox.md index 747a557..b7ae862 100644 --- a/contents/MySQLInbox.md +++ b/contents/MySQLInbox.md @@ -16,27 +16,35 @@ For this we will need the *Inbox* packages for the MySQL *Inbox*. * **Paramore.Brighter.Inbox.MySql** -``` csharp +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Paramore.Brighter; +using Paramore.Brighter.Inbox.MySql; +using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices(hostContext, services) => + .ConfigureServices((hostContext, services) => { ConfigureBrighter(hostContext, services); - } + }); private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { services.AddConsumers(options => { var configuration = new RelationalDatabaseConfiguration(connectionString, "brighter_test", inboxTableName: "inbox_messages"); - opt.InboxConfiguration = new InboxConfiguration(new MySqlInbox(configuration)); - ... + options.InboxConfiguration = new InboxConfiguration(new MySqlInbox(configuration)); + // ... }); } -... +// ... ``` +In Brighter 10.7.0 this configuration takes effect only in an application that also calls `AddProducers`; see [Global Inbox Configuration in a Consumer-Only Application](/contents/BrighterInboxSupport.md#global-inbox-configuration-in-a-consumer-only-application). + ## Provisioning the MySQL Inbox Table You have two equally valid options for creating and maintaining the Inbox table: diff --git a/contents/MySQLOutbox.md b/contents/MySQLOutbox.md index a2b5b8c..604229a 100644 --- a/contents/MySQLOutbox.md +++ b/contents/MySQLOutbox.md @@ -50,6 +50,8 @@ The MySQL Outbox requires a specific table in your database to store messages be The `MySqlOutboxBuilder.GetDDL()` method creates the SQL script for you. You can execute this script against your database to create the outbox table. ```csharp +using Paramore.Brighter.Outbox.MySql; + // The table name can be whatever you choose. string tableName = "Outbox"; @@ -103,6 +105,9 @@ To configure the MySQL Outbox, you need to provide an outbox implementation in t First, define the configuration for your MySQL database connection. We recommend retrieving the connection string from your application's configuration (e.g., `appsettings.json`) rather than hardcoding it. ```csharp +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; + // Get connection string from configuration var connectionString = "server=localhost;port=3306;database=brighter;user=root;password=password;SslMode=None"; @@ -131,6 +136,15 @@ For more detailed information on integrating with Entity Framework Core, please Here is a complete example of configuring the MySQL Outbox with EF Core. ```csharp +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.MySql; +using Paramore.Brighter.MySql.EntityFrameworkCore; +using Paramore.Brighter.Outbox.MySql; +using Paramore.Brighter.Outbox.Hosting; + // In your DbContext, you would have your entities public class MyDbContext : DbContext { @@ -138,36 +152,39 @@ public class MyDbContext : DbContext public MyDbContext(DbContextOptions options) : base(options) {} } -// In ConfigureServices or Program.cs -public void ConfigureServices(IServiceCollection services) +// In your Startup class; in Program.cs, the same calls go on builder.Services +public class Startup { - var connectionString = "server=localhost;port=3306;database=brighter;user=root;password=password;SslMode=None"; - - // 1. Add your DbContext - services.AddDbContext(options => - options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)) - ); - - // 2. Configure the Outbox - var outboxConfiguration = new RelationalDatabaseConfiguration(connectionString, outBoxTableName: "Outbox"); - services.AddSingleton(outboxConfiguration); - - // 3. Configure Brighter - services.AddBrighter(options => + public void ConfigureServices(IServiceCollection services) { - .... - }) - .AddProducers(producers => - { - producers.Outbox = new MySqlOutbox(outboxConfiguration); - producers.ConnectionProvider = typeof(MySqlConnectionProvider); - // Use the EF Core transaction provider with your DbContext - producers.TransactionProvider = typeof(MySqlEntityFrameworkTransactionProvider); + var connectionString = "server=localhost;port=3306;database=brighter;user=root;password=password;SslMode=None"; + + // 1. Add your DbContext + services.AddDbContext(options => + options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)) + ); + + // 2. Configure the Outbox + var outboxConfiguration = new RelationalDatabaseConfiguration(connectionString, outBoxTableName: "Outbox"); + services.AddSingleton(outboxConfiguration); + + // 3. Configure Brighter + services.AddBrighter(options => + { + // ... other Brighter options + }) + .AddProducers(producers => + { + producers.Outbox = new MySqlOutbox(outboxConfiguration); + producers.ConnectionProvider = typeof(MySqlConnectionProvider); + // Use the EF Core transaction provider with your DbContext + producers.TransactionProvider = typeof(MySqlEntityFrameworkTransactionProvider); - // ... configure your producers (e.g., for RabbitMQ, Kafka) - }) - .UseOutboxSweeper() // Optionally add the background sweeper service - .AutoFromAssemblies(); // Scan for handlers and mappers + // ... configure your producers (e.g., for RabbitMQ, Kafka) + }) + .UseOutboxSweeper() // Optionally add the background sweeper service + .AutoFromAssemblies(); // Scan for handlers and mappers + } } ``` diff --git a/contents/PostgresInbox.md b/contents/PostgresInbox.md index 61bb0de..77b1e24 100644 --- a/contents/PostgresInbox.md +++ b/contents/PostgresInbox.md @@ -16,28 +16,35 @@ For this we will need the *Inbox* packages for the Postgres *Inbox*. * **Paramore.Brighter.Inbox.Postgres** -``` csharp +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Paramore.Brighter; +using Paramore.Brighter.Inbox.Postgres; +using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices(hostContext, services) => + .ConfigureServices((hostContext, services) => { ConfigureBrighter(hostContext, services); - } + }); private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { services.AddConsumers(options => { var config = new RelationalDatabaseConfiguration(connectionString, "brightertests", inboxTableName: "inboxmessages"); - opt.InboxConfiguration = new InboxConfiguration(new PostgreSqlInbox(config)); - ... + options.InboxConfiguration = new InboxConfiguration(new PostgreSqlInbox(config)); + // ... }); } -... - +// ... ``` +In Brighter 10.7.0 this configuration takes effect only in an application that also calls `AddProducers`; see [Global Inbox Configuration in a Consumer-Only Application](/contents/BrighterInboxSupport.md#global-inbox-configuration-in-a-consumer-only-application). + ## Provisioning the Postgres Inbox Table You have two equally valid options for creating and maintaining the Inbox table: diff --git a/contents/PostgresOutbox.md b/contents/PostgresOutbox.md index 2a61f69..ac7004c 100644 --- a/contents/PostgresOutbox.md +++ b/contents/PostgresOutbox.md @@ -109,6 +109,9 @@ To configure the PostgreSQL Outbox, you need to provide an outbox implementation First, define the configuration for your PostgreSQL database connection. We recommend retrieving the connection string from your application's configuration (e.g., `appsettings.json`) rather than hardcoding it. ```csharp +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; + // Get connection string from configuration var connectionString = "Host=localhost;Port=5432;Database=brighter;Username=postgres;Password=password"; @@ -137,6 +140,15 @@ For more detailed information on integrating with Entity Framework Core, please Here is a complete example of configuring the PostgreSQL Outbox with EF Core. ```csharp +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.PostgreSql; +using Paramore.Brighter.PostgreSql.EntityFrameworkCore; +using Paramore.Brighter.Outbox.PostgreSql; +using Paramore.Brighter.Outbox.Hosting; + // In your DbContext, you would have your entities public class MyDbContext : DbContext { @@ -144,36 +156,39 @@ public class MyDbContext : DbContext public MyDbContext(DbContextOptions options) : base(options) {} } -// In ConfigureServices or Program.cs -public void ConfigureServices(IServiceCollection services) +// In your Startup class; in Program.cs, the same calls go on builder.Services +public class Startup { - var connectionString = "Host=localhost;Port=5432;Database=brighter;Username=postgres;Password=password"; - - // 1. Add your DbContext - services.AddDbContext(options => - options.UseNpgsql(connectionString) - ); - - // 2. Configure the Outbox - var outboxConfiguration = new RelationalDatabaseConfiguration(connectionString, outBoxTableName: "Outbox"); - services.AddSingleton(outboxConfiguration); - - // 3. Configure Brighter - services.AddBrighter(options => + public void ConfigureServices(IServiceCollection services) { - // ... other Brighter options - }) - .AddProducers(producers => - { - producers.Outbox = new PostgreSqlOutbox(outboxConfiguration); - producers.ConnectionProvider = typeof(PostgreSqlConnectionProvider); - // Use the EF Core transaction provider with your DbContext - producers.TransactionProvider = typeof(PostgreSqlEntityFrameworkTransactionProvider); + var connectionString = "Host=localhost;Port=5432;Database=brighter;Username=postgres;Password=password"; + + // 1. Add your DbContext + services.AddDbContext(options => + options.UseNpgsql(connectionString) + ); + + // 2. Configure the Outbox + var outboxConfiguration = new RelationalDatabaseConfiguration(connectionString, outBoxTableName: "Outbox"); + services.AddSingleton(outboxConfiguration); + + // 3. Configure Brighter + services.AddBrighter(options => + { + // ... other Brighter options + }) + .AddProducers(producers => + { + producers.Outbox = new PostgreSqlOutbox(outboxConfiguration); + producers.ConnectionProvider = typeof(PostgreSqlConnectionProvider); + // Use the EF Core transaction provider with your DbContext + producers.TransactionProvider = typeof(PostgreSqlEntityFrameworkTransactionProvider); - // ... configure your producers (e.g., for RabbitMQ, Kafka) - }) - .UseOutboxSweeper() // Optionally add the background sweeper service - .AutoFromAssemblies(); // Scan for handlers and mappers + // ... configure your producers (e.g., for RabbitMQ, Kafka) + }) + .UseOutboxSweeper() // Optionally add the background sweeper service + .AutoFromAssemblies(); // Scan for handlers and mappers + } } ``` diff --git a/contents/SpannerInbox.md b/contents/SpannerInbox.md index 0426eca..802c999 100644 --- a/contents/SpannerInbox.md +++ b/contents/SpannerInbox.md @@ -52,6 +52,8 @@ public static class SpannerInboxRegistration } ``` +In Brighter 10.7.0 this configuration takes effect only in an application that also calls `AddProducers`; see [Global Inbox Configuration in a Consumer-Only Application](/contents/BrighterInboxSupport.md#global-inbox-configuration-in-a-consumer-only-application). + Constructed with the configuration alone, `SpannerInboxAsync` builds its own `SpannerConnectionProvider`; a second constructor takes an `IAmARelationalDbConnectionProvider` when you want to share one. diff --git a/contents/SqliteInbox.md b/contents/SqliteInbox.md index 9880bbe..2ecbc80 100644 --- a/contents/SqliteInbox.md +++ b/contents/SqliteInbox.md @@ -16,28 +16,36 @@ For this we will need the *Inbox* packages for the Sqlite *Inbox*. * **Paramore.Brighter.Inbox.Sqlite** -``` csharp +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Paramore.Brighter; +using Paramore.Brighter.Inbox; +using Paramore.Brighter.Inbox.Sqlite; +using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices(hostContext, services) => + .ConfigureServices((hostContext, services) => { ConfigureBrighter(hostContext, services); - } + }); private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { services.AddConsumers(options => { var configuration = new RelationalDatabaseConfiguration(connectionString, "brighter", inboxTableName: "inbox_messages"); - opt.InboxConfiguration = new InboxConfiguration(new SqliteInbox(configuration), actionOnExists: OnceOnlyAction.Warn); - ... + options.InboxConfiguration = new InboxConfiguration(new SqliteInbox(configuration), actionOnExists: OnceOnlyAction.Warn); + // ... }); } -... - +// ... ``` +In Brighter 10.7.0 this configuration takes effect only in an application that also calls `AddProducers`; see [Global Inbox Configuration in a Consumer-Only Application](/contents/BrighterInboxSupport.md#global-inbox-configuration-in-a-consumer-only-application). + ## Provisioning the Sqlite Inbox Table You have two equally valid options for creating and maintaining the Inbox table: diff --git a/contents/SqliteOutbox.md b/contents/SqliteOutbox.md index a951305..afe22c4 100644 --- a/contents/SqliteOutbox.md +++ b/contents/SqliteOutbox.md @@ -50,6 +50,8 @@ The SQLite Outbox requires a specific table in your database to store messages b The `SqliteOutboxBuilder.GetDDL()` method creates the SQL script for you. You can execute this script against your database to create the outbox table. ```csharp +using Paramore.Brighter.Outbox.Sqlite; + // The table name can be whatever you choose. string tableName = "Outbox"; @@ -102,6 +104,9 @@ To configure the SQLite Outbox, you need to provide an outbox implementation in First, define the configuration for your SQLite database connection. We recommend retrieving the connection string from your application's configuration (e.g., `appsettings.json`) rather than hardcoding it. ```csharp +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; + // Get connection string from configuration var connectionString = "Data Source=brighter.db"; @@ -130,6 +135,15 @@ For more detailed information on integrating with Entity Framework Core, please Here is a complete example of configuring the SQLite Outbox with EF Core. ```csharp +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Sqlite; +using Paramore.Brighter.Sqlite.EntityFrameworkCore; +using Paramore.Brighter.Outbox.Sqlite; +using Paramore.Brighter.Outbox.Hosting; + // In your DbContext, you would have your entities public class MyDbContext : DbContext { @@ -137,35 +151,38 @@ public class MyDbContext : DbContext public MyDbContext(DbContextOptions options) : base(options) {} } -// In ConfigureServices or Program.cs -public void ConfigureServices(IServiceCollection services) +// In your Startup class; in Program.cs, the same calls go on builder.Services +public class Startup { - var connectionString = "Data Source=brighter.db"; - - // 1. Add your DbContext - services.AddDbContext(options => - options.UseSqlite(connectionString) - ); - - // 2. Configure the Outbox - var outboxConfiguration = new RelationalDatabaseConfiguration(connectionString, outBoxTableName: "Outbox"); - services.AddSingleton(outboxConfiguration); - - // 3. Configure Brighter - services.AddBrighter(options => + public void ConfigureServices(IServiceCollection services) { - // ... other Brighter options - }) - .AddProducers(producers => - { - producers.Outbox = new SqliteOutbox(outboxConfiguration); - producers.ConnectionProvider = typeof(SqliteConnectionProvider); - // Use the EF Core transaction provider with your DbContext - producers.TransactionProvider = typeof(SqliteEntityFrameworkTransactionProvider); + var connectionString = "Data Source=brighter.db"; + + // 1. Add your DbContext + services.AddDbContext(options => + options.UseSqlite(connectionString) + ); + + // 2. Configure the Outbox + var outboxConfiguration = new RelationalDatabaseConfiguration(connectionString, outBoxTableName: "Outbox"); + services.AddSingleton(outboxConfiguration); + + // 3. Configure Brighter + services.AddBrighter(options => + { + // ... other Brighter options + }) + .AddProducers(producers => + { + producers.Outbox = new SqliteOutbox(outboxConfiguration); + producers.ConnectionProvider = typeof(SqliteConnectionProvider); + // Use the EF Core transaction provider with your DbContext + producers.TransactionProvider = typeof(SqliteEntityFrameworkTransactionProvider); - // ... configure your producers (e.g., for RabbitMQ, Kafka) - }) - .UseOutboxSweeper() // Optionally add the background sweeper service - .AutoFromAssemblies(); // Scan for handlers and mappers + // ... configure your producers (e.g., for RabbitMQ, Kafka) + }) + .UseOutboxSweeper() // Optionally add the background sweeper service + .AutoFromAssemblies(); // Scan for handlers and mappers + } } ``` diff --git a/tools/blockcheck/scaffold/pages.tsv b/tools/blockcheck/scaffold/pages.tsv index e69f91b..3dc3828 100644 --- a/tools/blockcheck/scaffold/pages.tsv +++ b/tools/blockcheck/scaffold/pages.tsv @@ -72,3 +72,14 @@ contents/SchedulingAMessage.md SchedulingAMessageContext - contents/QueryResultTypes.md QueryResultTypesContext - contents/QueryObjectValidation.md QueryObjectValidationContext - contents/TestingQueryHandlers.md TestingQueryHandlersContext - +contents/MSSQLOutbox.md RelationalOutboxContext - +contents/MySQLOutbox.md RelationalOutboxContext - +contents/PostgresOutbox.md RelationalOutboxContext - +contents/SqliteOutbox.md RelationalOutboxContext - +contents/MSSQLInbox.md RelationalInboxContext - +contents/MySQLInbox.md RelationalInboxContext - +contents/PostgresInbox.md RelationalInboxContext - +contents/SqliteInbox.md RelationalInboxContext - +contents/InMemoryInbox.md InMemoryBoxContext - +contents/InMemoryOutbox.md InMemoryBoxContext - +contents/DynamoInbox.md DynamoInboxContext - diff --git a/tools/blockcheck/scaffold/units/DynamoInboxContext.cs b/tools/blockcheck/scaffold/units/DynamoInboxContext.cs new file mode 100644 index 0000000..c74e840 --- /dev/null +++ b/tools/blockcheck/scaffold/units/DynamoInboxContext.cs @@ -0,0 +1,15 @@ +// Values DynamoInbox.md names in its blocks and never declares. +// +// Every member is typed from a pinned package, returns a default, and does nothing. A block that +// calls a member of one of these is checked against the real type, so a wrong member or argument +// still fails. +// +// blockcheck: using static DynamoInboxContext; + +using Amazon.Runtime; + +public static class DynamoInboxContext +{ + // block 1: `new AmazonDynamoDBClient(credentials, …)` — the reader's AWS credentials + public static AWSCredentials credentials => null!; +} diff --git a/tools/blockcheck/scaffold/units/DynamoOutboxContext.cs b/tools/blockcheck/scaffold/units/DynamoOutboxContext.cs index c153965..b552f3f 100644 --- a/tools/blockcheck/scaffold/units/DynamoOutboxContext.cs +++ b/tools/blockcheck/scaffold/units/DynamoOutboxContext.cs @@ -1,11 +1,14 @@ -// Values DynamoOutbox.md names in its blocks and never declares. +// Values and types DynamoOutbox.md names in its blocks and never declares. // -// Identifiers only: every member is typed from a pinned package or the BCL, returns a default, -// and does nothing. A block that calls a member of one of these is checked +// Block 2 is the Brighter WebAPI_Dynamo sample's `AddGreetingHandlerAsync`, and the page shows +// none of the requests or entities it uses; each stub carries only the members a block names, +// typed as the sample types them. Every value member is typed from a pinned package or the BCL, +// returns a default, and does nothing. A block that calls a member of one of these is checked // against the real type, so a wrong member or argument still fails. // // blockcheck: using static DynamoOutboxContext; +using System.Collections.Generic; using Amazon.DynamoDBv2; using Paramore.Brighter; @@ -15,3 +18,19 @@ public static class DynamoOutboxContext public static IAmAProducerRegistry producerRegistry => null!; public static IAmazonDynamoDB client => null!; } + +// block 2: `RequestHandlerAsync`, `addGreeting.Name`, `addGreeting.Greeting` +public class AddGreeting(string name, string greeting) : Command(Id.Random()) +{ + public string Name { get; } = name; + public string Greeting { get; } = greeting; +} + +// block 2: `context.LoadAsync(…)`, `person.Greetings.Add(…)` +public class Person +{ + public List Greetings { get; set; } = new List(); +} + +// block 2: `new GreetingMade(addGreeting.Greeting)` +public class GreetingMade(string greeting) : Event(Id.Random()); diff --git a/tools/blockcheck/scaffold/units/InMemoryBoxContext.cs b/tools/blockcheck/scaffold/units/InMemoryBoxContext.cs new file mode 100644 index 0000000..49502a3 --- /dev/null +++ b/tools/blockcheck/scaffold/units/InMemoryBoxContext.cs @@ -0,0 +1,52 @@ +// Values and types the InMemory Inbox and Outbox pages name in their blocks and never declare: +// InMemoryInbox.md and InMemoryOutbox.md, whose handlers share one small Person domain. +// +// Every value member is typed from a pinned package or the BCL, returns a default, and does +// nothing. Each type stub carries only the members a block names. A block that calls a member of +// one of these is checked against the real type, so a wrong member or argument still fails. +// +// blockcheck: using static InMemoryBoxContext; + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; + +public static class InMemoryBoxContext +{ + // InMemoryInbox.md block 1, InMemoryOutbox.md block 1: `services.AddConsumers(…)`, `services.AddBrighter(…)` + public static IServiceCollection services => null!; + // InMemoryInbox.md block 1: `options.Subscriptions = subscriptions` + public static IEnumerable subscriptions => null!; + // InMemoryOutbox.md block 1: `options.ProducerRegistry = producerRegistry` + public static IAmAProducerRegistry producerRegistry => null!; +} + +// InMemoryOutbox.md block 2: `RequestHandlerAsync`, `command.Name`, `command.Email` +public class CreatePerson() : Command(Id.Random()) +{ + public string Name { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; +} + +// InMemoryInbox.md block 2: `RequestHandlerAsync`, `@event.PersonId`; +// InMemoryOutbox.md block 2: `new PersonCreated { PersonId = person.Id }` +public class PersonCreated() : Event(Id.Random()) +{ + public string PersonId { get; set; } = string.Empty; +} + +// InMemoryOutbox.md block 2: `new Person(command.Name, command.Email)`, `person.Id`; +// InMemoryInbox.md block 2: `person.MarkAsCreated()` +public class Person(string name, string email) +{ + public string Id { get; } = string.Empty; + public void MarkAsCreated() { } +} + +// InMemoryInbox.md block 2: `_repository.GetByIdAsync(…)`; both pages' block 2: `_repository.SaveAsync(person)` +public class PersonRepository +{ + public Task GetByIdAsync(string id) => Task.FromResult(null!); + public Task SaveAsync(Person person) => Task.CompletedTask; +} diff --git a/tools/blockcheck/scaffold/units/PageContext.cs b/tools/blockcheck/scaffold/units/PageContext.cs index e325e42..da4bfce 100644 --- a/tools/blockcheck/scaffold/units/PageContext.cs +++ b/tools/blockcheck/scaffold/units/PageContext.cs @@ -1,13 +1,38 @@ -// Values DapperOutbox.md names in its blocks and never declares. +// Values and types DapperOutbox.md names in its blocks and never declares. // -// Identifiers only: every member is typed from the BCL and does nothing. It -// supplies what block 1 passes to `new RelationalDatabaseConfiguration(...)`, -// which the page elides as a connection string the reader already has. +// Block 1 passes a connection string the page elides as one the reader already has. Block 2 is the +// Brighter WebAPI_Dapper sample's `AddGreetingHandlerAsync`, and the page shows none of the +// requests or entities it uses. Each stub carries only the members a block names, typed as the +// sample types them. // // blockcheck: using static PageContext; +using Paramore.Brighter; + public static class PageContext { // DapperOutbox.md block 1: `connectionString,` public static string connectionString => "Server=localhost;Database=Greetings;"; } + +// block 2: `RequestHandlerAsync`, `addGreeting.Name`, `addGreeting.Greeting` +public class AddGreeting(string name, string greeting) : Command(Id.Random()) +{ + public string Name { get; } = name; + public string Greeting { get; } = greeting; +} + +// block 2: `conn.QueryAsync(…)` +public class Person; + +// block 2: `new Greeting(addGreeting.Greeting, person)`, `greeting.Message`, `greeting.RecipientId`, +// `greeting.Greet()` +public class Greeting(string message, Person recipient) +{ + public string? Message { get; set; } = message; + public long RecipientId { get; set; } + public string Greet() => $"{Message}!"; +} + +// block 2: `new GreetingMade(greeting.Greet())` +public class GreetingMade(string greeting) : Event(Id.Random()); diff --git a/tools/blockcheck/scaffold/units/RelationalInboxContext.cs b/tools/blockcheck/scaffold/units/RelationalInboxContext.cs new file mode 100644 index 0000000..ed8a53f --- /dev/null +++ b/tools/blockcheck/scaffold/units/RelationalInboxContext.cs @@ -0,0 +1,12 @@ +// Values the four relational Inbox pages name in their blocks and never declare: MSSQLInbox.md, +// MySQLInbox.md, PostgresInbox.md and SqliteInbox.md, which repeat one block per backend. +// +// Every member is typed from the BCL, returns a default, and does nothing. +// +// blockcheck: using static RelationalInboxContext; + +public static class RelationalInboxContext +{ + // block 1: `new RelationalDatabaseConfiguration(connectionString, …)` — the application's own + public static string connectionString => string.Empty; +} diff --git a/tools/blockcheck/scaffold/units/RelationalOutboxContext.cs b/tools/blockcheck/scaffold/units/RelationalOutboxContext.cs new file mode 100644 index 0000000..c298b4c --- /dev/null +++ b/tools/blockcheck/scaffold/units/RelationalOutboxContext.cs @@ -0,0 +1,16 @@ +// Values the four relational Outbox pages name in their blocks and never declare: MSSQLOutbox.md, +// MySQLOutbox.md, PostgresOutbox.md and SqliteOutbox.md, which repeat one set of blocks per backend. +// +// Every member is typed from a pinned package or the BCL, returns a default, and does nothing. A +// block that calls a member of one of these is checked against the real type, so a wrong member +// or argument still fails. +// +// blockcheck: using static RelationalOutboxContext; + +using Microsoft.Extensions.DependencyInjection; + +public static class RelationalOutboxContext +{ + // block 2: `services.AddSingleton(dbConfig)` — the composition root's + public static IServiceCollection services => null!; +} From f8c21aa436f22610407980c27e02b9b0d0f4a95b Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 19:50:11 +0100 Subject: [PATCH 05/21] blockcheck: baseline the twenty-four blocks 4.2 made build Twenty-two on the outbox and inbox pages, BrighterBasicConfiguration.md #3 and #4 by the lambda recurrence; MSSQLOutbox.md and PostgresOutbox.md #1 re-admitted with RelationalOutboxContext.cs. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- tools/blockcheck/baseline.tsv | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tools/blockcheck/baseline.tsv b/tools/blockcheck/baseline.tsv index 9f1a40f..51087bc 100644 --- a/tools/blockcheck/baseline.tsv +++ b/tools/blockcheck/baseline.tsv @@ -61,8 +61,11 @@ contents/CommandProcessorConfigurationReference.md 4 - 280d1b7 contents/Compression.md 1 CompressionContext.cs 71d0c60 contents/Compression.md 2 CompressionContext.cs 71d0c60 contents/DapperOutbox.md 1 PageContext.cs 280d1b7 +contents/DapperOutbox.md 2 PageContext.cs a8ede7c contents/DistributedLock.md 1 - 24e6e53 contents/DynamoOutbox.md 1 DynamoOutboxContext.cs 280d1b7 +contents/DynamoOutbox.md 2 DynamoOutboxContext.cs a8ede7c +contents/DynamoOutbox.md 3 DynamoOutboxContext.cs a8ede7c contents/DynamoOutbox.md 4 DynamoOutboxContext.cs 280d1b7 contents/ErrorHandlingOptions.md 1 ErrorHandlingOptionsContext.cs 71d0c60 contents/ErrorHandlingOptions.md 2 ErrorHandlingOptionsContext.cs 71d0c60 @@ -101,7 +104,9 @@ contents/Logging.md 1 - 280d1b7 contents/Logging.md 2 - 280d1b7 contents/MQTTConfiguration.md 1 TransportConfigurationContext.cs 7edaada contents/MSSQLMessageBroker.md 1 TransportConfigurationContext.cs 7edaada -contents/MSSQLOutbox.md 1 - 280d1b7 +contents/MSSQLOutbox.md 1 RelationalOutboxContext.cs a8ede7c +contents/MSSQLOutbox.md 2 RelationalOutboxContext.cs a8ede7c +contents/MSSQLOutbox.md 3 RelationalOutboxContext.cs a8ede7c contents/MSSQLTransportInboxAndOutbox.md 1 RelationalTransportContext.cs 280d1b7 contents/MSSQLTransportInboxAndOutbox.md 2 RelationalTransportContext.cs 280d1b7 contents/MSSQLTransportInboxAndOutbox.md 3 RelationalTransportContext.cs 280d1b7 @@ -145,7 +150,9 @@ contents/PostgreSQLTransportAndOutbox.md 4 RelationalTransportContext.cs 7edaada contents/PostgreSQLTransportAndOutbox.md 5 RelationalTransportContext.cs 7edaada contents/PostgreSQLTransportAndOutbox.md 6 RelationalTransportContext.cs 7edaada contents/PostgreSQLTransportAndOutbox.md 7 RelationalTransportContext.cs 280d1b7 -contents/PostgresOutbox.md 1 - 280d1b7 +contents/PostgresOutbox.md 1 RelationalOutboxContext.cs a8ede7c +contents/PostgresOutbox.md 2 RelationalOutboxContext.cs a8ede7c +contents/PostgresOutbox.md 3 RelationalOutboxContext.cs a8ede7c contents/ProjectionQueryPatterns.md 4 - 280d1b7 contents/QueriesAndQueryObjects.md 1 - 280d1b7 contents/QueriesAndQueryObjects.md 2 - 280d1b7 @@ -215,3 +222,20 @@ contents/TutorialStreamingWithKafka.md 1 TutorialStreamingWithKafkaContext.cs 80 contents/TutorialStreamingWithKafka.md 2 TutorialStreamingWithKafkaContext.cs 8090477 contents/UsingSweeperCircuitBreaking.md 1 UsingSweeperCircuitBreakingContext.cs 280d1b7 contents/UsingTheContextBag.md 12 - 280d1b7 +contents/BrighterBasicConfiguration.md 3 - a8ede7c +contents/BrighterBasicConfiguration.md 4 - a8ede7c +contents/DynamoInbox.md 1 DynamoInboxContext.cs a8ede7c +contents/InMemoryInbox.md 1 InMemoryBoxContext.cs a8ede7c +contents/InMemoryInbox.md 2 InMemoryBoxContext.cs a8ede7c +contents/InMemoryOutbox.md 1 InMemoryBoxContext.cs a8ede7c +contents/InMemoryOutbox.md 2 InMemoryBoxContext.cs a8ede7c +contents/MSSQLInbox.md 1 RelationalInboxContext.cs a8ede7c +contents/MySQLInbox.md 1 RelationalInboxContext.cs a8ede7c +contents/MySQLOutbox.md 1 RelationalOutboxContext.cs a8ede7c +contents/MySQLOutbox.md 2 RelationalOutboxContext.cs a8ede7c +contents/MySQLOutbox.md 3 RelationalOutboxContext.cs a8ede7c +contents/PostgresInbox.md 1 RelationalInboxContext.cs a8ede7c +contents/SqliteInbox.md 1 RelationalInboxContext.cs a8ede7c +contents/SqliteOutbox.md 1 RelationalOutboxContext.cs a8ede7c +contents/SqliteOutbox.md 2 RelationalOutboxContext.cs a8ede7c +contents/SqliteOutbox.md 3 RelationalOutboxContext.cs a8ede7c From 7d04918eca2e6cfc64fb99266d8256d0429ec545 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 19:52:01 +0100 Subject: [PATCH 06/21] =?UTF-8?q?spec:=20017=20phase=204=20task=204.2=20?= =?UTF-8?q?=E2=80=94=20outbox=20and=20inbox=20pages=20whole;=20#4335=20sta?= =?UTF-8?q?ted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- spec/017-compile_repairs/tasks.md | 96 ++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/spec/017-compile_repairs/tasks.md b/spec/017-compile_repairs/tasks.md index 23a90a2..9ba55d2 100644 --- a/spec/017-compile_repairs/tasks.md +++ b/spec/017-compile_repairs/tasks.md @@ -1251,7 +1251,7 @@ skipped with an accepted reason, or listed. Page list: § *The tranches*, phase before it is touched**: parse (placeholder / fragment / not code) or other (defect / wrapper artefact), from `--classify` and `--explain` -- [ ] **Task 4.2:** Repair the outbox and inbox pages of the tranche +- [x] **Task 4.2:** Repair the outbox and inbox pages of the tranche - Input: the phase 4 rows whose page is an Outbox or Inbox; 4.1's verdicts - Output: each page whole; baseline rows; each defect in § *Defect ledger* with its recurrence grep (obligation 14) @@ -1397,6 +1397,89 @@ placeholder leaves them FAILED: `AzureBlobArchiveProvider.md` #1 and the five in text one — a search of the page for `UseMisfireHandler` misses the line. The page is in no tranche; recorded for phase 5 to remove +**Task 4.2 — the outbox and inbox pages.** Thirteen pages. **BUILT 189 → 214** (+25): all **22** +FAILED blocks on the thirteen, `TickerQScheduler.md` #3 from the pin, and `BrighterBasicConfiguration.md` +#3, #4 from the lambda recurrence. **None of the thirteen keeps a FAILED block.** `pagelint` +**658 → 636**: **19** on the tranche pages, **2** on `BrighterBasicConfiguration.md` and **1** on +`AzureBlobArchiveProvider.md`, both touched by the recurrence (per page, against a worktree at +`d8633b1`). Pages with nothing BUILT **74 → 64**: `DynamoInbox.md`, `InMemoryInbox.md`, +`InMemoryOutbox.md`, `MSSQLInbox.md`, `MySQLInbox.md`, `MySQLOutbox.md`, `PostgresInbox.md`, +`SqliteInbox.md`, `SqliteOutbox.md` and `BrighterBasicConfiguration.md`. Two methods, one figure: +requirements' `awk` and a Python join over the same report. + +- **The pin, measured alone first** (`bd2b1ed`): `Npgsql.EntityFrameworkCore.PostgreSQL` 9.0.4 and + `TickerQ.Dashboard`, `TickerQ.EntityFrameworkCore` 9.0.2; **98** `PackageReference`s, **542** + reference assemblies. **Said at 4.1:** +2 BUILT. **Measured:** +1, `TickerQScheduler.md` #3. + #2 now fails on `Program` alone — `typeof(Program).Assembly` in a `Program.cs` with no declaration + after its statements, the `statements` wrapper's limitation that keeps `QueryPipelinePolicies.md` + #1 FAILED (Q2). Its row in § *Blocks that stay FAILED* is rewritten to that reason +- **The ≤ 60 target.** Of phase 4's ten pages with nothing BUILT and a reachable block, **4** landed + (`InMemoryInbox.md`, `InMemoryOutbox.md`, `MySQLOutbox.md`, `SqliteOutbox.md`); the other six are + lock pages, 4.3's. Five of the six hard-only pages landed as well, and `BrighterBasicConfiguration.md` + off the tranche. 64 − 60 = **4** still to land +- **`using`s:** every repaired block. On `MSSQLInbox.md`, `InMemoryInbox.md` and `InMemoryOutbox.md` + they replace the leading `// ...` +- **Four units and two grown.** `RelationalOutboxContext.cs` supplies `services` to the four EF Core + outbox pages; `RelationalInboxContext.cs` supplies `connectionString` to the four relational inbox + pages — two units, because each outbox page's block 2 declares its own `connectionString`. + `InMemoryBoxContext.cs` supplies `services`, `subscriptions`, `producerRegistry` and the small + `Person` domain both InMemory pages' handlers use. `DynamoInboxContext.cs` supplies `credentials`. + `PageContext.cs` (`DapperOutbox.md`) and `DynamoOutboxContext.cs` gain the requests and entities of + the samples their handlers come from, `WebAPI_Dapper` and `WebAPI_Dynamo`, typed as 10.7.0's samples + type them; each page now names its sample. None is a type a page tells the reader to write (rule 1, + by reading). `MSSQLOutbox.md` and `PostgresOutbox.md` #1, BUILT before, are re-admitted with the + new unit. `--report` → *"34 units checked, 0 violations"* +- **The wrapper artefacts, made whole** (design: a fragment the reader needs whole). The EF Core + outbox pages' free `public void ConfigureServices` now sits in `public class Startup`, with a + comment that in `Program.cs` the same calls go on `builder.Services`. `DapperOutbox.md` and + `DynamoOutbox.md` #2, an `override` with no class, are shown in `AddGreetingHandlerAsync` with the + fields and constructor the sample declares. `MySQLOutbox.md`'s `....` is `// ... other Brighter + options`, as its siblings write it +- **The unopened `ConfigureServices` lambda, at every recurrence: 13 → 0 on 8 pages**, the two + off-tranche pages included. `pagelint --changed` then asked for `using`s on the touched + `BrighterBasicConfiguration.md` #3, #4 and `AzureBlobArchiveProvider.md` #1; with them, + `--explain` found only a bare `...` in each `BrighterBasicConfiguration.md` block, now `// ...`, + and both build. `AzureBlobArchiveProvider.md` #1 keeps its five defects for 4.4. The five + `DispatcherConfigurationReference.md` blocks each open with `// ...`, so rule 6 does not reach them; + they stay FAILED on placeholders and names the page never shows +- **Six defects, and one upstream** (§ *Defect ledger*): `[UseInboxAsync]` on a class; `Post` awaited + with a `cancellationToken` it does not take; a `;` inside an object initialiser; `opt.` in a lambda + whose parameter is `options`; the InMemory Inbox said to keep entries until restart; the page's + `Warn` configuration beside an attribute whose default `Throw` wins. **BrighterCommand/Brighter#4335** + — a global `InboxConfiguration` is ignored unless the application calls `AddProducers`. Fixed by + #4396 on `master`, in no release. **Maintainer's ruling, 2026-09-27: state it once, link from the + inbox pages.** `BrighterInboxSupport.md` gains *Global Inbox Configuration in a Consumer-Only + Application*, with the workaround, and the nine inbox pages one sentence each after their + configuration block +- **Behaviour, run with controls** against released 10.7.0 packages in scratch console apps, + net10.0, one process per case: + + | Claim | Case → result | Control → result | + |---|---|---| + | `InMemoryInbox.md`: *"No cleanup: Old entries remain until process restart"* | entry written, fake clock +11 min, another add → entry **gone**, count **1** | no advance → present, count **2**; +6 min, under the 10-min scan interval → present | + | *"Memory bound: All seen message IDs held in memory"* | `EntryLimit = 4`, 10 adds → **8** held (compacted once, to half, then not again within the interval) | `EntryLimit = -1` → **10** | + | #1 with #2: a duplicate reaching the handler | the page's `Warn` configuration and #2's attribute, the same event published twice → publish 2 throws **`OnceOnlyException`**; handler runs **1** | the attribute with `onceOnlyAction: OnceOnlyAction.Warn` → both return; runs **1** | + | The global Inbox, from `InboxConfiguration` alone (#4335) | no attribute, `AddConsumers` only → both return; handler runs **2** | the same with `AddProducers` → runs **1**. The attribute without producers → runs **1** | + | The deduplication window, end to end | publish, fake clock +11 min, publish another, publish the first again → runs **2** | — the first two rows are its controls | + | `SqliteOutbox.md` intro: messages *"saved within the same transaction as your business logic"* | #1's DDL, #3 verbatim in `Startup`, an insert and `DepositPostAsync` in the EF transaction, commit → Greeting **1**, Outbox **1** | rollback → Greeting **0**, Outbox **0** | + + Read, not run: `DapperOutbox.md` #2 is the handler `TransactionalMessagingWithTheOutbox.md` #1 ran + in 3.3, unchanged but for its class. `DynamoOutbox.md` #2 and `DynamoInbox.md` #1 need DynamoDB; + they register and transact through types each compiled against, and assert nothing the SQLite run + above does not. The relational inbox blocks configure the store 3.2's MSSQL inbox run exercised +- **`attr_mismatch.py` → 7**, before the baseline rows +- **Baseline:** 1 row at `bd2b1ed` (the pin), 24 rows and 2 re-admissions at `a8ede7c`. `--report` → + exit **0**, *"983 blocks: 214 BUILT, 753 FAILED, 16 SKIPPED"*, baseline 214, 0 findings. Joined on + page and ordinal against the report at `9479991`, all 25 blocks that moved went `FAILED -> BUILT`, + and there are no new keys +- `linkcheck` 165 files, 0 broken; `versioncheck` 0 stale of 18 across 5; `symbolcheck` 0 findings; + `optioncheck` 0 mismatches across 59 tables, 519 rows; shape, redirects and `--verify` unmoved; + `pagelint --changed origin/master` 0 errors. **Pages changed: 20** (`git diff --name-only + 9479991..HEAD -- contents`): the **13** tranche pages, `AzureBlobArchiveProvider.md` (4.4's) and + **6** outside the tranche — `BrighterBasicConfiguration.md` and `DispatcherConfigurationReference.md` + by the lambda recurrence, `BrighterInboxSupport.md`, `FirestoreInbox.md`, `MongoDBInbox.md` and + `SpannerInbox.md` by the #4335 ruling + --- ## Phase 5 — Tranche 2b and the recorded falsehoods *(6 tasks, one PR, CHANGES THE SITE)* @@ -1749,8 +1832,7 @@ is rewritten against the tables below. | `DistributedLock.md` | 2 | `CS0234` `Paramore.Brighter.DynamoDb.V4`, `Locking.DynamoDB.V4`, `Outbox.DynamoDB.V4` | V4 package, not in the pin (D3, 018). The page recommends the V4 package, so the block carries its namespaces; it builds against the released V4 packages in scratch, **0** errors | 3 | | `DynamoDbDistributedLock.md` | 1 | `CS0234` `Locking.DynamoDB.V4`; `CS0103` `dynamoDb` | V4 package, not in the pin (D3, 018). `dynamoDb` wants a value stub once V4 is pinned | 3 | | `DynamoDbDistributedLock.md` | 2 | `CS0234` `Paramore.Brighter.DynamoDb.V4`, `Locking.DynamoDB.V4`, `Outbox.DynamoDB.V4` | V4 package, not in the pin (D3, 018); builds against the released V4 packages in scratch, **0** errors | 3 | -| `TickerQScheduler.md` | 2 | `CS0234` `TickerQ.EntityFrameworkCore`; `CS1061` `AddOperationalStore`; `CS0246` `TickerQDbContext` | `TickerQ.EntityFrameworkCore` is not in the pin; phase 4 asks for it at 9.0.2. Builds against the released packages in scratch, net9.0 and net10.0, **0** errors | 3 | -| `TickerQScheduler.md` | 3 | `CS0234` `TickerQ.Dashboard`; `CS1061` `AddDashboard` | `TickerQ.Dashboard` is not in the pin; phase 4 asks for it at 9.0.2. Builds against the released packages in scratch, net9.0 and net10.0, **0** errors | 3 | +| `TickerQScheduler.md` | 2 | `CS0246` `Program` | instrument: `typeof(Program).Assembly` in a `Program.cs` with no declaration after its statements takes the `statements` wrapper, which declares no `Program`; Q2 (1.8) rules out a stub. Builds against the released packages in scratch, net9.0 and net10.0, **0** errors | 3 | | `PaginationQueryPatterns.md` | 2 | `CS0246` `GetOrdersPageQuery`, `PagedResult<>`, `OrderDto`; `ApplicationDbContext` | same-page: block 1 declares the first three; block 2 is *"Handler with pagination:"*, straight after it | 3 | | `PaginationQueryPatterns.md` | 3 | `CS0246` `OrderDto` | same-page: block 1 declares it | 3 | | `PaginationQueryPatterns.md` | 4 | `CS0246` `GetOrdersCursorQuery`, `CursorPagedResult<>`, `OrderDto`; `ApplicationDbContext` | same-page: block 3 declares the first two, block 1 `OrderDto`; block 4 is *"Handler with cursor pagination:"*, straight after block 3 | 3 | @@ -1818,6 +1900,14 @@ BUILT, re-admitted at `ec38400`. | Darker's default policies described as *"exponential backoff"* and a breaker that *"opens after consecutive failures"*, and as applying once registered. They retry 3 times after 50, 100 and 150 ms, the breaker opens on 1 failure for 500 ms, and neither runs without `[RetryableQuery]` | Darker 4.1.1 `QueryProcessorBuilderExtensions.cs:51`, `RetryableQueryDecorator.cs`; run, control without the attribute | `QueryPipelinePolicies.md` (the list and block 2's comment) | `grep -rnE 'Retries with exponential backoff\|Opens after consecutive failures\|Retry policy with exponential backoff' contents/` | **3** | **0** | 3.5, reading the page against Darker's source, then running | | *"The ASP.NET model binder will validate these attributes before the query reaches your handler"*. Only a controller marked `[ApiController]`, or a minimal API after `AddValidation()` (.NET 10), rejects the query; elsewhere it reaches the code | run on net10.0, controls both ways | `QueryObjectValidation.md` | `grep -rn 'model binder will validate' contents/` | **1** | **0** | 3.5, running block 2's claim | | `[RetryableQuery]`'s second argument described and used as a circuit-breaker name that adds a breaker to the retry. It is a policy name, and the decorator runs that one policy. `"DefaultCircuitBreaker"` is not registered by `AddDefaultPolicies()`, so it throws `ConfigurationException`; `circuitBreakerName:` is not a parameter (`CS1739`) | Darker 4.1.1 `RetryableQueryAttribute.cs:11`, `Constants.cs`; run, control `Constants.CircuitBreakerPolicyName`; compiled | `QueryPipeline.md` (4 lines, and the parameter list at line 239), `CQRSWithBrighterAndDarker.md` (2), `DarkerAndBrighterPipelines.md`, `ImplementAQueryHandler.md`, `QueryPatterns.md` | `grep -rnE 'RetryableQuery\(.*(DefaultCircuitBreaker\|circuitBreakerName)' contents/` | **9** lines, 5 pages | **open — phase 5**, maintainer's ruling | 3.5, reading Darker's source for the tranche's policy defaults | +| `.ConfigureServices(hostContext, services) =>` — the lambda's parameter list never opened, and its body never closed (`CS1519`, `CS1001`) | compiled, old form `CS1519` | `MSSQLInbox.md`, `MySQLInbox.md`, `PostgresInbox.md`, `SqliteInbox.md`, `DynamoInbox.md`, `AzureBlobArchiveProvider.md`, `BrighterBasicConfiguration.md` ×2, `DispatcherConfigurationReference.md` ×5 | `grep -rn 'ConfigureServices(hostContext, services) =>' contents/` | **13** lines, 8 pages | **0** | 4.1, `--classify` | +| `opt.InboxConfiguration` inside `AddConsumers(options => …)` — `CS0103` | compiled | `MySQLInbox.md`, `PostgresInbox.md`, `SqliteInbox.md` | `grep -rn '^\s*opt\.InboxConfiguration' contents/` — **4** before, **1** after, `DynamoInbox.md`'s, whose parameter is `opt` | **3** | **0** | 4.1, a scan of every lambda | +| `[UseInboxAsync]` on a handler class — `CS0592`; `RequestHandlerAttribute` is valid on methods only | `RequestHandlerAttribute.cs`, `AttributeUsage(AttributeTargets.Method)` | `InMemoryInbox.md` #2 | `grep -rn -A1 '^\s*\[UseInbox' contents/ \| grep -c class` | **1** | **0** | 4.1, `--explain` | +| `await _commandProcessor.Post(…, cancellationToken: …)` — `Post` returns `void` and takes no `cancellationToken`; the async form is `PostAsync` | `IAmACommandProcessor.cs:205`, `:241` | `InMemoryOutbox.md` #2 | `grep -rnE 'await [_a-zA-Z.]*\.(Post\|Send\|Publish\|DepositPost\|ClearOutbox)\(' contents/` | **1** | **0** | 4.2, `--explain` after the block's `using`s | +| `new AmazonDynamoDBConfig { ServiceURL = "…"; }` — a `;` inside an object initialiser | compiled | `DynamoInbox.md` #1 | `grep -rnP 'new [A-Za-z_.<>]+(\([^()]*\))? *\{[^{}]*;[^{}]*\}' contents/`, one line only; control: the old page → **1** | **1** | **0** | 4.1, `--classify` | +| The InMemory Inbox said to keep every entry until restart (*"No cleanup"*, *"All seen message IDs held in memory"*). An entry expires `EntryTimeToLive` (5 min) after it is written, removed by a scan at most every `ExpirationScanInterval` (10 min); past `EntryLimit` (2048) adding compacts the oldest to half | `InMemoryBox.cs:64–100`, `InMemoryInbox.cs:316`; run, controls both ways | `InMemoryInbox.md` | `grep -rnE 'No cleanup\|All seen message IDs held in memory' contents/` | **2** | **0** | 4.2, reading the page against the source, then running | +| A global `actionOnExists: Warn` shown beside a `[UseInboxAsync]` that sets no `onceOnlyAction` — the attribute's default `Throw` wins, so a duplicate throws `OnceOnlyException` | `PipelineBuilder.cs:371`, `HasExistingUseInboxAttributesInPipeline`; run, control the attribute with `Warn` | `InMemoryInbox.md` #1, #2 | pages with `actionOnExists: OnceOnlyAction.Warn\|Replay` and a `[UseInbox…]` without `onceOnlyAction` on its line: 2, read — `TurningOnReplayOnSeen.md`'s attributes set it on the next line and the page states the precedence | **1** | **0** | 4.2, running #1 with #2 | +| A global `InboxConfiguration` in `AddConsumers` reaches the pipeline only through `ExternalBus(…)`, so an application that never calls `AddProducers` gets no global Inbox, and duplicates run again | `ServiceCollectionExtensions.cs:640–660`; run, control with `AddProducers` — **upstream, BrighterCommand/Brighter#4335**, fixed by #4396 on `master`, unreleased | `BrighterInboxSupport.md` states it with the workaround; linked from `MSSQLInbox.md`, `MySQLInbox.md`, `PostgresInbox.md`, `SqliteInbox.md`, `DynamoInbox.md`, `MongoDBInbox.md`, `FirestoreInbox.md`, `SpannerInbox.md`, `InMemoryInbox.md`. Not linked: the seven other pages that configure one | `git grep -l 'InboxConfiguration' d8633b1 -- contents` | **16** pages | **stated** on 1, linked from 9 — maintainer's ruling | 4.2, running `InMemoryInbox.md` #1 without #2's attribute | ## Friction ledger From b8f21ac1037498f13d0abd8269b7a1ee3a5b2aa3 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 19:56:36 +0100 Subject: [PATCH 07/21] docs: the six distributed-lock pages compile; session release run on three MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each lock page's two blocks take their using directives, and the Outbox placeholder becomes a value the new DistributedLockProviderContext unit supplies: opt.Outbox = outbox; // your … Outbox. No defect: every prose claim matches 10.7.0, and the session-scoped release on MS SQL, MySQL and Postgres was run against real servers with a control. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- contents/AzureBlobDistributedLock.md | 14 ++++++++++++- contents/FirestoreDistributedLock.md | 14 ++++++++++++- contents/MongoDbDistributedLock.md | 14 ++++++++++++- contents/MsSqlDistributedLock.md | 13 +++++++++++- contents/MySqlDistributedLock.md | 13 +++++++++++- contents/PostgresDistributedLock.md | 11 +++++++++- tools/blockcheck/scaffold/pages.tsv | 6 ++++++ .../units/DistributedLockProviderContext.cs | 21 +++++++++++++++++++ 8 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 tools/blockcheck/scaffold/units/DistributedLockProviderContext.cs diff --git a/contents/AzureBlobDistributedLock.md b/contents/AzureBlobDistributedLock.md index 3b7046e..0086434 100644 --- a/contents/AzureBlobDistributedLock.md +++ b/contents/AzureBlobDistributedLock.md @@ -28,6 +28,10 @@ Configure the provider with `AzureBlobLockingProvider`, passing an `TokenCredential`: ```csharp +using System; +using Azure.Identity; +using Paramore.Brighter.Locking.Azure; + new AzureBlobLockingProvider( new AzureBlobLockingProviderOptions( blobContainerUri: new Uri("https://myaccount.blob.core.windows.net/brighter-locks"), @@ -56,11 +60,19 @@ initialiser like any other member. ## Azure Blob Distributed Lock Example ```csharp +using System; +using Azure.Identity; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Locking.Azure; +using Paramore.Brighter.Outbox.Hosting; + services .AddBrighter() .AddProducers(opt => { - opt.Outbox = /* your external Outbox */; + opt.Outbox = outbox; // your external Outbox // ... connection/transaction providers for your Outbox ... opt.DistributedLock = new AzureBlobLockingProvider( diff --git a/contents/FirestoreDistributedLock.md b/contents/FirestoreDistributedLock.md index 6ebc9c2..102eac7 100644 --- a/contents/FirestoreDistributedLock.md +++ b/contents/FirestoreDistributedLock.md @@ -28,6 +28,10 @@ constructor takes your project id and database. Set its `Locking` property to a `FirestoreCollection` that names the lock collection and, optionally, a time-to-live: ```csharp +using System; +using Paramore.Brighter.Firestore; +using Paramore.Brighter.Locking.Firestore; + var configuration = new FirestoreConfiguration( projectId: "my-gcp-project", database: "(default)") @@ -55,6 +59,14 @@ matter here are `Locking.Name`, which names the collection holding the lock docu ## Firestore Distributed Lock Example ```csharp +using System; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Firestore; +using Paramore.Brighter.Locking.Firestore; +using Paramore.Brighter.Outbox.Hosting; + var configuration = new FirestoreConfiguration("my-gcp-project", "(default)") { Locking = new FirestoreCollection { Name = "brighter-locks", Ttl = TimeSpan.FromMinutes(1) } @@ -64,7 +76,7 @@ services .AddBrighter() .AddProducers(opt => { - opt.Outbox = /* your external Outbox */; + opt.Outbox = outbox; // your external Outbox // ... connection/transaction providers for your Outbox ... opt.DistributedLock = new FirestoreDistributedLock(configuration); diff --git a/contents/MongoDbDistributedLock.md b/contents/MongoDbDistributedLock.md index 0da7af5..53124f7 100644 --- a/contents/MongoDbDistributedLock.md +++ b/contents/MongoDbDistributedLock.md @@ -30,6 +30,10 @@ configuration type used by the MongoDB Outbox. Set its `Locking` property to a time-to-live for lock documents: ```csharp +using System; +using Paramore.Brighter.Locking.MongoDb; +using Paramore.Brighter.MongoDb; + var configuration = new MongoDbConfiguration( connectionString: "mongodb://localhost:27017", databaseName: "orders") @@ -57,6 +61,14 @@ that matter here are `Locking.Name`, which names the collection holding the lock ## MongoDB Distributed Lock Example ```csharp +using System; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Locking.MongoDb; +using Paramore.Brighter.MongoDb; +using Paramore.Brighter.Outbox.Hosting; + var configuration = new MongoDbConfiguration("mongodb://localhost:27017", "orders") { Locking = new MongoDbCollectionConfiguration { Name = "brighter_locks", TimeToLive = TimeSpan.FromMinutes(1) } @@ -66,7 +78,7 @@ services .AddBrighter() .AddProducers(opt => { - opt.Outbox = /* your MongoDB Outbox */; + opt.Outbox = outbox; // your MongoDB Outbox // ... connection/transaction providers for your Outbox ... opt.DistributedLock = new MongoDbLockingProvider(configuration); diff --git a/contents/MsSqlDistributedLock.md b/contents/MsSqlDistributedLock.md index e754ce2..1716781 100644 --- a/contents/MsSqlDistributedLock.md +++ b/contents/MsSqlDistributedLock.md @@ -33,6 +33,10 @@ documented once in the [Relational Database Configuration Reference](/contents/RelationalDatabaseConfigurationReference.md): ```csharp +using Paramore.Brighter; +using Paramore.Brighter.Locking.MsSql; +using Paramore.Brighter.MsSql; + var configuration = new RelationalDatabaseConfiguration( connectionString: "Server=localhost;Database=orders;Trusted_Connection=True;"); @@ -45,6 +49,13 @@ session. ## MS SQL Distributed Lock Example ```csharp +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Locking.MsSql; +using Paramore.Brighter.MsSql; +using Paramore.Brighter.Outbox.Hosting; + var configuration = new RelationalDatabaseConfiguration( "Server=localhost;Database=orders;Trusted_Connection=True;"); @@ -52,7 +63,7 @@ services .AddBrighter() .AddProducers(opt => { - opt.Outbox = /* your MS SQL Outbox */; + opt.Outbox = outbox; // your MS SQL Outbox opt.ConnectionProvider = typeof(MsSqlConnectionProvider); opt.TransactionProvider = typeof(MsSqlTransactionProvider); diff --git a/contents/MySqlDistributedLock.md b/contents/MySqlDistributedLock.md index 71c1dcd..0ce3c9c 100644 --- a/contents/MySqlDistributedLock.md +++ b/contents/MySqlDistributedLock.md @@ -32,6 +32,10 @@ connection string, and its options are documented once in the [Relational Databa Configuration Reference](/contents/RelationalDatabaseConfigurationReference.md): ```csharp +using Paramore.Brighter; +using Paramore.Brighter.Locking.MySql; +using Paramore.Brighter.MySql; + var configuration = new RelationalDatabaseConfiguration( connectionString: "Server=localhost;Database=orders;Uid=app;Pwd=secret;"); @@ -44,6 +48,13 @@ session. ## MySQL Distributed Lock Example ```csharp +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Locking.MySql; +using Paramore.Brighter.MySql; +using Paramore.Brighter.Outbox.Hosting; + var configuration = new RelationalDatabaseConfiguration( "Server=localhost;Database=orders;Uid=app;Pwd=secret;"); @@ -51,7 +62,7 @@ services .AddBrighter() .AddProducers(opt => { - opt.Outbox = /* your MySQL Outbox */; + opt.Outbox = outbox; // your MySQL Outbox opt.ConnectionProvider = typeof(MySqlConnectionProvider); opt.TransactionProvider = typeof(MySqlTransactionProvider); diff --git a/contents/PostgresDistributedLock.md b/contents/PostgresDistributedLock.md index d69bbb8..4d15a39 100644 --- a/contents/PostgresDistributedLock.md +++ b/contents/PostgresDistributedLock.md @@ -27,6 +27,8 @@ Configure the provider with `PostgresLockingProvider`, passing a `PostgresLockingProviderOptions` that carries the connection string: ```csharp +using Paramore.Brighter.Locking.PostgresSql; + new PostgresLockingProvider( new PostgresLockingProviderOptions( connectionString: "Host=localhost;Database=orders;Username=app;Password=secret")); @@ -46,13 +48,20 @@ even though the Postgres Outbox beside it does. ## Postgres Distributed Lock Example ```csharp +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Locking.PostgresSql; +using Paramore.Brighter.Outbox.Hosting; +using Paramore.Brighter.PostgreSql; + const string connectionString = "Host=localhost;Database=orders;Username=app;Password=secret"; services .AddBrighter() .AddProducers(opt => { - opt.Outbox = /* your Postgres Outbox */; + opt.Outbox = outbox; // your Postgres Outbox opt.ConnectionProvider = typeof(PostgreSqlConnectionProvider); opt.TransactionProvider = typeof(PostgreSqlTransactionProvider); diff --git a/tools/blockcheck/scaffold/pages.tsv b/tools/blockcheck/scaffold/pages.tsv index 3dc3828..39375f5 100644 --- a/tools/blockcheck/scaffold/pages.tsv +++ b/tools/blockcheck/scaffold/pages.tsv @@ -83,3 +83,9 @@ contents/SqliteInbox.md RelationalInboxContext - contents/InMemoryInbox.md InMemoryBoxContext - contents/InMemoryOutbox.md InMemoryBoxContext - contents/DynamoInbox.md DynamoInboxContext - +contents/AzureBlobDistributedLock.md DistributedLockProviderContext - +contents/FirestoreDistributedLock.md DistributedLockProviderContext - +contents/MongoDbDistributedLock.md DistributedLockProviderContext - +contents/MsSqlDistributedLock.md DistributedLockProviderContext - +contents/MySqlDistributedLock.md DistributedLockProviderContext - +contents/PostgresDistributedLock.md DistributedLockProviderContext - diff --git a/tools/blockcheck/scaffold/units/DistributedLockProviderContext.cs b/tools/blockcheck/scaffold/units/DistributedLockProviderContext.cs new file mode 100644 index 0000000..bb6f1ee --- /dev/null +++ b/tools/blockcheck/scaffold/units/DistributedLockProviderContext.cs @@ -0,0 +1,21 @@ +// Values the six distributed-lock provider pages name in their blocks and never declare: +// AzureBlobDistributedLock.md, FirestoreDistributedLock.md, MongoDbDistributedLock.md, +// MsSqlDistributedLock.md, MySqlDistributedLock.md and PostgresDistributedLock.md, which repeat +// one registration block per backend. +// +// Every member is typed from a pinned package or the BCL, returns a default, and does nothing. A +// block that calls a member of one of these is checked against the real type, so a wrong member +// or argument still fails. +// +// blockcheck: using static DistributedLockProviderContext; + +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; + +public static class DistributedLockProviderContext +{ + // block 2: `services.AddBrighter()` — the composition root's + public static IServiceCollection services => null!; + // block 2: `opt.Outbox = outbox` — the reader's own Outbox, configured on its page + public static IAmAnOutbox outbox => null!; +} From 05b891658e68280e01e83ba7ac8dc964ba742e02 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 19:56:58 +0100 Subject: [PATCH 08/21] blockcheck: baseline the twelve blocks 4.3 made build Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- tools/blockcheck/baseline.tsv | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tools/blockcheck/baseline.tsv b/tools/blockcheck/baseline.tsv index 51087bc..133be28 100644 --- a/tools/blockcheck/baseline.tsv +++ b/tools/blockcheck/baseline.tsv @@ -239,3 +239,15 @@ contents/SqliteInbox.md 1 RelationalInboxContext.cs a8ede7c contents/SqliteOutbox.md 1 RelationalOutboxContext.cs a8ede7c contents/SqliteOutbox.md 2 RelationalOutboxContext.cs a8ede7c contents/SqliteOutbox.md 3 RelationalOutboxContext.cs a8ede7c +contents/AzureBlobDistributedLock.md 1 DistributedLockProviderContext.cs b8f21ac +contents/AzureBlobDistributedLock.md 2 DistributedLockProviderContext.cs b8f21ac +contents/FirestoreDistributedLock.md 1 DistributedLockProviderContext.cs b8f21ac +contents/FirestoreDistributedLock.md 2 DistributedLockProviderContext.cs b8f21ac +contents/MongoDbDistributedLock.md 1 DistributedLockProviderContext.cs b8f21ac +contents/MongoDbDistributedLock.md 2 DistributedLockProviderContext.cs b8f21ac +contents/MsSqlDistributedLock.md 1 DistributedLockProviderContext.cs b8f21ac +contents/MsSqlDistributedLock.md 2 DistributedLockProviderContext.cs b8f21ac +contents/MySqlDistributedLock.md 1 DistributedLockProviderContext.cs b8f21ac +contents/MySqlDistributedLock.md 2 DistributedLockProviderContext.cs b8f21ac +contents/PostgresDistributedLock.md 1 DistributedLockProviderContext.cs b8f21ac +contents/PostgresDistributedLock.md 2 DistributedLockProviderContext.cs b8f21ac From 05fdeaf27e6d95d042dc41049c00193a0ec5bc54 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 19:57:44 +0100 Subject: [PATCH 09/21] =?UTF-8?q?spec:=20017=20phase=204=20task=204.3=20?= =?UTF-8?q?=E2=80=94=20lock=20pages=20whole;=20the=20=E2=89=A4=2060=20targ?= =?UTF-8?q?et=20met=20at=2058?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- spec/017-compile_repairs/tasks.md | 50 +++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/spec/017-compile_repairs/tasks.md b/spec/017-compile_repairs/tasks.md index 9ba55d2..b9abc39 100644 --- a/spec/017-compile_repairs/tasks.md +++ b/spec/017-compile_repairs/tasks.md @@ -1256,7 +1256,7 @@ skipped with an accepted reason, or listed. Page list: § *The tranches*, phase - Output: each page whole; baseline rows; each defect in § *Defect ledger* with its recurrence grep (obligation 14) -- [ ] **Task 4.3:** Repair the distributed-lock pages of the tranche +- [x] **Task 4.3:** Repair the distributed-lock pages of the tranche - Input: the phase 4 rows whose page is a Distributed Lock; 4.1's verdicts - Output: each page whole; baseline rows; ledger rows as 4.2 - Notes: the lock pages share a shape; a defect found on one is grepped for on all before the @@ -1385,7 +1385,7 @@ placeholder leaves them FAILED: `AzureBlobArchiveProvider.md` #1 and the five in - **`Order`**: no phase 4 block names it (`grep -cw Order` over each of the 40 FAILED blocks' `--show` output → 0). The 1.10 ruling has nothing to act on here - **Shared worlds.** The four EF Core outbox pages (`MSSQLOutbox.md`, `MySQLOutbox.md`, - `PostgresOutbox.md`, `SqliteOutbox.md`) repeat one block shape, and the five `*DistributedLock.md` + `PostgresOutbox.md`, `SqliteOutbox.md`) repeat one block shape, and the six `*DistributedLock.md` pages another; a unit or a repair on one is tried on its siblings before the next page is opened. `InMemoryInbox.md` #1 wants `services` and `subscriptions`, which `InMemoryTransportContext.cs` already supplies @@ -1480,6 +1480,52 @@ requirements' `awk` and a Python join over the same report. by the lambda recurrence, `BrighterInboxSupport.md`, `FirestoreInbox.md`, `MongoDBInbox.md` and `SpannerInbox.md` by the #4335 ruling +**Task 4.3 — the distributed-lock pages.** Six pages. **BUILT 214 → 226** (+12): #1 and #2 on each +of `AzureBlobDistributedLock.md`, `FirestoreDistributedLock.md`, `MongoDbDistributedLock.md`, +`MsSqlDistributedLock.md`, `MySqlDistributedLock.md` and `PostgresDistributedLock.md`. **None stays +FAILED.** `pagelint` **636 → 624**, two on each page (per page, against a worktree at `7d04918`). +Pages with nothing BUILT **64 → 58**, all six; requirements' `awk` and a Python join agree. **The +≤ 60 target is met**: phase 4 landed all ten of its pages with nothing BUILT and a reachable block, +and five of its six hard-only pages. + +- **Said at 4.1:** *"the five `*DistributedLock.md` pages"* share a shape. **Measured:** the phase 4 + table holds six, and all six share it. Rewritten above +- **One repair, tried on all six before any page was opened alone.** Block 1 takes its `using`s. + Block 2 takes them too, and its `opt.Outbox = /* your … Outbox */;` becomes `opt.Outbox = outbox; + // your … Outbox` — the design's placeholder rule, completing the statement it sat in +- **One unit.** `DistributedLockProviderContext.cs` supplies `services` and `outbox`, typed + `IAmAnOutbox` as `ProducersConfiguration.Outbox` is (`ProducersConfiguration.cs:212`). Each page + configures its Outbox on the Outbox's own page and tells the reader to write neither (rule 1, by + reading). `--report` → *"35 units checked, 0 violations"* +- **No defect.** Every page's prose was read against 10.7.0: `sp_getapplock` in `Exclusive` mode at + `Session` scope with a zero timeout (`MsSqlLockingQueries.cs`); `GET_LOCK` with a one-second + timeout and a SHA-512 name truncated to 160 bits (`MySqlLockingProvider.cs:170`); + `pg_try_advisory_lock`; a MongoDB insert on `_id` with the duplicate key refused and a TTL index + from `Locking.TimeToLive` (`BaseMongoDb.cs:111`); a Firestore create with `Exists = false`; an + Azure blob uploaded if absent and leased for `LeaseValidity`, the container never created. + `optioncheck` reads the two tables here, unchanged +- **Behaviour, run with controls** against released 10.7.0 packages and real servers in Docker + (`postgres:16`, `mysql:8`, `azure-sql-edge`), net10.0, one process per case. Each provider is + built as its page's block 1 builds it; a "crash" is the holder's server session killed without a + release: + + | Claim | Case → result | Control → result | + |---|---|---| + | `PostgresDistributedLock.md`: released when the session closes, *"including if the holding instance crashes"* | A obtains; B refused; A's backend terminated → a new instance **obtains** | A alive → a new instance **refused** | + | `MySqlDistributedLock.md`: the same | A obtains; B refused; A's connection killed → **obtains** | A alive → **refused** | + | `MsSqlDistributedLock.md`: the same | A obtains; B refused; A's session killed → **obtains** | A alive → **refused** | + + Read, not run: the MongoDB, Firestore and Azure Blob providers, whose recovery is a TTL or a + lease the pages already describe as bounded by it, and which need their emulators +- **`attr_mismatch.py` → 7**, before the baseline rows +- **Baseline:** 12 rows at `b8f21ac`. `--report` → exit **0**, *"983 blocks: 226 BUILT, 741 FAILED, + 16 SKIPPED"*, baseline 226, 0 findings. Joined on page and ordinal against 4.2's report, all 12 + blocks that moved went `FAILED -> BUILT`, and there are no new keys +- `linkcheck` 165 files, 0 broken; `versioncheck` 0 stale of 18 across 5; `symbolcheck` 0 findings; + `optioncheck` 0 mismatches across 59 tables, 519 rows; shape, redirects and `--verify` unmoved; + `pagelint --changed origin/master` 0 errors. **Pages changed: 6** (`git diff --name-only + 7d04918..HEAD -- contents`), all on the tranche + --- ## Phase 5 — Tranche 2b and the recorded falsehoods *(6 tasks, one PR, CHANGES THE SITE)* From 2defac6f8dabc7b807117563023a971b202c80d9 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 20:59:55 +0100 Subject: [PATCH 10/21] docs: the sweeper circuit-breaking and Azure Blob archive pages compile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UsingSweeperCircuitBreaking.md: using directives on blocks 2 and 3, and `services` in the page's unit. Block 4 named a CircuitBreakerState no package or block declares, and shared a Dictionary between TripTopic and CoolDown; it now declares its state and uses a ConcurrentDictionary. Block 5 left CoolDown and TrippedTopics unwritten, and an IDistributedCache cannot list what is tripped; it is now whole, on a Redis sorted set. AzureBlobArchiveProvider.md: the block is rewritten against 10.7.0 — AzureCliCredential, not AzCliCredential; the options' constructor and Uri; UseOutboxArchiver's TTransaction; ArchiveBatchSize; a TimeSpan MinimumAge; the option assignments through `options.`. The page gains its opening sentence, the two packages the provider does not bring in, and its options. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- contents/AzureBlobArchiveProvider.md | 85 ++++++++++++------ contents/UsingSweeperCircuitBreaking.md | 87 +++++++++++-------- .../UsingSweeperCircuitBreakingContext.cs | 3 + 3 files changed, 114 insertions(+), 61 deletions(-) diff --git a/contents/AzureBlobArchiveProvider.md b/contents/AzureBlobArchiveProvider.md index 628a16a..ab64f63 100644 --- a/contents/AzureBlobArchiveProvider.md +++ b/contents/AzureBlobArchiveProvider.md @@ -1,5 +1,5 @@ --- -description: "The Azure Blob Archive Provider is a provider for Outbox Archiver." +description: "The Azure Blob Archive Provider writes the messages the Outbox Archiver takes from your Outbox into an Azure Blob Storage container." layout: description: visible: false @@ -7,50 +7,83 @@ layout: # Azure Blob Archive Provider -> **Reference** · Applies to **Brighter V10** +> **Reference** · Applies to **Brighter V10** · Prerequisites: [Outbox Archiver](/contents/OutboxArchiver.md) -## Azure Blob Archive Provider Usage -The Azure Blob Archive Provider is a provider for [Outbox Archiver](/contents/OutboxArchiver.md). +The Azure Blob Archive Provider writes the messages the Outbox Archiver takes from your Outbox into an Azure Blob Storage container. It is an `IAmAnArchiveProvider` you pass to `UseOutboxArchiver`; the [Outbox Archiver](/contents/OutboxArchiver.md) decides when a message is old enough to archive, and removes it from the Outbox once the provider has written it. -For this we will need the *Archive* packages for the Azure *Archive Provider*. +## Azure Blob Archive Provider Configuration -* **Paramore.Brighter.Archive.Azure** +You need three packages: + +* **Paramore.Brighter.Archive.Azure** — the provider, in the `Paramore.Brighter.Storage.Azure` namespace +* **Paramore.Brighter.Outbox.Hosting** — `UseOutboxArchiver`, which runs the Archiver as a hosted service +* **Azure.Identity** — a `TokenCredential` for the provider to write with. The provider package does not bring it in ```csharp +using System; +using System.Data.Common; using Azure.Identity; +using Azure.Storage.Blobs.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Paramore.Brighter.Storage.Azure; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Outbox.Hosting; +using Paramore.Brighter.Storage.Azure; private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { - ConfigureBrighter(hostContext, services); + ConfigureBrighter(services); }); -private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) +private static void ConfigureBrighter(IServiceCollection services) { - services.AddBrighter(options => - { ... }) - .UseOutboxArchiver( - new AzureBlobArchiveProvider(new AzureBlobArchiveProviderOptions() - { - BlobContainerUri = "https://brighterarchivertest.blob.core.windows.net/messagearchive", - TokenCredential = New AzCliCredential(); - } - ), - options => { - TimerInterval = 5; // Every 5 seconds - BatchSize = 500; // 500 messages at a time - MinimumAge = 744; // 1 month - } - ); + services.AddBrighter() + .AddProducers(configure => + { + // ... your producer registry, and the Outbox the Archiver reads from + }) + // DbTransaction is the transaction type of a relational Outbox; see Outbox Archiver for the others + .UseOutboxArchiver( + new AzureBlobArchiveProvider(new AzureBlobArchiveProviderOptions( + blobContainerUri: new Uri("https://brighterarchivertest.blob.core.windows.net/messagearchive"), + tokenCredential: new AzureCliCredential(), + accessTier: AccessTier.Cool, + tagBlobs: true)), + options => + { + options.TimerInterval = 5; // every 5 seconds + options.ArchiveBatchSize = 500; // 500 messages at a time + options.MinimumAge = TimeSpan.FromDays(31); // dispatched more than a month ago + }); } +``` -... +`TTransaction` is your Outbox's transaction type, not its transaction provider; [Outbox Archiver](/contents/OutboxArchiver.md) lists the type for each Outbox, and the options the second argument sets. -``` +## Azure Blob Archive Provider Options + +`AzureBlobArchiveProviderOptions` takes its first four values as constructor arguments; the rest have defaults. Its properties are `init`-only, so set them, and the two functions, in an object initializer when you create the options. + +| Option | Type | Default | Description | +|---|---|---|---| +| `BlobContainerUri` | `Uri` | required | The container the provider writes to. The provider does not create it | +| `TokenCredential` | `TokenCredential` | required | The credential the provider writes with — `AzureCliCredential` locally, a managed identity or `DefaultAzureCredential` in Azure | +| `AccessTier` | `AccessTier` | required | The access tier each blob is written in, such as `Hot`, `Cool` or `Archive` | +| `TagBlobs` | `bool` | required | Whether to write index tags on each blob, from `TagsFunc` | +| `MaxConcurrentUploads` | `int` | `8` | The most transfers one upload runs in parallel | +| `MaxUploadSize` | `int` | `50` | The largest chunk one transfer sends, in megabytes | +| `TagsFunc` | `Func>` | the message's topic, correlation id, message type, timestamp and content type | The tags written when `TagBlobs` is `true` | +| `StorageLocationFunc` | `Func` | the message's Id | The name of the blob a message is written to, within the container | + +## What the Azure Blob Archive Provider Writes + +Each archived message becomes one blob, named by `StorageLocationFunc` — by default, the message's Id. The blob holds the message **body**; the header reaches the archive only through the tags, and only when `TagBlobs` is `true`. If you need more of the header, write it into the tags with your own `TagsFunc`. + +A message whose blob already exists is not written again, so archiving a message twice is harmless. + +## Further Reading +- [Outbox Archiver](/contents/OutboxArchiver.md) - When messages are archived, and the `TTransaction` for each Outbox +- [Outbox Support](/contents/BrighterOutboxSupport.md) - The Outbox, the Sweeper and the Archiver diff --git a/contents/UsingSweeperCircuitBreaking.md b/contents/UsingSweeperCircuitBreaking.md index 27fa284..94fe56c 100644 --- a/contents/UsingSweeperCircuitBreaking.md +++ b/contents/UsingSweeperCircuitBreaking.md @@ -54,7 +54,9 @@ public void ConfigureServices(IServiceCollection services) Adjust the cooldown based on your needs: ```csharp -// ... +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter.CircuitBreaker; + // Short cooldown for quickly recovering topics services.AddSingleton( new InMemoryOutboxCircuitBreaker(new OutboxCircuitBreakerOptions @@ -77,7 +79,10 @@ services.AddSingleton( If you don't register an `IAmAnOutboxCircuitBreaker`, the sweeper will continue to attempt publishing to all topics even after failures: ```csharp -// ... +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Outbox.Hosting; + // No circuit breaker registered - all topics always attempted services.AddBrighter(/* configuration */) .UseOutboxSweeper(); // Sweeper without circuit breaking @@ -87,23 +92,28 @@ services.AddBrighter(/* configuration */) ### Custom Circuit Breaker Implementation -Implement `IAmAnOutboxCircuitBreaker` for custom behavior: +Implement `IAmAnOutboxCircuitBreaker` for custom behavior. This one cools a topic down after a period of time rather than a number of sweeps, and counts how often each topic has tripped: ```csharp -// ... +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Paramore.Brighter; +using Paramore.Brighter.CircuitBreaker; + public class CustomOutboxCircuitBreaker : IAmAnOutboxCircuitBreaker { - private readonly Dictionary _topics = new(); + private static readonly TimeSpan CooldownPeriod = TimeSpan.FromMinutes(10); + + // A failed dispatch can trip a topic while a sweep is cooling topics down, so the map must be concurrent + private readonly ConcurrentDictionary _topics = new(); public void TripTopic(RoutingKey topic) { - _topics[topic] = new CircuitBreakerState - { - TrippedAt = DateTime.UtcNow, - FailureCount = _topics.ContainsKey(topic) - ? _topics[topic].FailureCount + 1 - : 1 - }; + _topics.AddOrUpdate( + topic, + _ => new CircuitBreakerState(DateTime.UtcNow, FailureCount: 1), + (_, state) => new CircuitBreakerState(DateTime.UtcNow, state.FailureCount + 1)); // Custom logic: Log, emit metrics, send alerts, etc. } @@ -111,51 +121,58 @@ public class CustomOutboxCircuitBreaker : IAmAnOutboxCircuitBreaker public void CoolDown() { var now = DateTime.UtcNow; - var recovered = new List(); - foreach (var kvp in _topics) + foreach (var (topic, state) in _topics) { - var cooldownPeriod = TimeSpan.FromMinutes(10); - if (now - kvp.Value.TrippedAt > cooldownPeriod) + // Remove only the state we read, so a topic tripped again meanwhile stays tripped + if (now - state.TrippedAt > CooldownPeriod + && _topics.TryRemove(new KeyValuePair(topic, state))) { - recovered.Add(kvp.Key); + // Custom logic: Log recovery, emit metrics, etc. } } - - foreach (var topic in recovered) - { - _topics.Remove(topic); - // Custom logic: Log recovery, emit metrics, etc. - } } public IEnumerable TrippedTopics => _topics.Keys; + + private sealed record CircuitBreakerState(DateTime TrippedAt, int FailureCount); } ``` ### Distributed Circuit Breaker -For multi-instance deployments, consider a distributed circuit breaker using Redis, SQL, or other shared storage: +For multi-instance deployments, keep the tripped topics in shared storage, so a topic one instance trips is skipped by all of them. The store has to be able to list what is tripped, because the sweeper asks for `TrippedTopics` — a plain key-value cache such as `IDistributedCache` cannot enumerate its keys, so it cannot answer. A Redis sorted set can: each member is a topic, and its score is the time the trip expires. ```csharp -// ... -public class DistributedOutboxCircuitBreaker : IAmAnOutboxCircuitBreaker +using System; +using System.Collections.Generic; +using System.Linq; +using Paramore.Brighter; +using Paramore.Brighter.CircuitBreaker; +using StackExchange.Redis; + +public class RedisOutboxCircuitBreaker(IConnectionMultiplexer redis, TimeSpan cooldown) : IAmAnOutboxCircuitBreaker { - private readonly IDistributedCache _cache; + private const string TrippedTopicsKey = "brighter:outbox:tripped-topics"; + private readonly IDatabase _database = redis.GetDatabase(); public void TripTopic(RoutingKey topic) - { - var key = $"circuit-breaker:{topic.Value}"; - _cache.SetString(key, DateTime.UtcNow.ToString(), new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) - }); - } + => _database.SortedSetAdd(TrippedTopicsKey, topic.Value, Now() + cooldown.TotalMilliseconds); + + // A trip expires by time, so cooling down only removes the trips that have expired + public void CoolDown() + => _database.SortedSetRemoveRangeByScore(TrippedTopicsKey, double.NegativeInfinity, Now()); + + public IEnumerable TrippedTopics + => _database.SortedSetRangeByScore(TrippedTopicsKey, Now(), double.PositiveInfinity) + .Select(topic => new RoutingKey(topic.ToString())); - // Implement other methods using distributed cache + private static double Now() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } ``` +Because a trip expires by time rather than by counting `CoolDown` calls, the breaker behaves the same however many instances call `CoolDown`. Register it as a singleton, as [the basic setup](#basic-setup-with-outbox-sweeper) registers the in-memory breaker. + ## Further Reading - [Sweeper Circuit Breaking](/contents/SweeperCircuitBreaking.md) - Configuration, monitoring and troubleshooting diff --git a/tools/blockcheck/scaffold/units/UsingSweeperCircuitBreakingContext.cs b/tools/blockcheck/scaffold/units/UsingSweeperCircuitBreakingContext.cs index 0be8762..0118776 100644 --- a/tools/blockcheck/scaffold/units/UsingSweeperCircuitBreakingContext.cs +++ b/tools/blockcheck/scaffold/units/UsingSweeperCircuitBreakingContext.cs @@ -6,10 +6,13 @@ // // blockcheck: using static UsingSweeperCircuitBreakingContext; +using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter; public static class UsingSweeperCircuitBreakingContext { public static RelationalDatabaseConfiguration outboxConfiguration => null!; public static IAmAProducerRegistry producerRegistry => null!; + // blocks 2 and 3: `services.AddSingleton(…)`, `services.AddBrighter()` — the composition root's collection, which block 1 takes as a parameter + public static IServiceCollection services => null!; } From d47bfdff0cf0f01466a3d56a35657fa6f549e596 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 21:00:20 +0100 Subject: [PATCH 11/21] blockcheck: baseline the five blocks 4.4 made build Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- tools/blockcheck/baseline.tsv | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/blockcheck/baseline.tsv b/tools/blockcheck/baseline.tsv index 133be28..617228d 100644 --- a/tools/blockcheck/baseline.tsv +++ b/tools/blockcheck/baseline.tsv @@ -251,3 +251,8 @@ contents/MySqlDistributedLock.md 1 DistributedLockProviderContext.cs b8f21ac contents/MySqlDistributedLock.md 2 DistributedLockProviderContext.cs b8f21ac contents/PostgresDistributedLock.md 1 DistributedLockProviderContext.cs b8f21ac contents/PostgresDistributedLock.md 2 DistributedLockProviderContext.cs b8f21ac +contents/AzureBlobArchiveProvider.md 1 - 2defac6 +contents/UsingSweeperCircuitBreaking.md 2 UsingSweeperCircuitBreakingContext.cs 2defac6 +contents/UsingSweeperCircuitBreaking.md 3 UsingSweeperCircuitBreakingContext.cs 2defac6 +contents/UsingSweeperCircuitBreaking.md 4 UsingSweeperCircuitBreakingContext.cs 2defac6 +contents/UsingSweeperCircuitBreaking.md 5 UsingSweeperCircuitBreakingContext.cs 2defac6 From cdeabca5f764ef9e35c474618634772dcbe3d12a Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 21:01:10 +0100 Subject: [PATCH 12/21] =?UTF-8?q?spec:=20017=20phase=204=20task=204.4=20?= =?UTF-8?q?=E2=80=94=20the=20remaining=20outbox=20pages=20whole;=2057=20wi?= =?UTF-8?q?th=20nothing=20built?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- spec/017-compile_repairs/tasks.md | 71 ++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/spec/017-compile_repairs/tasks.md b/spec/017-compile_repairs/tasks.md index b9abc39..28d58d5 100644 --- a/spec/017-compile_repairs/tasks.md +++ b/spec/017-compile_repairs/tasks.md @@ -1262,7 +1262,7 @@ skipped with an accepted reason, or listed. Page list: § *The tranches*, phase - Notes: the lock pages share a shape; a defect found on one is grepped for on all before the next is opened. -- [ ] **Task 4.4:** Repair the remaining *Outbox and Inbox* tranche pages +- [x] **Task 4.4:** Repair the remaining *Outbox and Inbox* tranche pages - Input: the phase 4 rows not covered by 4.2 or 4.3 (at § 2's pin: `UsingSweeperCircuitBreaking.md`, `AzureBlobArchiveProvider.md`, `ReplayOnSeenReference.md`); 4.1's verdicts - Output: each page whole; baseline rows; ledger rows as 4.2 @@ -1526,6 +1526,70 @@ and five of its six hard-only pages. `pagelint --changed origin/master` 0 errors. **Pages changed: 6** (`git diff --name-only 7d04918..HEAD -- contents`), all on the tranche +**Task 4.4 — the remaining outbox pages.** Three pages. **BUILT 226 → 231** (+5): +`AzureBlobArchiveProvider.md` #1 and `UsingSweeperCircuitBreaking.md` #2–#5. **One stays FAILED**, +`ReplayOnSeenReference.md` #1, by P2-2 (§ *Blocks that stay FAILED*). `pagelint` **624 → 620**, the +four `UsingSweeperCircuitBreaking.md` blocks that opened with `// ...`. Pages with nothing BUILT +**58 → 57**, `AzureBlobArchiveProvider.md`; `ReplayOnSeenReference.md` stays among them. + +- **Said at 4.1:** a ceiling of **230** BUILT. **Measured: 231.** The ceiling left out 4.2's two + off-tranche recurrence blocks (`BrighterBasicConfiguration.md` #3, #4) and counted + `TickerQScheduler.md` #2, which stays FAILED on `Program`: 230 + 2 − 1 = 231 +- **`UsingSweeperCircuitBreaking.md`.** #2, #3 take their `using`s, and the page's unit gains + `services`, which block 1 takes as a parameter. #4, read with its `using`s, named a + `CircuitBreakerState` no package or block declares, and shared a `Dictionary` between `TripTopic` + and `CoolDown`; it now declares the state as a nested record and uses a `ConcurrentDictionary` + with a conditional remove. #5 was the fragment 4.1 read, and **made whole** (design: the reader + needs the whole — `TrippedTopics` is the part a distributed breaker has to get right). Its + `IDistributedCache` cannot enumerate keys, so a whole version on it could not answer + `TrippedTopics`; it is now a Redis sorted set, scored by expiry. A sixth block registering it was + written and removed, a new *same-page* FAILED block for a registration #1 already shows; a + sentence links #1 instead +- **`AzureBlobArchiveProvider.md`.** **Said at 4.1:** five defects behind the placeholders. + **Measured, compiled with the placeholders filled** (the control below): the six 4.1's row names, + and two more — `AzCliCredential` is no type in Azure.Identity (`AzureCliCredential`), and + `BlobContainerUri` is a `Uri`. The block is rewritten against 10.7.0. The page gains an opening + sentence after its banner, with the `description:` rule 7 checks against it, its Prerequisites, the two + packages the provider does not bring in (`dotnet list package --include-transitive` on + `Paramore.Brighter.Archive.Azure` 10.7.0 alone: neither `Azure.Identity` nor + `Paramore.Brighter.Outbox.Hosting`), and a table of `AzureBlobArchiveProviderOptions`, read from + `AzureBlobArchiveProviderOptions.cs`. **It is not `optioncheck`-marked:** marked, the tool reported + `CANNOT CONSTRUCT` (it cannot synthesise the `AccessTier` constructor argument) and `ROW NAMES + NOTHING` for `TagsFunc` and `StorageLocationFunc`, which are fields — a marker would check nothing +- **Behaviour, run with controls** against released 10.7.0 packages, net10.0, one process per case; + Redis in Docker (`redis:7`): + + | Claim | Case → result | Control → result | + |---|---|---| + | #1: *"default cooldown of 10 sweeps"*; #2: *"Recover after 3 sweeps"*, *"30 sweeps"* — each sweep calls `CoolDown` and then reads `TrippedTopics` (`OutboxProducerMediator.cs:721`, `:735`) | `CooldownCount = 3` → skipped **3** sweeps, retried on the 4th | default → **10**; `30` → **30** | + | #4 needs a concurrent map | the old block (its `CircuitBreakerState` written as it uses it), `TripTopic` and `CoolDown` on two threads for 3 s → **`InvalidOperationException`**, *"Collection was modified"*, twice in two runs | the new block → **none**, twice | + | #5 shares trips across instances and expires them | two breakers on two connections, cooldown 2 s: A trips → A and B both list `orders`; +1 s, B cools down → both still list it | +2.5 s, B cools down → both empty, **0** members left | + | `AzureBlobArchiveProvider.md` #1 configures the Archiver it says | the block in a host, built → `TimerInterval 5`, `ArchiveBatchSize 500`, `MinimumAge 31.00:00:00`, provider `AzureBlobArchiveProvider` | the old block, placeholders filled → `CS1003`; with `New …;` also mended, `CS0029`, `CS0103` ×3, `CS0246`, `CS7036` | + + Read, not run: #3's *"all topics always attempted"* (`_outboxCircuitBreaker?.TrippedTopics` is + `null` with none registered, `OutboxProducerMediator.cs:735`); the Azure provider's writes — one + blob per message named by its Id, the body only, an existing blob not rewritten, the container + never created (`AzureBlobArchiveProvider.cs`), which need Azurite and a token credential it accepts +- **Found off the tranche:** `SweeperCircuitBreaking.md` #2 and #5 configure the sweeper through + `options.OutboxSweeper = new OutboxSweeperOptions { SweepInterval = … }` — no such property or + type at 10.7.0; `UseOutboxSweeper` takes a `TimedOutboxSweeperOptions`, whose interval is + `TimerInterval`, an `int` of seconds. Line 94's formula names `SweepInterval` too + (`grep -rn 'SweepInterval' contents/` → **3**, that page only). The page is in no tranche; + recorded for the maintainer, not repaired +- **Recurrence greps, all 0 beyond the repaired lines:** `AzCliCredential`, `BlobContainerUri *= *"`, + `new AzureBlobArchiveProviderOptions()`, `MinimumAge *= *[0-9]`, `BatchSize` within eight lines of + `UseOutboxArchiver`, an unshown `CircuitBreakerState`, `IDistributedCache` in code; the page's two + breakers are the only `: IAmAnOutboxCircuitBreaker` in `contents/` +- **`attr_mismatch.py` → 7**, before the baseline rows +- **Baseline:** 5 rows at `2defac6`. `--report` → exit **0**, *"983 blocks: 231 BUILT, 736 FAILED, + 16 SKIPPED"*, baseline 231, 35 units, 0 violations, 0 findings. Joined on page and ordinal + against the report at `05fdeaf`, the 5 blocks that moved went `FAILED -> BUILT`, 983 keys both + sides +- `linkcheck` 165 files, 0 broken; `versioncheck` 0 stale of 18 across 5; `symbolcheck` 0 findings; + `optioncheck` 0 mismatches across 59 tables, 519 rows; no `SUMMARY.md` change, so shape, redirects + and `--verify` unmoved; `pagelint --changed origin/master` 0 errors. **Pages changed: 2** + (`git diff --name-only 05fdeaf..HEAD -- contents`), both on the tranche + --- ## Phase 5 — Tranche 2b and the recorded falsehoods *(6 tasks, one PR, CHANGES THE SITE)* @@ -1883,6 +1947,7 @@ is rewritten against the tables below. | `PaginationQueryPatterns.md` | 3 | `CS0246` `OrderDto` | same-page: block 1 declares it | 3 | | `PaginationQueryPatterns.md` | 4 | `CS0246` `GetOrdersCursorQuery`, `CursorPagedResult<>`, `OrderDto`; `ApplicationDbContext` | same-page: block 3 declares the first two, block 1 `OrderDto`; block 4 is *"Handler with cursor pagination:"*, straight after block 3 | 3 | | `QueryPipelinePolicies.md` | 1 | `CS0246` `Program` | instrument: a `Program.cs` with no declaration after its statements takes the `statements` wrapper, which declares no `Program`; Q2 (1.8) rules out a stub. Builds as a `Program.cs` in scratch against Darker 4.1.1, **0** errors | 3 | +| `ReplayOnSeenReference.md` | 1 | `CS0117` `RequestContextBagNames.CausationId`; `CS0246` `ProcessPayment`; `CS0103` `_commandProcessor`, `batchId`, `orderId` | P2-2: `CausationId` is on Brighter `master` (`RequestContextBagNames.cs:143`), in no release. The pin bump brings it in through the ratchet; the other names are the handler's, and want a unit then | 4 | ## Splits @@ -1954,6 +2019,10 @@ BUILT, re-admitted at `ec38400`. | The InMemory Inbox said to keep every entry until restart (*"No cleanup"*, *"All seen message IDs held in memory"*). An entry expires `EntryTimeToLive` (5 min) after it is written, removed by a scan at most every `ExpirationScanInterval` (10 min); past `EntryLimit` (2048) adding compacts the oldest to half | `InMemoryBox.cs:64–100`, `InMemoryInbox.cs:316`; run, controls both ways | `InMemoryInbox.md` | `grep -rnE 'No cleanup\|All seen message IDs held in memory' contents/` | **2** | **0** | 4.2, reading the page against the source, then running | | A global `actionOnExists: Warn` shown beside a `[UseInboxAsync]` that sets no `onceOnlyAction` — the attribute's default `Throw` wins, so a duplicate throws `OnceOnlyException` | `PipelineBuilder.cs:371`, `HasExistingUseInboxAttributesInPipeline`; run, control the attribute with `Warn` | `InMemoryInbox.md` #1, #2 | pages with `actionOnExists: OnceOnlyAction.Warn\|Replay` and a `[UseInbox…]` without `onceOnlyAction` on its line: 2, read — `TurningOnReplayOnSeen.md`'s attributes set it on the next line and the page states the precedence | **1** | **0** | 4.2, running #1 with #2 | | A global `InboxConfiguration` in `AddConsumers` reaches the pipeline only through `ExternalBus(…)`, so an application that never calls `AddProducers` gets no global Inbox, and duplicates run again | `ServiceCollectionExtensions.cs:640–660`; run, control with `AddProducers` — **upstream, BrighterCommand/Brighter#4335**, fixed by #4396 on `master`, unreleased | `BrighterInboxSupport.md` states it with the workaround; linked from `MSSQLInbox.md`, `MySQLInbox.md`, `PostgresInbox.md`, `SqliteInbox.md`, `DynamoInbox.md`, `MongoDBInbox.md`, `FirestoreInbox.md`, `SpannerInbox.md`, `InMemoryInbox.md`. Not linked: the seven other pages that configure one | `git grep -l 'InboxConfiguration' d8633b1 -- contents` | **16** pages | **stated** on 1, linked from 9 — maintainer's ruling | 4.2, running `InMemoryInbox.md` #1 without #2's attribute | +| `CircuitBreakerState` — named in a custom `IAmAnOutboxCircuitBreaker` and declared by no package or block (`CS0246`) | `git grep CircuitBreakerState 10.7.0 -- src` → 0 | `UsingSweeperCircuitBreaking.md` #4 | an unshown `CircuitBreakerState` in `contents/` | **1** | **0** | 4.4, `--classify` | +| A custom breaker's `Dictionary` enumerated by `CoolDown` while `TripTopic` writes it — `InvalidOperationException` | run, control the concurrent form; 10.7.0's own breaker is concurrent for this (`InMemoryOutboxCircuitBreaker.cs`) | `UsingSweeperCircuitBreaking.md` #4 | `grep -rn ': IAmAnOutboxCircuitBreaker' contents/` → the page's two, both concurrent | **1** | **0** | 4.4, reading #4 once it built | +| A distributed breaker on `IDistributedCache`, with `CoolDown` and `TrippedTopics` left unwritten — the cache cannot enumerate keys, so `TrippedTopics` cannot be written on it | `IDistributedCache` has `Get`, `Set`, `Refresh`, `Remove` and their async forms only; the Redis form run across two connections | `UsingSweeperCircuitBreaking.md` #5 | `grep -rn 'IDistributedCache' contents/` → 1, the prose saying why | **1** | **0** | 4.4, making the fragment whole | +| The Azure archive block, against 10.7.0: `New AzCliCredential();` in an initialiser — no such type (`AzureCliCredential`); `AzureBlobArchiveProviderOptions` built parameterless, its `init` properties assigned, `BlobContainerUri` a string (`CS7036`, `CS0029`); `UseOutboxArchiver` without `TTransaction`; `BatchSize` for `ArchiveBatchSize`; `MinimumAge = 744` for a `TimeSpan`; option assignments with no `options.` (`CS0103`) | `AzureBlobArchiveProviderOptions.cs`, `HostedServiceCollectionExtensions.cs:52`, `TimedOutboxArchiverOptions.cs`; compiled, control the old block | `AzureBlobArchiveProvider.md` #1 | the seven greps in § *Phase 4 as executed*, 4.4's entry | **1** block, 8 defects | **0** | 4.1 (six), 4.4 (two, compiling the control) | ## Friction ledger From 28b2d5fa422fb12de43fde1ef1c8f9b863039097 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 21:51:17 +0100 Subject: [PATCH 13/21] docs: configure the sweeper's interval as 10.7.0 does, and count the cooldown right MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SweeperCircuitBreaking.md set the sweep interval through options.OutboxSweeper = new OutboxSweeperOptions { SweepInterval = … }, which does not exist: UseOutboxSweeper takes a TimedOutboxSweeperOptions, whose TimerInterval is an int of seconds. Blocks 2 and 7 now configure it there, and build. The cooldown formula was also one sweep short. A tripped topic sits out CooldownCount sweeps and is retried on the next, so the time until retry is (CooldownCount + 1) × TimerInterval — run against a real sweeper with a failing producer. The step list and UsingSweeperCircuitBreaking.md's comments now say so. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- contents/SweeperCircuitBreaking.md | 65 +++++++++++++++---------- contents/UsingSweeperCircuitBreaking.md | 4 +- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/contents/SweeperCircuitBreaking.md b/contents/SweeperCircuitBreaking.md index 0299ebc..1ef96e5 100644 --- a/contents/SweeperCircuitBreaking.md +++ b/contents/SweeperCircuitBreaking.md @@ -38,9 +38,9 @@ When a topic fails to publish: 1. **Failure detected**: An exception occurs during message publication to a specific topic 2. **Circuit trips**: The circuit breaker marks that topic as "tripped" 3. **Cooldown begins**: A cooldown counter is set for the tripped topic (default: 10 sweeps) -4. **Subsequent sweeps**: On each sweep, the cooldown counter decrements for all tripped topics -5. **Recovery**: When the cooldown reaches zero, the topic is removed from the tripped list -6. **Retry**: The topic becomes available for publishing attempts again +4. **Subsequent sweeps**: Each sweep decrements the counter for every tripped topic before it reads the Outbox, and skips the tripped topics' messages +5. **Recovery**: The sweep after the counter reaches zero removes the topic from the tripped list, so a topic sits out `CooldownCount` sweeps +6. **Retry**: That same sweep publishes the topic's messages again ### Benefits @@ -89,32 +89,37 @@ The `OutboxCircuitBreakerOptions` class provides the following configuration: ### Calculating Cooldown Time -The actual cooldown time depends on your Outbox Sweeper configuration: +The actual cooldown time depends on how often the Outbox Sweeper runs, which you set with `TimerInterval`, in seconds, on the options you pass to `UseOutboxSweeper`. A topic trips during one sweep, sits out the next `CooldownCount` sweeps, and is retried on the one after: -**Formula**: `Cooldown Time = CooldownCount × SweepInterval` +**Formula**: `Time until retry = (CooldownCount + 1) × TimerInterval` **Example**: - `CooldownCount = 10` -- Sweeper runs every 60 seconds -- **Cooldown Time = 10 × 60s = 10 minutes** +- `TimerInterval = 60`, so the Sweeper runs every 60 seconds +- **Time until retry = (10 + 1) × 60s = 11 minutes** ```csharp -services.AddBrighter(options => -{ - // Sweeper configuration - options.OutboxSweeper = new OutboxSweeperOptions +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter.CircuitBreaker; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Outbox.Hosting; + +services.AddBrighter() + .AddProducers(configure => { - SweepInterval = TimeSpan.FromSeconds(60) // Sweep every 60 seconds - }; -}) -.UseOutboxSweeper(); + // ... your producer registry and Outbox + }) + .UseOutboxSweeper(options => + { + options.TimerInterval = 60; // Sweep every 60 seconds + }); -// Circuit breaker with 10 cooldown sweeps = 10 minutes total cooldown +// A tripped topic sits out 10 sweeps and is retried on the 11th: (10 + 1) × 60s = 11 minutes services.AddSingleton( new InMemoryOutboxCircuitBreaker(new OutboxCircuitBreakerOptions { - CooldownCount = 10 // 10 sweeps × 60s = 10 minutes + CooldownCount = 10 }) ); ``` @@ -264,19 +269,29 @@ Balance between quick recovery and avoiding repeated failures: ### 2. Align Cooldown with Sweep Interval -Consider the total cooldown time: +Consider the time until a tripped topic is retried, `(CooldownCount + 1) × TimerInterval`: ```csharp -// Fast sweeping with short cooldown = quick recovery -options.OutboxSweeper = new OutboxSweeperOptions -{ - SweepInterval = TimeSpan.FromSeconds(30) // 30s sweep -}; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter.CircuitBreaker; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Outbox.Hosting; + +// Fast sweeping with a short cooldown = quick recovery +services.AddBrighter() + .AddProducers(configure => + { + // ... your producer registry and Outbox + }) + .UseOutboxSweeper(options => + { + options.TimerInterval = 30; // Sweep every 30 seconds + }); services.AddSingleton( new InMemoryOutboxCircuitBreaker(new OutboxCircuitBreakerOptions { - CooldownCount = 5 // 5 × 30s = 2.5 minutes total cooldown + CooldownCount = 5 // (5 + 1) × 30s = 3 minutes until retry }) ); ``` @@ -354,7 +369,7 @@ Regularly test circuit breaker behavior: 1. Verify Outbox Sweeper is running 2. Check cooldown count is not excessively high -3. Ensure sweeper interval is appropriate +3. Ensure the Sweeper's `TimerInterval` is appropriate 4. Confirm circuit breaker is properly registered ### All Topics Tripping diff --git a/contents/UsingSweeperCircuitBreaking.md b/contents/UsingSweeperCircuitBreaking.md index 94fe56c..3e45913 100644 --- a/contents/UsingSweeperCircuitBreaking.md +++ b/contents/UsingSweeperCircuitBreaking.md @@ -61,7 +61,7 @@ using Paramore.Brighter.CircuitBreaker; services.AddSingleton( new InMemoryOutboxCircuitBreaker(new OutboxCircuitBreakerOptions { - CooldownCount = 3 // Recover after 3 sweeps + CooldownCount = 3 // Sit out 3 sweeps, retry on the 4th }) ); @@ -69,7 +69,7 @@ services.AddSingleton( services.AddSingleton( new InMemoryOutboxCircuitBreaker(new OutboxCircuitBreakerOptions { - CooldownCount = 30 // Recover after 30 sweeps + CooldownCount = 30 // Sit out 30 sweeps, retry on the 31st }) ); ``` From 6263986c4687791139b7156cc970438f22220936 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 21:51:25 +0100 Subject: [PATCH 14/21] blockcheck: baseline the two sweeper-interval blocks Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- tools/blockcheck/baseline.tsv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/blockcheck/baseline.tsv b/tools/blockcheck/baseline.tsv index 617228d..32623b4 100644 --- a/tools/blockcheck/baseline.tsv +++ b/tools/blockcheck/baseline.tsv @@ -256,3 +256,5 @@ contents/UsingSweeperCircuitBreaking.md 2 UsingSweeperCircuitBreakingContext.cs contents/UsingSweeperCircuitBreaking.md 3 UsingSweeperCircuitBreakingContext.cs 2defac6 contents/UsingSweeperCircuitBreaking.md 4 UsingSweeperCircuitBreakingContext.cs 2defac6 contents/UsingSweeperCircuitBreaking.md 5 UsingSweeperCircuitBreakingContext.cs 2defac6 +contents/SweeperCircuitBreaking.md 2 SweeperCircuitBreakingContext.cs 28b2d5f +contents/SweeperCircuitBreaking.md 7 SweeperCircuitBreakingContext.cs 28b2d5f From e7cec73e566dcc31f39714752f57fc766ce31066 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 21:52:11 +0100 Subject: [PATCH 15/21] =?UTF-8?q?spec:=20017=20phase=204=20task=204.4=20?= =?UTF-8?q?=E2=80=94=20SweeperCircuitBreaking.md=20repaired=20by=20ruling;?= =?UTF-8?q?=20233=20built?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- spec/017-compile_repairs/tasks.md | 60 +++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/spec/017-compile_repairs/tasks.md b/spec/017-compile_repairs/tasks.md index 28d58d5..91bf372 100644 --- a/spec/017-compile_repairs/tasks.md +++ b/spec/017-compile_repairs/tasks.md @@ -1526,15 +1526,18 @@ and five of its six hard-only pages. `pagelint --changed origin/master` 0 errors. **Pages changed: 6** (`git diff --name-only 7d04918..HEAD -- contents`), all on the tranche -**Task 4.4 — the remaining outbox pages.** Three pages. **BUILT 226 → 231** (+5): -`AzureBlobArchiveProvider.md` #1 and `UsingSweeperCircuitBreaking.md` #2–#5. **One stays FAILED**, -`ReplayOnSeenReference.md` #1, by P2-2 (§ *Blocks that stay FAILED*). `pagelint` **624 → 620**, the -four `UsingSweeperCircuitBreaking.md` blocks that opened with `// ...`. Pages with nothing BUILT +**Task 4.4 — the remaining outbox pages.** Three pages, and one off the tranche by ruling. **BUILT +226 → 233** (+7): `AzureBlobArchiveProvider.md` #1, `UsingSweeperCircuitBreaking.md` #2–#5 and +`SweeperCircuitBreaking.md` #2, #7. **One stays FAILED**, +`ReplayOnSeenReference.md` #1, by P2-2 (§ *Blocks that stay FAILED*). `pagelint` **624 → 618**, the +four `UsingSweeperCircuitBreaking.md` blocks that opened with `// ...` and the two +`SweeperCircuitBreaking.md` blocks. Pages with nothing BUILT **58 → 57**, `AzureBlobArchiveProvider.md`; `ReplayOnSeenReference.md` stays among them. -- **Said at 4.1:** a ceiling of **230** BUILT. **Measured: 231.** The ceiling left out 4.2's two - off-tranche recurrence blocks (`BrighterBasicConfiguration.md` #3, #4) and counted - `TickerQScheduler.md` #2, which stays FAILED on `Program`: 230 + 2 − 1 = 231 +- **Said at 4.1:** a ceiling of **230** BUILT. **Measured: 233.** The ceiling left out 4.2's two + off-tranche recurrence blocks (`BrighterBasicConfiguration.md` #3, #4) and the two + `SweeperCircuitBreaking.md` blocks repaired by ruling, and counted `TickerQScheduler.md` #2, which + stays FAILED on `Program`: 230 + 2 + 2 − 1 = 233 - **`UsingSweeperCircuitBreaking.md`.** #2, #3 take their `using`s, and the page's unit gains `services`, which block 1 takes as a parameter. #4, read with its `using`s, named a `CircuitBreakerState` no package or block declares, and shared a `Dictionary` between `TripTopic` @@ -1570,25 +1573,42 @@ four `UsingSweeperCircuitBreaking.md` blocks that opened with `// ...`. Pages wi `null` with none registered, `OutboxProducerMediator.cs:735`); the Azure provider's writes — one blob per message named by its Id, the body only, an existing blob not rewritten, the container never created (`AzureBlobArchiveProvider.cs`), which need Azurite and a token credential it accepts -- **Found off the tranche:** `SweeperCircuitBreaking.md` #2 and #5 configure the sweeper through - `options.OutboxSweeper = new OutboxSweeperOptions { SweepInterval = … }` — no such property or - type at 10.7.0; `UseOutboxSweeper` takes a `TimedOutboxSweeperOptions`, whose interval is - `TimerInterval`, an `int` of seconds. Line 94's formula names `SweepInterval` too - (`grep -rn 'SweepInterval' contents/` → **3**, that page only). The page is in no tranche; - recorded for the maintainer, not repaired +- **Off the tranche, repaired — maintainer's ruling, 2026-09-27: *"fix it in this PR"*.** + `SweeperCircuitBreaking.md` #2 and #7 configured the sweeper through `options.OutboxSweeper = new + OutboxSweeperOptions { SweepInterval = … }` — no such property or type at 10.7.0 (`git grep` → 0); + `UseOutboxSweeper` takes a `TimedOutboxSweeperOptions`, whose interval is `TimerInterval`, an `int` + of seconds (`TimedOutboxSweeper.cs`, a `Timer` of that period). Both blocks now configure it there, + with their `using`s, and **build** (`FAILED -> BUILT`, baselined at `28b2d5f`). **Said in the first + draft of this entry:** #2 and #5. **Measured:** #2 and #7; #5 is the MongoDB block +- **The same repair found the formula one sweep short.** The page said `Cooldown Time = CooldownCount × + SweepInterval` and, in its steps, that a topic recovers *"when the cooldown reaches zero"*. + `CoolDown` runs first in each sweep (the sweeper is its only caller, `OutboxSweeper.cs:79`) and + removes a topic when its count goes **below** zero, so a topic sits out `CooldownCount` sweeps and is + retried on the next: `(CooldownCount + 1) × TimerInterval`. Rewritten there, in the page's steps, + and in `UsingSweeperCircuitBreaking.md` #2's two comments (*"Recover after 3 sweeps"*). **Run** + end to end, released 10.7.0, net10.0: a real `UseOutboxSweeper` host, `TimerInterval = 1`, an + InMemory Outbox and a producer that always throws. `CooldownCount = 2` → sends every **3 s**; `3` → + every **4 s**. Controls: no breaker → every **1 s**; `CooldownCount = 0` → every **1 s**. Each sweep + made 4 send attempts, Brighter's own send retry +- **Found and not repaired, put to the maintainer:** the same page's #5 calls + `.UseMongoDbOutbox(…)` (`git grep UseMongoDbOutbox 10.7.0 -- src` → 0; `grep -rn` over `contents/` + → that line only), under *"Circuit breaking is fully integrated with MongoDB Outbox"*; and its + § 6 says immediate clearing is *"NOT subject to circuit breaking"* while § *Bulk Dispatch Support* + says `ClearOutboxAsync` *"respects circuit breaker state"*. Unverified which is right - **Recurrence greps, all 0 beyond the repaired lines:** `AzCliCredential`, `BlobContainerUri *= *"`, `new AzureBlobArchiveProviderOptions()`, `MinimumAge *= *[0-9]`, `BatchSize` within eight lines of `UseOutboxArchiver`, an unshown `CircuitBreakerState`, `IDistributedCache` in code; the page's two breakers are the only `: IAmAnOutboxCircuitBreaker` in `contents/` - **`attr_mismatch.py` → 7**, before the baseline rows -- **Baseline:** 5 rows at `2defac6`. `--report` → exit **0**, *"983 blocks: 231 BUILT, 736 FAILED, - 16 SKIPPED"*, baseline 231, 35 units, 0 violations, 0 findings. Joined on page and ordinal - against the report at `05fdeaf`, the 5 blocks that moved went `FAILED -> BUILT`, 983 keys both - sides +- **Baseline:** 5 rows at `2defac6`, 2 at `28b2d5f`. `--report` → exit **0**, *"983 blocks: 233 + BUILT, 734 FAILED, 16 SKIPPED"*, baseline 233, 35 units, 0 violations, 0 findings. Joined on page + and ordinal against the report at `05fdeaf`, the 7 blocks that moved went `FAILED -> BUILT`, 983 + keys both sides - `linkcheck` 165 files, 0 broken; `versioncheck` 0 stale of 18 across 5; `symbolcheck` 0 findings; `optioncheck` 0 mismatches across 59 tables, 519 rows; no `SUMMARY.md` change, so shape, redirects - and `--verify` unmoved; `pagelint --changed origin/master` 0 errors. **Pages changed: 2** - (`git diff --name-only 05fdeaf..HEAD -- contents`), both on the tranche + and `--verify` unmoved; `pagelint --changed origin/master` 0 errors. **Pages changed: 3** + (`git diff --name-only 05fdeaf..HEAD -- contents`): the two tranche pages and + `SweeperCircuitBreaking.md` --- @@ -2023,6 +2043,8 @@ BUILT, re-admitted at `ec38400`. | A custom breaker's `Dictionary` enumerated by `CoolDown` while `TripTopic` writes it — `InvalidOperationException` | run, control the concurrent form; 10.7.0's own breaker is concurrent for this (`InMemoryOutboxCircuitBreaker.cs`) | `UsingSweeperCircuitBreaking.md` #4 | `grep -rn ': IAmAnOutboxCircuitBreaker' contents/` → the page's two, both concurrent | **1** | **0** | 4.4, reading #4 once it built | | A distributed breaker on `IDistributedCache`, with `CoolDown` and `TrippedTopics` left unwritten — the cache cannot enumerate keys, so `TrippedTopics` cannot be written on it | `IDistributedCache` has `Get`, `Set`, `Refresh`, `Remove` and their async forms only; the Redis form run across two connections | `UsingSweeperCircuitBreaking.md` #5 | `grep -rn 'IDistributedCache' contents/` → 1, the prose saying why | **1** | **0** | 4.4, making the fragment whole | | The Azure archive block, against 10.7.0: `New AzCliCredential();` in an initialiser — no such type (`AzureCliCredential`); `AzureBlobArchiveProviderOptions` built parameterless, its `init` properties assigned, `BlobContainerUri` a string (`CS7036`, `CS0029`); `UseOutboxArchiver` without `TTransaction`; `BatchSize` for `ArchiveBatchSize`; `MinimumAge = 744` for a `TimeSpan`; option assignments with no `options.` (`CS0103`) | `AzureBlobArchiveProviderOptions.cs`, `HostedServiceCollectionExtensions.cs:52`, `TimedOutboxArchiverOptions.cs`; compiled, control the old block | `AzureBlobArchiveProvider.md` #1 | the seven greps in § *Phase 4 as executed*, 4.4's entry | **1** block, 8 defects | **0** | 4.1 (six), 4.4 (two, compiling the control) | +| The sweep interval set through `options.OutboxSweeper = new OutboxSweeperOptions { SweepInterval = … }` — no such type or property; it is `UseOutboxSweeper(o => o.TimerInterval = …)`, an `int` of seconds | `TimedOutboxSweeperOptions.cs`, `HostedServiceCollectionExtensions.cs:41`; compiled | `SweeperCircuitBreaking.md` #2, #7 and its formula | `grep -rnE 'SweepInterval\|OutboxSweeperOptions\b' contents/` (`Timed` excluded) | **3** lines | **0** | 4.4, reading `UsingSweeperCircuitBreaking.md`'s sibling; maintainer's ruling | +| Cooldown time given as `CooldownCount × interval`, recovery *"when the cooldown reaches zero"* — a topic sits out `CooldownCount` sweeps and is retried on the next, `(CooldownCount + 1) × TimerInterval` | `InMemoryOutboxCircuitBreaker.cs` (removes below zero), `OutboxProducerMediator.cs:721`; run end to end, controls no breaker and `0` | `SweeperCircuitBreaking.md` (formula, example, #2, #7 comments, steps), `UsingSweeperCircuitBreaking.md` #2 | `grep -rnE '(^\|[^+] )[0-9]+ (sweeps )?× [0-9]+s\|total cooldown\|[Rr]ecover after [0-9]\|When the cooldown reaches zero' contents/`, at `05fdeaf` and after | **8** | **0** | 4.4, running the sweeper for the row above | ## Friction ledger From a61893b50a2a6d514163b232497c2dc95ca8fff4 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 22:32:52 +0100 Subject: [PATCH 16/21] docs: say which Outboxes honour a tripped topic, and what explicit clearing does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SweeperCircuitBreaking.md said circuit breaking "works with all Brighter Outbox implementations", beside a UseMongoDbOutbox that does not exist. The sweeper passes TrippedTopics to the Outbox, and at 10.7.0 the DynamoDB Outbox (V3 and V4) ignores it and Spanner's query drops it — run against DynamoDB Local and the Spanner emulator, with SQLite and MongoDB as controls. The section is now a table of which Outboxes honour it, and the MongoDB block registers the Outbox as MongoDBOutbox.md does. The page also said both that explicit clearing is "NOT subject to circuit breaking" and that ClearOutboxAsync "respects circuit breaker state". Run: an explicit clear sends a tripped topic's messages, and a failed ClearOutboxAsync trips the topic where a failed ClearOutbox does not. The bulk section now shows UseBulk on the sweeper, which honours trips. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- contents/SweeperCircuitBreaking.md | 92 +++++++++++-------- .../units/SweeperCircuitBreakingContext.cs | 2 - 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/contents/SweeperCircuitBreaking.md b/contents/SweeperCircuitBreaking.md index 1ef96e5..e7744ea 100644 --- a/contents/SweeperCircuitBreaking.md +++ b/contents/SweeperCircuitBreaking.md @@ -27,7 +27,7 @@ The Sweeper Circuit Breaker operates at the topic level during Outbox clearing o ### Normal Operation 1. **Outbox Sweeper runs**: Periodically attempts to clear outstanding messages from the Outbox -2. **Messages grouped by topic**: Messages are organized by their routing key (topic) +2. **Tripped topics left out**: The sweeper asks the Outbox for outstanding messages, passing it the tripped topics to leave out 3. **Publish attempts**: The sweeper attempts to publish messages to their respective topics 4. **Success**: Messages are published and marked as dispatched @@ -209,52 +209,67 @@ services.AddHealthChecks() .AddCheck("outbox_circuit_breaker"); ``` -## Transport-Specific Integration +## Sweeper Circuit Breaking Outbox Support -Brighter V10 includes circuit breaking integration with specific transports: +The sweeper does not filter tripped topics itself. It passes `TrippedTopics` to the Outbox when it asks for outstanding messages, and the Outbox leaves those topics out of its query — so circuit breaking works only where the Outbox honours that list. At Brighter 10.7.0: -### MongoDB Transport +| Outbox | Leaves tripped topics out | +|---|---| +| MS SQL Server, MySQL, PostgreSQL, SQLite | Yes | +| MongoDB | Yes | +| Firestore | Yes | +| InMemory | Yes | +| DynamoDB, both the V3 and V4 packages | **No** — it accepts the list and ignores it | +| Spanner | **No** — its query has no place for the filter, so the filter is dropped | -Circuit breaking is fully integrated with MongoDB Outbox: +With DynamoDB or Spanner, a registered breaker still records trips, and `TrippedTopics` still reports them, so the monitoring above works. But every sweep reads and sends a tripped topic's messages as though nothing had tripped. -```csharp -services.AddBrighter(/* configuration */) - .UseMongoDbOutbox(/* MongoDB configuration */) - .UseOutboxSweeper(); +An Outbox that honours the list needs nothing extra: register the breaker and the sweeper beside it as usual. With MongoDB, for example: -// Circuit breaker works automatically with MongoDB transport -services.AddSingleton( - new InMemoryOutboxCircuitBreaker() -); -``` +```csharp +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter.CircuitBreaker; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.MongoDb; +using Paramore.Brighter.Outbox.Hosting; +using Paramore.Brighter.Outbox.MongoDb; -### Other Transports +var mongoDbConfiguration = new MongoDbConfiguration("mongodb://localhost:27017", "BrighterDatabase") +{ + Outbox = new MongoDbCollectionConfiguration { Name = "Outbox" } +}; -Circuit breaking works with all Brighter Outbox implementations. Set the one you want as `Outbox` on the options passed to `AddProducers()`: +services.AddSingleton(new InMemoryOutboxCircuitBreaker()); -- **MS SQL Server** (`MsSqlOutbox`) -- **PostgreSQL** (`PostgreSqlOutbox`) -- **MySQL** (`MySqlOutbox`) -- **SQLite** (`SqliteOutbox`) -- **DynamoDB** (`DynamoDbOutbox`) -- **MongoDB** (`MongoDbOutbox`) +services.AddBrighter() + .AddProducers(configure => + { + // ... your producer registry + configure.Outbox = new MongoDbOutbox(mongoDbConfiguration); + configure.ConnectionProvider = typeof(MongoDbConnectionProvider); + configure.TransactionProvider = typeof(MongoDbUnitOfWork); + }) + .UseOutboxSweeper(); +``` ## Bulk Dispatch Support -V10 includes proper circuit breaking support for bulk dispatch operations. When dispatching multiple messages in a batch: - -1. **Batch grouping**: Messages are grouped by topic -2. **Per-topic circuit breaking**: Each topic's circuit breaker status is checked before dispatching -3. **Healthy topics proceed**: Only topics that aren't tripped are dispatched -4. **Individual retry**: Failed batches can be retried individually per topic +With `UseBulk` set on its options, the sweeper sends each topic's outstanding messages in batches, through a producer that implements `IAmABulkMessageProducerAsync`. Circuit breaking works as it does for single messages: tripped topics are left out when the sweeper reads the Outbox, and a batch that fails to send trips its topic. ```csharp -// Bulk dispatch respects circuit breaker state -await commandProcessor.ClearOutboxAsync( - messageIds, // List of message IDs to dispatch - continueOnCapturedContext: false, - cancellationToken: cancellationToken -); +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Outbox.Hosting; + +services.AddBrighter() + .AddProducers(configure => + { + // ... your producer registry and Outbox + }) + .UseOutboxSweeper(options => + { + options.UseBulk = true; // needs a producer that implements IAmABulkMessageProducerAsync + }); ``` ## Sweeper Circuit Breaking Best Practices @@ -339,14 +354,16 @@ services.AddBrighter() ### 6. Consider Immediate vs. Sweeper Clearing -Circuit breaking only applies to **sweeper-based clearing**: +Only the sweeper skips tripped topics. When you clear explicitly with `ClearOutbox` or `ClearOutboxAsync`, Brighter sends every message you name, whether or not its topic is tripped. + +A failed send from `ClearOutboxAsync` does trip the topic, so the sweeper then skips it. A failed send from `ClearOutbox` trips it only when the producer reports failures through publish confirmation. ```csharp // ... -// Immediate clearing - NOT subject to circuit breaking +// Explicit clearing - sends every message named, tripped topic or not await commandProcessor.ClearOutboxAsync(messageIds); -// Sweeper clearing - subject to circuit breaking +// Sweeper clearing - skips tripped topics // Happens automatically via UseOutboxSweeper ``` @@ -418,6 +435,7 @@ Regularly test circuit breaker behavior: 2. Using the sweeper: `UseOutboxSweeper()` 3. Exceptions are being thrown during publish (not silently failing) 4. Circuit breaker implementation is correct +5. Your Outbox leaves tripped topics out — DynamoDB and Spanner do not (see [Outbox Support](#sweeper-circuit-breaking-outbox-support)) ## Sweeper Circuit Breaking Summary diff --git a/tools/blockcheck/scaffold/units/SweeperCircuitBreakingContext.cs b/tools/blockcheck/scaffold/units/SweeperCircuitBreakingContext.cs index 7ed86f6..5650936 100644 --- a/tools/blockcheck/scaffold/units/SweeperCircuitBreakingContext.cs +++ b/tools/blockcheck/scaffold/units/SweeperCircuitBreakingContext.cs @@ -7,7 +7,6 @@ // blockcheck: using static SweeperCircuitBreakingContext; using System.Collections.Generic; -using System.Threading; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter; @@ -15,7 +14,6 @@ public static class SweeperCircuitBreakingContext { public static IAmACommandProcessor commandProcessor => null!; public static IEnumerable messageIds => null!; - public static CancellationToken cancellationToken => default; public static IServiceCollection services => null!; public static RelationalDatabaseConfiguration outboxConfiguration => null!; } From 2a247873b6b4ae91dde2e9d79d21dd482cd6af74 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 22:33:09 +0100 Subject: [PATCH 17/21] blockcheck: baseline SweeperCircuitBreaking.md #5, and re-admit the page's rows with its unit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- tools/blockcheck/baseline.tsv | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tools/blockcheck/baseline.tsv b/tools/blockcheck/baseline.tsv index 32623b4..8016b14 100644 --- a/tools/blockcheck/baseline.tsv +++ b/tools/blockcheck/baseline.tsv @@ -198,9 +198,9 @@ contents/SpannerInbox.md 1 - 280d1b7 contents/SpannerInbox.md 2 - 280d1b7 contents/SpannerOutbox.md 1 - 280d1b7 contents/SpannerOutbox.md 2 - 280d1b7 -contents/SweeperCircuitBreaking.md 6 SweeperCircuitBreakingContext.cs 280d1b7 -contents/SweeperCircuitBreaking.md 8 SweeperCircuitBreakingContext.cs 280d1b7 -contents/SweeperCircuitBreaking.md 9 SweeperCircuitBreakingContext.cs 9c57ae2 +contents/SweeperCircuitBreaking.md 6 SweeperCircuitBreakingContext.cs a61893b +contents/SweeperCircuitBreaking.md 8 SweeperCircuitBreakingContext.cs a61893b +contents/SweeperCircuitBreaking.md 9 SweeperCircuitBreakingContext.cs a61893b contents/Telemetry.md 1 - 9c57ae2 contents/Telemetry.md 4 - 9c57ae2 contents/TestingQueryHandlers.md 1 TestingQueryHandlersContext.cs 3462412 @@ -256,5 +256,6 @@ contents/UsingSweeperCircuitBreaking.md 2 UsingSweeperCircuitBreakingContext.cs contents/UsingSweeperCircuitBreaking.md 3 UsingSweeperCircuitBreakingContext.cs 2defac6 contents/UsingSweeperCircuitBreaking.md 4 UsingSweeperCircuitBreakingContext.cs 2defac6 contents/UsingSweeperCircuitBreaking.md 5 UsingSweeperCircuitBreakingContext.cs 2defac6 -contents/SweeperCircuitBreaking.md 2 SweeperCircuitBreakingContext.cs 28b2d5f -contents/SweeperCircuitBreaking.md 7 SweeperCircuitBreakingContext.cs 28b2d5f +contents/SweeperCircuitBreaking.md 2 SweeperCircuitBreakingContext.cs a61893b +contents/SweeperCircuitBreaking.md 7 SweeperCircuitBreakingContext.cs a61893b +contents/SweeperCircuitBreaking.md 5 SweeperCircuitBreakingContext.cs a61893b From 9e129ddff7ed86ef6a6b0d47603ff0696c796bd6 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 22:33:40 +0100 Subject: [PATCH 18/21] =?UTF-8?q?spec:=20017=20phase=204=20task=204.4=20?= =?UTF-8?q?=E2=80=94=20SweeperCircuitBreaking.md's=20outbox=20and=20cleari?= =?UTF-8?q?ng=20claims;=20234=20built?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- spec/017-compile_repairs/tasks.md | 63 +++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/spec/017-compile_repairs/tasks.md b/spec/017-compile_repairs/tasks.md index 91bf372..d54a1bd 100644 --- a/spec/017-compile_repairs/tasks.md +++ b/spec/017-compile_repairs/tasks.md @@ -1527,17 +1527,17 @@ and five of its six hard-only pages. 7d04918..HEAD -- contents`), all on the tranche **Task 4.4 — the remaining outbox pages.** Three pages, and one off the tranche by ruling. **BUILT -226 → 233** (+7): `AzureBlobArchiveProvider.md` #1, `UsingSweeperCircuitBreaking.md` #2–#5 and -`SweeperCircuitBreaking.md` #2, #7. **One stays FAILED**, -`ReplayOnSeenReference.md` #1, by P2-2 (§ *Blocks that stay FAILED*). `pagelint` **624 → 618**, the -four `UsingSweeperCircuitBreaking.md` blocks that opened with `// ...` and the two -`SweeperCircuitBreaking.md` blocks. Pages with nothing BUILT +226 → 234** (+8): `AzureBlobArchiveProvider.md` #1, `UsingSweeperCircuitBreaking.md` #2–#5 and +`SweeperCircuitBreaking.md` #2, #5, #7. **One stays FAILED**, +`ReplayOnSeenReference.md` #1, by P2-2 (§ *Blocks that stay FAILED*). `pagelint` **624 → 616**, the +four `UsingSweeperCircuitBreaking.md` blocks that opened with `// ...` and four +`SweeperCircuitBreaking.md` blocks (#2, #5, #6, #7). Pages with nothing BUILT **58 → 57**, `AzureBlobArchiveProvider.md`; `ReplayOnSeenReference.md` stays among them. -- **Said at 4.1:** a ceiling of **230** BUILT. **Measured: 233.** The ceiling left out 4.2's two - off-tranche recurrence blocks (`BrighterBasicConfiguration.md` #3, #4) and the two +- **Said at 4.1:** a ceiling of **230** BUILT. **Measured: 234.** The ceiling left out 4.2's two + off-tranche recurrence blocks (`BrighterBasicConfiguration.md` #3, #4) and the three `SweeperCircuitBreaking.md` blocks repaired by ruling, and counted `TickerQScheduler.md` #2, which - stays FAILED on `Program`: 230 + 2 + 2 − 1 = 233 + stays FAILED on `Program`: 230 + 2 + 3 − 1 = 234 - **`UsingSweeperCircuitBreaking.md`.** #2, #3 take their `using`s, and the page's unit gains `services`, which block 1 takes as a parameter. #4, read with its `using`s, named a `CircuitBreakerState` no package or block declares, and shared a `Dictionary` between `TripTopic` @@ -1590,19 +1590,48 @@ four `UsingSweeperCircuitBreaking.md` blocks that opened with `// ...` and the t InMemory Outbox and a producer that always throws. `CooldownCount = 2` → sends every **3 s**; `3` → every **4 s**. Controls: no breaker → every **1 s**; `CooldownCount = 0` → every **1 s**. Each sweep made 4 send attempts, Brighter's own send retry -- **Found and not repaired, put to the maintainer:** the same page's #5 calls - `.UseMongoDbOutbox(…)` (`git grep UseMongoDbOutbox 10.7.0 -- src` → 0; `grep -rn` over `contents/` - → that line only), under *"Circuit breaking is fully integrated with MongoDB Outbox"*; and its - § 6 says immediate clearing is *"NOT subject to circuit breaking"* while § *Bulk Dispatch Support* - says `ClearOutboxAsync` *"respects circuit breaker state"*. Unverified which is right +- **Then the page's two other falsehoods — maintainer's ruling, 2026-09-27: *"put them in this + PR"*.** Read against 10.7.0 and run: + - **Which Outboxes honour a trip.** #5 called `.UseMongoDbOutbox(…)` (`git grep` → 0) under + *"fully integrated with MongoDB Outbox"*, and the page said breaking *"works with all Brighter + Outbox implementations"*. The sweeper passes `TrippedTopics` to `OutstandingMessagesAsync` + (`OutboxProducerMediator.cs:735`) and each Outbox filters, or does not. **Run** against released + 10.7.0 packages, net10.0, two messages (`orders`, `payments`), `OutstandingMessagesAsync` with + `orders` tripped and, as each store's own control, with none: **SQLite** → `[payments]`; + **MongoDB** (`mongo:7`) → `[payments]`; **DynamoDB** (`amazon/dynamodb-local`) → `[orders,payments]`; + **Spanner** (the emulator) → `[orders,payments]`; every control → both. Read: DynamoDB V3 and V4 + take the parameter and never use it (`DynamoDbOutbox.cs:582`); Spanner's `PagedOutstandingCommand` + has no `{1}` for the `NOT IN` clause `RelationDatabaseOutbox` formats into it + (`SpannerQueries.cs:12`); MSSQL, MySQL and PostgreSQL carry the `{1}` and share SQLite's code; + Firestore and InMemory filter (`FirestoreOutbox.cs:901`, `InMemoryOutbox.cs:566`, and the + InMemory sweeper runs above). The section is now *Sweeper Circuit Breaking Outbox Support*, a + table of that, and #5 registers a MongoDB Outbox as `MongoDBOutbox.md` does — **it builds**. + **Upstream, not yet filed:** DynamoDB and Spanner ignoring `trippedTopics` is a Brighter defect + - **Explicit clearing.** § 6 said explicit clearing is *"NOT subject to circuit breaking"*; § *Bulk + Dispatch Support* said `ClearOutboxAsync` *"respects circuit breaker state"*, and that *"failed + batches can be retried individually per topic"*. **Run**, an InMemory Outbox, a producer that + always throws: a topic tripped beforehand → `ClearOutbox` and `ClearOutboxAsync` each still make + **4** send attempts; a fresh breaker → `ClearOutboxAsync` leaves `orders` **tripped**, + `ClearOutbox` leaves **none** (`DispatchAsync` trips on `!sent`, `Dispatch` only through a + publish-confirmation callback, `:984`). Neither section was right. § 6 now says both halves; § + *Bulk Dispatch Support* shows the sweeper's `UseBulk`, and says what it does — **run**: a bulk + sweeper, `TimerInterval = 1`, a batch producer that throws, `CooldownCount = 2` → batches every + **3 s**, 20 of 20 attempts through `SendAsync(IAmAMessageBatch)`; control, no breaker → every + **1 s**. The *"retried individually"* claim has nothing behind it in the source and is gone + - The page's step list said messages are *"grouped by topic"* on every sweep; only bulk groups. + Step 2 now says the tripped topics are passed to the Outbox. A troubleshooting item names the + two Outboxes that ignore them + - #6 no longer names `cancellationToken`, so the unit loses it (the unit rule's violation, read + from `--report`), and the page's five baselined rows are re-admitted at `a61893b` with #5 - **Recurrence greps, all 0 beyond the repaired lines:** `AzCliCredential`, `BlobContainerUri *= *"`, `new AzureBlobArchiveProviderOptions()`, `MinimumAge *= *[0-9]`, `BatchSize` within eight lines of `UseOutboxArchiver`, an unshown `CircuitBreakerState`, `IDistributedCache` in code; the page's two breakers are the only `: IAmAnOutboxCircuitBreaker` in `contents/` - **`attr_mismatch.py` → 7**, before the baseline rows -- **Baseline:** 5 rows at `2defac6`, 2 at `28b2d5f`. `--report` → exit **0**, *"983 blocks: 233 - BUILT, 734 FAILED, 16 SKIPPED"*, baseline 233, 35 units, 0 violations, 0 findings. Joined on page - and ordinal against the report at `05fdeaf`, the 7 blocks that moved went `FAILED -> BUILT`, 983 +- **Baseline:** 5 rows at `2defac6`; `SweeperCircuitBreaking.md` #2, #7 at `28b2d5f`, then #5 and + the page's other four rows re-admitted at `a61893b`. `--report` → exit **0**, *"983 blocks: 234 + BUILT, 733 FAILED, 16 SKIPPED"*, baseline 234, 35 units, 0 violations, 0 findings. Joined on page + and ordinal against the report at `05fdeaf`, the 8 blocks that moved went `FAILED -> BUILT`, 983 keys both sides - `linkcheck` 165 files, 0 broken; `versioncheck` 0 stale of 18 across 5; `symbolcheck` 0 findings; `optioncheck` 0 mismatches across 59 tables, 519 rows; no `SUMMARY.md` change, so shape, redirects @@ -2045,6 +2074,8 @@ BUILT, re-admitted at `ec38400`. | The Azure archive block, against 10.7.0: `New AzCliCredential();` in an initialiser — no such type (`AzureCliCredential`); `AzureBlobArchiveProviderOptions` built parameterless, its `init` properties assigned, `BlobContainerUri` a string (`CS7036`, `CS0029`); `UseOutboxArchiver` without `TTransaction`; `BatchSize` for `ArchiveBatchSize`; `MinimumAge = 744` for a `TimeSpan`; option assignments with no `options.` (`CS0103`) | `AzureBlobArchiveProviderOptions.cs`, `HostedServiceCollectionExtensions.cs:52`, `TimedOutboxArchiverOptions.cs`; compiled, control the old block | `AzureBlobArchiveProvider.md` #1 | the seven greps in § *Phase 4 as executed*, 4.4's entry | **1** block, 8 defects | **0** | 4.1 (six), 4.4 (two, compiling the control) | | The sweep interval set through `options.OutboxSweeper = new OutboxSweeperOptions { SweepInterval = … }` — no such type or property; it is `UseOutboxSweeper(o => o.TimerInterval = …)`, an `int` of seconds | `TimedOutboxSweeperOptions.cs`, `HostedServiceCollectionExtensions.cs:41`; compiled | `SweeperCircuitBreaking.md` #2, #7 and its formula | `grep -rnE 'SweepInterval\|OutboxSweeperOptions\b' contents/` (`Timed` excluded) | **3** lines | **0** | 4.4, reading `UsingSweeperCircuitBreaking.md`'s sibling; maintainer's ruling | | Cooldown time given as `CooldownCount × interval`, recovery *"when the cooldown reaches zero"* — a topic sits out `CooldownCount` sweeps and is retried on the next, `(CooldownCount + 1) × TimerInterval` | `InMemoryOutboxCircuitBreaker.cs` (removes below zero), `OutboxProducerMediator.cs:721`; run end to end, controls no breaker and `0` | `SweeperCircuitBreaking.md` (formula, example, #2, #7 comments, steps), `UsingSweeperCircuitBreaking.md` #2 | `grep -rnE '(^\|[^+] )[0-9]+ (sweeps )?× [0-9]+s\|total cooldown\|[Rr]ecover after [0-9]\|When the cooldown reaches zero' contents/`, at `05fdeaf` and after | **8** | **0** | 4.4, running the sweeper for the row above | +| Circuit breaking said to work with every Outbox, and `.UseMongoDbOutbox(…)` — no such method. The DynamoDB (V3, V4) and Spanner Outboxes ignore `trippedTopics`, so a tripped topic is swept as normal | `DynamoDbOutbox.cs:582`, `SpannerQueries.cs:12`; run against DynamoDB Local and the Spanner emulator, controls SQLite and MongoDB — **upstream, not yet filed** | `SweeperCircuitBreaking.md` (section, #5, troubleshooting) | the next row's grep | **3** | **0** — the table states it | 4.4, maintainer's ruling | +| Explicit clearing said both to ignore the breaker and to respect it, and failed batches to be *"retried individually per topic"*. An explicit clear sends a tripped topic's messages; a failed `ClearOutboxAsync` trips the topic, a failed `ClearOutbox` does not unless the producer confirms publication | `OutboxProducerMediator.cs:425`, `:1220`, `:984`; run, sync and async, pre-tripped and fresh | `SweeperCircuitBreaking.md` § 6, § *Bulk Dispatch Support* | `grep -rnE 'UseMongoDbOutbox\|fully integrated with MongoDB\|works automatically with MongoDB\|works with all Brighter Outbox\|NOT subject to circuit breaking\|respects circuit breaker state\|retried individually per topic' contents/`, at `05fdeaf` and after — this row and the one above | **4** | **0** | 4.4, maintainer's ruling | ## Friction ledger From be7793c2cb09d534b07c2c4c40d487df0940c62f Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 27 Sep 2026 22:33:58 +0100 Subject: [PATCH 19/21] =?UTF-8?q?spec:=20017=20task=204.4=20=E2=80=94=20th?= =?UTF-8?q?e=20two=20ledger=20rows'=20before-counts,=20measured=20apart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- spec/017-compile_repairs/tasks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/017-compile_repairs/tasks.md b/spec/017-compile_repairs/tasks.md index d54a1bd..ba8a518 100644 --- a/spec/017-compile_repairs/tasks.md +++ b/spec/017-compile_repairs/tasks.md @@ -2074,8 +2074,8 @@ BUILT, re-admitted at `ec38400`. | The Azure archive block, against 10.7.0: `New AzCliCredential();` in an initialiser — no such type (`AzureCliCredential`); `AzureBlobArchiveProviderOptions` built parameterless, its `init` properties assigned, `BlobContainerUri` a string (`CS7036`, `CS0029`); `UseOutboxArchiver` without `TTransaction`; `BatchSize` for `ArchiveBatchSize`; `MinimumAge = 744` for a `TimeSpan`; option assignments with no `options.` (`CS0103`) | `AzureBlobArchiveProviderOptions.cs`, `HostedServiceCollectionExtensions.cs:52`, `TimedOutboxArchiverOptions.cs`; compiled, control the old block | `AzureBlobArchiveProvider.md` #1 | the seven greps in § *Phase 4 as executed*, 4.4's entry | **1** block, 8 defects | **0** | 4.1 (six), 4.4 (two, compiling the control) | | The sweep interval set through `options.OutboxSweeper = new OutboxSweeperOptions { SweepInterval = … }` — no such type or property; it is `UseOutboxSweeper(o => o.TimerInterval = …)`, an `int` of seconds | `TimedOutboxSweeperOptions.cs`, `HostedServiceCollectionExtensions.cs:41`; compiled | `SweeperCircuitBreaking.md` #2, #7 and its formula | `grep -rnE 'SweepInterval\|OutboxSweeperOptions\b' contents/` (`Timed` excluded) | **3** lines | **0** | 4.4, reading `UsingSweeperCircuitBreaking.md`'s sibling; maintainer's ruling | | Cooldown time given as `CooldownCount × interval`, recovery *"when the cooldown reaches zero"* — a topic sits out `CooldownCount` sweeps and is retried on the next, `(CooldownCount + 1) × TimerInterval` | `InMemoryOutboxCircuitBreaker.cs` (removes below zero), `OutboxProducerMediator.cs:721`; run end to end, controls no breaker and `0` | `SweeperCircuitBreaking.md` (formula, example, #2, #7 comments, steps), `UsingSweeperCircuitBreaking.md` #2 | `grep -rnE '(^\|[^+] )[0-9]+ (sweeps )?× [0-9]+s\|total cooldown\|[Rr]ecover after [0-9]\|When the cooldown reaches zero' contents/`, at `05fdeaf` and after | **8** | **0** | 4.4, running the sweeper for the row above | -| Circuit breaking said to work with every Outbox, and `.UseMongoDbOutbox(…)` — no such method. The DynamoDB (V3, V4) and Spanner Outboxes ignore `trippedTopics`, so a tripped topic is swept as normal | `DynamoDbOutbox.cs:582`, `SpannerQueries.cs:12`; run against DynamoDB Local and the Spanner emulator, controls SQLite and MongoDB — **upstream, not yet filed** | `SweeperCircuitBreaking.md` (section, #5, troubleshooting) | the next row's grep | **3** | **0** — the table states it | 4.4, maintainer's ruling | -| Explicit clearing said both to ignore the breaker and to respect it, and failed batches to be *"retried individually per topic"*. An explicit clear sends a tripped topic's messages; a failed `ClearOutboxAsync` trips the topic, a failed `ClearOutbox` does not unless the producer confirms publication | `OutboxProducerMediator.cs:425`, `:1220`, `:984`; run, sync and async, pre-tripped and fresh | `SweeperCircuitBreaking.md` § 6, § *Bulk Dispatch Support* | `grep -rnE 'UseMongoDbOutbox\|fully integrated with MongoDB\|works automatically with MongoDB\|works with all Brighter Outbox\|NOT subject to circuit breaking\|respects circuit breaker state\|retried individually per topic' contents/`, at `05fdeaf` and after — this row and the one above | **4** | **0** | 4.4, maintainer's ruling | +| Circuit breaking said to work with every Outbox, and `.UseMongoDbOutbox(…)` — no such method. The DynamoDB (V3, V4) and Spanner Outboxes ignore `trippedTopics`, so a tripped topic is swept as normal | `DynamoDbOutbox.cs:582`, `SpannerQueries.cs:12`; run against DynamoDB Local and the Spanner emulator, controls SQLite and MongoDB — **upstream, not yet filed** | `SweeperCircuitBreaking.md` (section, #5, troubleshooting) | the next row's grep, its first four alternatives | **4** | **0** — the table states it | 4.4, maintainer's ruling | +| Explicit clearing said both to ignore the breaker and to respect it, and failed batches to be *"retried individually per topic"*. An explicit clear sends a tripped topic's messages; a failed `ClearOutboxAsync` trips the topic, a failed `ClearOutbox` does not unless the producer confirms publication | `OutboxProducerMediator.cs:425`, `:1220`, `:984`; run, sync and async, pre-tripped and fresh | `SweeperCircuitBreaking.md` § 6, § *Bulk Dispatch Support* | `grep -rnE 'UseMongoDbOutbox\|fully integrated with MongoDB\|works automatically with MongoDB\|works with all Brighter Outbox\|NOT subject to circuit breaking\|respects circuit breaker state\|retried individually per topic' contents/`, at `05fdeaf` and after; its last three alternatives are this row's | **3** | **0** | 4.4, maintainer's ruling | ## Friction ledger From ca6a0b2943ebbfff79b44010f31c69e928b7573d Mon Sep 17 00:00:00 2001 From: iancooper Date: Mon, 28 Sep 2026 00:13:45 +0100 Subject: [PATCH 20/21] docs: link the DynamoDB and Spanner tripped-topic issues, #4443 and #4444 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- contents/SweeperCircuitBreaking.md | 4 ++-- spec/017-compile_repairs/tasks.md | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/contents/SweeperCircuitBreaking.md b/contents/SweeperCircuitBreaking.md index e7744ea..02eff67 100644 --- a/contents/SweeperCircuitBreaking.md +++ b/contents/SweeperCircuitBreaking.md @@ -219,8 +219,8 @@ The sweeper does not filter tripped topics itself. It passes `TrippedTopics` to | MongoDB | Yes | | Firestore | Yes | | InMemory | Yes | -| DynamoDB, both the V3 and V4 packages | **No** — it accepts the list and ignores it | -| Spanner | **No** — its query has no place for the filter, so the filter is dropped | +| DynamoDB, both the V3 and V4 packages | **No** — it accepts the list and ignores it ([#4443](https://github.com/BrighterCommand/Brighter/issues/4443)) | +| Spanner | **No** — its query has no place for the filter, so the filter is dropped ([#4444](https://github.com/BrighterCommand/Brighter/issues/4444)) | With DynamoDB or Spanner, a registered breaker still records trips, and `TrippedTopics` still reports them, so the monitoring above works. But every sweep reads and sends a tripped topic's messages as though nothing had tripped. diff --git a/spec/017-compile_repairs/tasks.md b/spec/017-compile_repairs/tasks.md index ba8a518..71434cf 100644 --- a/spec/017-compile_repairs/tasks.md +++ b/spec/017-compile_repairs/tasks.md @@ -1606,7 +1606,9 @@ four `UsingSweeperCircuitBreaking.md` blocks that opened with `// ...` and four Firestore and InMemory filter (`FirestoreOutbox.cs:901`, `InMemoryOutbox.cs:566`, and the InMemory sweeper runs above). The section is now *Sweeper Circuit Breaking Outbox Support*, a table of that, and #5 registers a MongoDB Outbox as `MongoDBOutbox.md` does — **it builds**. - **Upstream, not yet filed:** DynamoDB and Spanner ignoring `trippedTopics` is a Brighter defect + **Upstream, filed 2026-09-28 by the maintainer's ruling, `Bug` and `0 - Backlog`:** + BrighterCommand/Brighter#4443 (DynamoDB) and #4444 (Spanner), both still so on `master` + `bb10b8fae`; the page's table links each - **Explicit clearing.** § 6 said explicit clearing is *"NOT subject to circuit breaking"*; § *Bulk Dispatch Support* said `ClearOutboxAsync` *"respects circuit breaker state"*, and that *"failed batches can be retried individually per topic"*. **Run**, an InMemory Outbox, a producer that @@ -2074,7 +2076,7 @@ BUILT, re-admitted at `ec38400`. | The Azure archive block, against 10.7.0: `New AzCliCredential();` in an initialiser — no such type (`AzureCliCredential`); `AzureBlobArchiveProviderOptions` built parameterless, its `init` properties assigned, `BlobContainerUri` a string (`CS7036`, `CS0029`); `UseOutboxArchiver` without `TTransaction`; `BatchSize` for `ArchiveBatchSize`; `MinimumAge = 744` for a `TimeSpan`; option assignments with no `options.` (`CS0103`) | `AzureBlobArchiveProviderOptions.cs`, `HostedServiceCollectionExtensions.cs:52`, `TimedOutboxArchiverOptions.cs`; compiled, control the old block | `AzureBlobArchiveProvider.md` #1 | the seven greps in § *Phase 4 as executed*, 4.4's entry | **1** block, 8 defects | **0** | 4.1 (six), 4.4 (two, compiling the control) | | The sweep interval set through `options.OutboxSweeper = new OutboxSweeperOptions { SweepInterval = … }` — no such type or property; it is `UseOutboxSweeper(o => o.TimerInterval = …)`, an `int` of seconds | `TimedOutboxSweeperOptions.cs`, `HostedServiceCollectionExtensions.cs:41`; compiled | `SweeperCircuitBreaking.md` #2, #7 and its formula | `grep -rnE 'SweepInterval\|OutboxSweeperOptions\b' contents/` (`Timed` excluded) | **3** lines | **0** | 4.4, reading `UsingSweeperCircuitBreaking.md`'s sibling; maintainer's ruling | | Cooldown time given as `CooldownCount × interval`, recovery *"when the cooldown reaches zero"* — a topic sits out `CooldownCount` sweeps and is retried on the next, `(CooldownCount + 1) × TimerInterval` | `InMemoryOutboxCircuitBreaker.cs` (removes below zero), `OutboxProducerMediator.cs:721`; run end to end, controls no breaker and `0` | `SweeperCircuitBreaking.md` (formula, example, #2, #7 comments, steps), `UsingSweeperCircuitBreaking.md` #2 | `grep -rnE '(^\|[^+] )[0-9]+ (sweeps )?× [0-9]+s\|total cooldown\|[Rr]ecover after [0-9]\|When the cooldown reaches zero' contents/`, at `05fdeaf` and after | **8** | **0** | 4.4, running the sweeper for the row above | -| Circuit breaking said to work with every Outbox, and `.UseMongoDbOutbox(…)` — no such method. The DynamoDB (V3, V4) and Spanner Outboxes ignore `trippedTopics`, so a tripped topic is swept as normal | `DynamoDbOutbox.cs:582`, `SpannerQueries.cs:12`; run against DynamoDB Local and the Spanner emulator, controls SQLite and MongoDB — **upstream, not yet filed** | `SweeperCircuitBreaking.md` (section, #5, troubleshooting) | the next row's grep, its first four alternatives | **4** | **0** — the table states it | 4.4, maintainer's ruling | +| Circuit breaking said to work with every Outbox, and `.UseMongoDbOutbox(…)` — no such method. The DynamoDB (V3, V4) and Spanner Outboxes ignore `trippedTopics`, so a tripped topic is swept as normal | `DynamoDbOutbox.cs:582`, `SpannerQueries.cs:12`; run against DynamoDB Local and the Spanner emulator, controls SQLite and MongoDB — **upstream, BrighterCommand/Brighter#4443, #4444**, filed 4.4 | `SweeperCircuitBreaking.md` (section, #5, troubleshooting) | the next row's grep, its first four alternatives | **4** | **0** — the table states it | 4.4, maintainer's ruling | | Explicit clearing said both to ignore the breaker and to respect it, and failed batches to be *"retried individually per topic"*. An explicit clear sends a tripped topic's messages; a failed `ClearOutboxAsync` trips the topic, a failed `ClearOutbox` does not unless the producer confirms publication | `OutboxProducerMediator.cs:425`, `:1220`, `:984`; run, sync and async, pre-tripped and fresh | `SweeperCircuitBreaking.md` § 6, § *Bulk Dispatch Support* | `grep -rnE 'UseMongoDbOutbox\|fully integrated with MongoDB\|works automatically with MongoDB\|works with all Brighter Outbox\|NOT subject to circuit breaking\|respects circuit breaker state\|retried individually per topic' contents/`, at `05fdeaf` and after; its last three alternatives are this row's | **3** | **0** | 4.4, maintainer's ruling | ## Friction ledger From b3fcc64d10a96ac099dbd6c7d03370023b0cac55 Mon Sep 17 00:00:00 2001 From: iancooper Date: Mon, 28 Sep 2026 02:09:53 +0100 Subject: [PATCH 21/21] =?UTF-8?q?spec:=20017=20phase=204=20task=204.5=20?= =?UTF-8?q?=E2=80=94=20phase=204=20closed;=20rows=202=20and=209=20moved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Am29wrQUS81NVh8dUyNuy --- spec/017-compile_repairs/tasks.md | 66 ++++++++++++++++++++++++++++++- tools/README.md | 11 +++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/spec/017-compile_repairs/tasks.md b/spec/017-compile_repairs/tasks.md index 71434cf..13695a5 100644 --- a/spec/017-compile_repairs/tasks.md +++ b/spec/017-compile_repairs/tasks.md @@ -1269,7 +1269,7 @@ skipped with an accepted reason, or listed. Page list: § *The tranches*, phase - Notes: `ReplayOnSeenReference.md#1` uses API not in 10.7.0 (requirements P2-2); it stays FAILED and is listed with that reason, not repaired toward `master`. -- [ ] **Task 4.5:** Close phase 4 — checks, figures, PR +- [x] **Task 4.5:** Close phase 4 — checks, figures, PR - Input: 4.1's prediction; the PR's diff - Output: as 2.6, for phase 4 @@ -1641,6 +1641,70 @@ four `UsingSweeperCircuitBreaking.md` blocks that opened with `// ...` and four (`git diff --name-only 05fdeaf..HEAD -- contents`): the two tranche pages and `SweeperCircuitBreaking.md` + +**Task 4.5 — phase 4 closed, 2026-09-28, branch at `ca6a0b2`.** Every gate run bare, exit code +read before its output: + +| # | Gate | Exit | Read | Predicted (4.1) | Agrees? | +|---:|---|---:|---|---|---| +| 1 | `linkcheck` | 0 | 165 files, 0 broken | none | **yes** | +| 2 | `pagelint` | 0 | 0 errors, **616** warnings across 76 pages, 162 pages | 0 errors; 658 → between 640 and 622, recurrences explained | **no — 6 lower**, explained below | +| 3 | shape | 0 | 161 pages, 12 sections, widest 12 of 20, deepest 4 of 4 | none | **yes** | +| 4 | redirects | 0 | 77 entries, 7858 bytes | none | **yes** | +| 5 | `versioncheck` | 0 | 0 stale of 18, across 5 pages | none, scope held at 18 across 5 | **yes** | +| 6 | `optioncheck` | 0 | 0 mismatches, 59 tables, 519 rows | none | **yes** | +| 7 | `--verify` | 0 | 161 predicted = 161 published | none | **yes** | +| 8 | `symbolcheck` | 0 | 0 findings, 22 entries, 161 pages, 3 silenced | none | **yes** | +| 9 | `blockcheck` | 0 | 983: **234** BUILT, 733 FAILED, 16 SKIPPED; 0 findings; **542** reference assemblies; baseline 234; **35** units, 0 violations; 53 pages mapped | BUILT 204–230; SKIPPED 16; exit 0; 0 violations | **no — 4 above the ceiling**, explained below | +| — | `attr_mismatch.py` | 1 | **7** | held at 7 | **yes** | + +`pagelint --changed origin/master` → exit **0**, 0 errors. + +**Gate 9, reconciled.** Phase 4 moved **45** blocks: 25 in 4.2, 12 in 4.3, 8 in 4.4. Against 4.1's +ceiling of 230: `BrighterBasicConfiguration.md` #3, #4 (+2, the unopened-lambda recurrence) and +`SweeperCircuitBreaking.md` #2, #5, #7 (+3, the maintainer's ruling) lie off the tranche, and +`TickerQScheduler.md` #2, counted in the ceiling, stays FAILED on `Program` (−1): 230 + 2 + 3 − 1 = +**234**. The pin carries **98** `PackageReference`s (`grep -c`), 95 at phase 3's close. + +**Every FAILED block on the 22 tranche pages is listed**: `after.tsv`'s FAILED keys on those pages +are **1**, `ReplayOnSeenReference.md` #1, and § *Blocks that stay FAILED* holds it (P2-2). + +**Gate 2, reconciled.** Per-page warnings at `d8633b1` (a worktree) against the branch: the 22 +tranche pages fell by **36**, which is 4.1's floor of 622 exactly — every reachable and every hard +block given its `using`s. The further **−6** are on two pages outside the tranche: +`SweeperCircuitBreaking.md` −4 (the ruling) and `BrighterBasicConfiguration.md` −2 (the +recurrence). `DispatcherConfigurationReference.md`, also touched by the recurrence, moved nothing: +its blocks open with `// ...`. 658 − 36 − 6 = **616**, across 76 pages, down from 96. + +**AC2, against a `before.tsv` regenerated from `c7329bb` in a worktree** (exit 0, *"989 blocks: 101 +BUILT, 872 FAILED, 16 SKIPPED"*, 989 rows): the diff prints **135** lines. **134** are `FAILED -> +BUILT` — 89 through phase 3 and phase 4's 45. The 135th is ` -> BUILT contents/SchedulingAMessage.md +10`, the key § *Splits* explains. No `-> SKIPPED`. **Control, both ways:** the report against itself +prints **0** lines; a copy with `SweeperCircuitBreaking.md` #8 set FAILED prints exactly one line +beside the known key that is not `FAILED -> BUILT`, `BUILT -> FAILED contents/SweeperCircuitBreaking.md 8`. + +**The ≤ 60 target: met, at 57.** Pages with nothing BUILT, by requirements' `awk` (`comm -23`) and a +Python join over the same report: **57** both. 4.1 said as low as 58; the 58th off the list is +`BrighterBasicConfiguration.md`, off the tranche. Phase 4 landed all ten of its pages with nothing +BUILT and a reachable block, and five of its six hard-only pages; `ReplayOnSeenReference.md` waits +on the pin. Phase 5 now has headroom, not a quota. + +**Every behavioural block was run with its control** (P0-10): the tables under 4.2, 4.3 and 4.4, +against released 10.7.0 packages, with real servers or emulators where a claim needed one. + +**Upstream:** BrighterCommand/Brighter#4335 stated on `BrighterInboxSupport.md` and linked from nine +inbox pages (ruling); #4443 (DynamoDB) and #4444 (Spanner) filed in 4.4, `Bug`, `0 - Backlog`, +linked from `SweeperCircuitBreaking.md`. + +**The PR changes 28 pages** (`git diff --name-only origin/master..HEAD -- contents | wc -l`): **21** +of the 22 tranche pages (`ReplayOnSeenReference.md` untouched) and **7** outside it — +`BrighterBasicConfiguration.md` and `DispatcherConfigurationReference.md` by recurrence; +`BrighterInboxSupport.md`, `FirestoreInbox.md`, `MongoDBInbox.md` and `SpannerInbox.md` by the #4335 +ruling; `SweeperCircuitBreaking.md` by ruling. **Said in session notes: 29** (20 + 6 + 3), which +counted `AzureBlobArchiveProvider.md` in both 4.2 and 4.4. Beside them: `tools/README.md` (rows 2 and +9, and a phase 4 paragraph), the baseline, `refs.csproj`, `pages.tsv` (36 → 53 pages mapped), 5 new +units and 4 changed, and this file. + --- ## Phase 5 — Tranche 2b and the recorded falsehoods *(6 tasks, one PR, CHANGES THE SITE)* diff --git a/tools/README.md b/tools/README.md index 17f6e26..565432e 100644 --- a/tools/README.md +++ b/tools/README.md @@ -56,17 +56,24 @@ added to show a workaround, so the corpus is **983 blocks** and the baseline **1 whose blocks were touched by the same defects' recurrences. Phase 2 had moved them at `5407298`: the baseline 101 → 129, `pagelint` 743 → 706. +**Rows 2 and 9 moved again at `ca6a0b2`, spec 017 phase 4**, the *Outbox and Inbox* pages with +one hard block each. The baseline went 189 → **234**, compiled with **35 scaffold units**. The pin +grew by three packages to **98** — the Npgsql EF Core provider and two TickerQ packages — so it +resolves **542 reference assemblies**. `pagelint` fell 658 → **616**: 36 on the repaired pages, and +6 on two pages outside the tranche: `BrighterBasicConfiguration.md`, by a defect's recurrence, and +`SweeperCircuitBreaking.md`, by the maintainer's ruling. + | # | Gate | Command | Expected at `412fd34` | |---:|---|---|---| | 1 | `linkcheck` | `python3 tools/linkcheck.py` | **165 files, 0 broken** | -| 2 | `pagelint` | `python3 tools/pagelint.py` | **0 errors, 658 warnings, 162 pages** — at `2223fcb`; it read **706** at `5407298`, **743** at `b941837`, **744** at `3be2a78` and **757** at `412fd34` | +| 2 | `pagelint` | `python3 tools/pagelint.py` | **0 errors, 616 warnings, 162 pages** — at `ca6a0b2`; it read **658** at `2223fcb`, **706** at `5407298`, **743** at `b941837`, **744** at `3be2a78` and **757** at `412fd34` | | 3 | shape | `python3 tools/urlmap.py --check-shape` | **161 pages, 12 sections, widest 12 of 20, deepest 4 of 4** | | 4 | redirects | `python3 tools/urlmap.py --check-redirects` | **77 entries, 7858 bytes** | | 5 | `versioncheck` | `python3 tools/versioncheck.py` | **0 stale pins of 18, across 5 pages** | | 6 | `optioncheck` | `dotnet run --project tools/optioncheck` | **0 mismatches across 59 tables, 519 rows** | | 7 | `--verify` | `python3 tools/urlmap.py --verify` | **161 predicted = 161 published** | | 8 | `symbolcheck` | `python3 tools/symbolcheck.py` | **0 findings — 22 entries, 161 pages, 3 silenced** — at `3be2a78`; it read **5 entries, 1 silenced** at `412fd34` | -| 9 | `blockcheck` | `python3 tools/blockcheck.py --report` | **983 blocks: 189 BUILT, 778 FAILED, 16 SKIPPED, 0 NOT_COMPILABLE — 0 findings, 16 skipped**, against **538 reference assemblies** with **30 scaffold units checked, 0 violations** — at `2223fcb`; it read **982 blocks, 129 BUILT** with 22 units at `5407298`, **989 blocks, 101 BUILT, 872 FAILED** at `b941837`, unmoved at `23aa74f` with 14 units, and **985 blocks, 92 BUILT, 12 SKIPPED** at `1e1944d` | +| 9 | `blockcheck` | `python3 tools/blockcheck.py --report` | **983 blocks: 234 BUILT, 733 FAILED, 16 SKIPPED, 0 NOT_COMPILABLE — 0 findings, 16 skipped**, against **542 reference assemblies** with **35 scaffold units checked, 0 violations** — at `ca6a0b2`; it read **189 BUILT** against 538 with 30 units at `2223fcb`, **982 blocks, 129 BUILT** with 22 units at `5407298`, **989 blocks, 101 BUILT, 872 FAILED** at `b941837`, unmoved at `23aa74f` with 14 units, and **985 blocks, 92 BUILT, 12 SKIPPED** at `1e1944d` | **Four of the nine are not in the `check` job of `.github/workflows/docs.yml`, and each absence is a decision rather than an oversight:**