|
| 1 | +# ADR-033: The final transport SPI: pull receivers, CESQL filters and the in-memory reference broker |
| 2 | + |
| 3 | +| | | |
| 4 | +|---|---| |
| 5 | +| **Status** | Accepted | |
| 6 | +| **Date** | 2026-11-11 | |
| 7 | +| **Deciders** | v3 architecture review; WP-3.1 implementation | |
| 8 | +| **Amends** | [doc 07](../07-Cloud-Transports.md) §2, §7; [ADR-014](ADR-014-transport-spi-and-conformance-kit-parity-required.md) | |
| 9 | + |
| 10 | +## Context |
| 11 | + |
| 12 | +Doc 07 §2 sketched a callback-based SPI with a separate sender and provisioner. The core built in M1 uses a pull-based `ITransport`. Here the receive loop, per-key workers, bounded channels and settlement live in the core (`ReceiveEndpoint`, RX-R1..R6). |
| 13 | + |
| 14 | +WP-3.1 has to freeze the SPI before three provider workstreams start in parallel. It also has to turn the 12-case kit into the planned TCK-01..45. |
| 15 | + |
| 16 | +## Decision |
| 17 | + |
| 18 | +1. **Pull, not callbacks.** `ITransportReceiver.ReceiveAsync(max, wait)` stays. |
| 19 | + - Every target broker has a native pull: SQS `ReceiveMessage`, Pub/Sub pull, Service Bus `ReceiveMessagesAsync`. |
| 20 | + - Keeping the loop in the core keeps backpressure, per-key ordering and shutdown identical on every broker. |
| 21 | + - The SPI stays at five types: `ITransport`, `ITransportReceiver`, `ReceivedMessage`, `TransportCapabilities` and `TransportTopology`. |
| 22 | +2. **Settlement is complete.** `ReceivedMessage` has `CompleteAsync`, `AbandonAsync(delay)` (native delay), `RenewAsync` (lock, visibility or lease extension) and `DeadLetterAsync(reason)` (the native backstop). |
| 23 | + - A late settlement after a lost lock is ignored. |
| 24 | + - The receive endpoint renews at half `ReceiveLockDuration` while a handler runs. |
| 25 | + - Disposing a receiver releases what it holds unsettled. |
| 26 | +3. **Capabilities drive the core.** `TransportCapabilities` keeps the size, attribute, retention and delay limits. It gains: |
| 27 | + - `OrderedDelivery`, `NativeDeadLetter`, `SubscriptionFilters`; |
| 28 | + - `MaxBatchSize`, `Deduplication` (with its window), `MaxLockDuration`. |
| 29 | + |
| 30 | + The core uses them to decide: |
| 31 | + - client-side filtering when a broker has no native filters (UT-RX-22); |
| 32 | + - whether to run the dead-letter ingestor; |
| 33 | + - how the TCK judges deduplication. |
| 34 | +4. **Errors are typed.** |
| 35 | + - `TransportException.IsTransient` (and `RetryAfter` for throttling) drives the error classifier. |
| 36 | + - `TransportSendException` reports a partial batch per message: oversized messages are refused before anything is sent for them, and the rest are sent. |
| 37 | + - `DestinationNotFoundException` names a missing endpoint or topic. |
| 38 | + - `TopologyValidationException.Problems` is the full diff under `Validate`, typed by `TopologyProblemKind`. |
| 39 | +5. **The topology carries what brokers need to configure.** `TransportTopology` gains: |
| 40 | + - `OrderedEndpoint` and `OrderedTopics`; |
| 41 | + - `MaxDeliveries`, the dead-letter threshold: the retry budget (1 + delayed retries) plus two; |
| 42 | + - per-topic `Filters`. |
| 43 | + |
| 44 | + The full topology model and naming conventions remain WP-3.5. `SourceFlowRuntime.Topology()` builds this topology for the host and the CLI. |
| 45 | +6. **Subscription filters are CloudEvents SQL.** |
| 46 | + - `CloudEventFilter` parses and evaluates CESQL 1.0 over the CloudEvents attributes: operators, `LIKE`, `IN`, `EXISTS` and the common functions. Errors never match. |
| 47 | + - Routing compiles `Only<T>()` to `type IN (…)` and ANDs it with `Where(…)`. |
| 48 | + - Transports translate `ExactTypes` and `TypePrefixes` natively. Anything else is evaluated client-side with the same evaluator, so results are identical (TCK-28). |
| 49 | +7. **The in-memory broker is the reference transport.** It sends messages through the real CloudEvents binary mapping with a 10-attribute budget, the strictest broker's, so the `sfext` overflow runs on every test. It also implements: |
| 50 | + - ordered endpoints (one in-flight message per key, head-of-line within a key); |
| 51 | + - locks with expiry and renewal; |
| 52 | + - a maximum delivery count and a dead-letter queue; |
| 53 | + - filtered fan-out, batches and partial failures; |
| 54 | + - a provisioning diff; |
| 55 | + - faults: outage, throttling, credential expiry, duplicates and dropped acks. |
| 56 | + |
| 57 | + `StrictDestinations` gives real-broker semantics for unknown destinations. It is off by default, so tests can send to services that start later. |
| 58 | +8. **The TCK runs the real core.** |
| 59 | + - `SourceFlow.Testing` carries a small generated domain (`TckModule`). Cases 18, 24, 25 and 45 start real services on the transport under test. |
| 60 | + - The fixture contract adds `Faults` (null on real cloud; those cases are skipped, never weakened), `InjectDefectAsync` for the `Validate` cases, `QuietPeriod` and `DelayTolerance`. |
| 61 | + - Payload protection cases 41–43 cover the transport side: ciphertext and marker are carried byte for byte and never inspected. Decryption and `DecryptionFailed` parking arrive with WP-5.1. |
| 62 | + |
| 63 | +## Consequences |
| 64 | + |
| 65 | +- Provider packages (WP-3.2..3.4) implement one small SPI, and must pass the same 45 cases as the in-memory broker. |
| 66 | +- Every in-memory test now exercises the CloudEvents wire mapping, so envelope regressions surface everywhere, not only in provider suites. |
| 67 | +- Brokers that cannot filter natively still filter correctly, at the cost of receiving and acknowledging unwanted events. |
| 68 | +- The broker dead-letter queue is a backstop. The ingestor moves its messages to the store, where `dlq replay` re-sends them. |
0 commit comments