Skip to content

Telemetry slice 3 — DriverLocationUpdated to Kafka + ADR-019 transport-agnostic topic naming - #46

Merged
erikshafer merged 5 commits into
mainfrom
telemetry/slice-3-kafka-publish
Jul 25, 2026
Merged

Telemetry slice 3 — DriverLocationUpdated to Kafka + ADR-019 transport-agnostic topic naming#46
erikshafer merged 5 commits into
mainfrom
telemetry/slice-3-kafka-publish

Conversation

@erikshafer

Copy link
Copy Markdown
Owner

Realizes W006 slice 3 — CritterCab's first Kafka topic and second live transport. After this PR, two of ADR-005's three transports are running and only Azure Service Bus remains modeled-but-unbuilt.

Session prompt: docs/prompts/implementations/008-telemetry-slice-3-kafka-publish.md · Retro: docs/retrospectives/implementations/008-telemetry-slice-3-kafka-publish.md

The seam did its job

PR #45 introduced IDriverLocationPublisher typed against the generated DriverLocationUpdated, with the proto already in codegen. So this PR swaps one implementation class and the host wiring — ReportLocationsHandler.cs does not appear in the diff at all. KafkaDriverLocationPublisher is one method.

The load-bearing decision

W006 §6.3 argues publish-before-store from failure-mode asymmetry: a failed store after a good publish costs a duplicate the consumer's dedup absorbs, while a failed publish after a good store costs a miss Dispatch cannot detect. Sound — but it silently assumes the publish's outcome is known when the store runs. It is not, twice over:

  • A Kafka publishing endpoint defaults to BufferedInMemory; PublishAsync returns once the record is queued in-process, before the broker acks.
  • Even SendInline() is insufficient alone, because Wolverine's default async retry block catches the send failure, logs it, re-posts to a background block and returns success.

The naive implementation would have produced a system where publish-first is true in statement order only, every broker failure lands in the branch §6.3 rejected, and nothing looks wrong. Shipped instead: SendInline() + Durability.UseSyncRetryBlock = true + UseIdempotentProducer(), so a rejection throws, the upsert is skipped, the baseline stays stale, and the next ping republishes.

Recorded in W006's Document History as an amendment, not a correction — §6.3's reasoning stands; it did not name its own precondition. UseSyncRetryBlock is process-global; affordable because Telemetry publishes to one transport.

ADR-019

W006 §11 candidate #1's trigger was literally "first Kafka topic lands," so it fires here. ADR-019 generalizes ADR-014's <source-bc>.<event-name-kebab> across transports while explicitly withholding its two ASB-specific operational clauses (session keying, outbox coordination) behind a per-transport table. ADR-014 stays Accepted and authoritative for ASB.

It also resolves a contradiction that predated this session: wolverine-kafka/SKILL.md had independently proposed a stream-descriptive naming rule for Kafka, so the repo held two conventions before either had a real topic to name. That argument was answered on its merits rather than dismissed.

This ADR is also the ADR-004 design-return interleave for the third consecutive Telemetry implementation PR.

Wiring

telemetry.driver-location-updated · partition key driverId · dedup key serverReceivedAt · no outbox · AutoProvision · binary protobuf, endpoint-scoped. Broker read by name, so Aspire (host port 5392, a slot the aspire skill had reserved), the Testcontainer, and Event Hubs all work without branching. Guarded on the connection string — no broker configured means the logging fallback and a service that still boots.

Tests — 33/33 green

  • Round trip (real broker, own fixture): asserts the partition key and that the value is binary protobuf DriverLocationUpdated.Parser accepts. The key assertion is the load-bearing one — without a key the records still arrive and only the ordering silently breaks.
  • Ordering (no broker, throwing publisher): a failed publish leaves no baseline, and the next ping republishes.
  • Separate fixture so the slice-1/2/4 suites never wait on Kafka.

Honest gaps

  • The claim that Wolverine's inline sender surfaces a broker rejection as an exception rests on source verification plus configuration, not a test. The ordering test covers CritterCab's half. Breaking a live broker mid-test would be slow and flaky; the gap is named in the retro rather than papered over.
  • aspire/SKILL.md's AddKafka example does not compile on 13.4.6 — hit on first build. Unnoticed because CI cannot see apphost.cs. DEBT row; the CI fix is its own session.
  • testing-integration's [CollectionDefinition(Name, DisableParallelization = true)] convention is followed by no shipped collection. Repo-wide, so deliberately not fixed slice-locally.

Review

jasperfx-source-verifier (6 gates, closed at authoring time) · critter-skill-auditor Phase 1 + Phase 2 · two-axis /code-review. All four found disjoint problems; fixes for the actionable ones are in the last two commits. 6 DEBT rows registered.

Next: PR D — slice 5, the Dispatch consumer, which also owns §6.3's Dedup GWT.

…fka)

Session prompt for PR C. The IDriverLocationPublisher seam shipped in PR #45
already carries the generated DriverLocationUpdated, so this session swaps only
the implementation and must not touch the contract.

A jasperfx-source-verifier pass ran during authoring and closed all six
verify-before-wiring gates against local wolverine @ V6.21.0-12. It surfaced the
session's one load-bearing fork: W006 §6.3 argues publish-first from failure-mode
asymmetry, but a Kafka publishing endpoint defaults to BufferedInMemory, where
PublishAsync returns before the broker acks — making that ordering nominal.
Resolved to SendInline + UseSyncRetryBlock + UseIdempotentProducer.

W006 §11 candidate #1 fires here (first Kafka topic lands) and lands as ADR-019
inside this PR, which also supplies the ADR-004 design-return interleave.
Swaps LoggingDriverLocationPublisher for a real WolverineFx.Kafka producer behind
the unchanged IDriverLocationPublisher seam (W006 §6.3). CritterCab's first Kafka
topic and second live transport; ReportLocationsHandler is untouched, which is
what the seam existed for.

Wiring: telemetry.driver-location-updated, partition key driverId, endpoint-scoped
binary protobuf, AutoProvision, and the broker read by name so Aspire, the
Testcontainer, and Event Hubs all work without branching.

SendInline + UseSyncRetryBlock rather than the BufferedInMemory default. Buffered
returns before the broker acks and Wolverine's default async retry block swallows
the failure, which would have made §6.3's publish-before-store ordering true only
in statement order — the upsert would land on a lost publish, the branch §6.3
argued against. Inline plus the sync block makes a rejection throw, so the baseline
stays stale and the next ping republishes.

Tests: a broker-backed round trip asserting the partition key and that the value is
binary protobuf the generated parser accepts, on its own fixture so the other suites
do not wait on Kafka; plus an ordering test pinning that a failed publish leaves no
baseline. 33/33 green.
… retro

Fires W006 §11 candidate #1, whose trigger was literally "first Kafka topic
lands". ADR-019 generalizes ADR-014's <source-bc>.<event-name-kebab> across
transports while explicitly withholding its two ASB-specific operational clauses
— session keying and outbox coordination — behind a per-transport table. ADR-014
stays Accepted and authoritative for ASB, with a scope note pointing at 019.

It also resolves a contradiction that predated this session: the wolverine-kafka
skill had independently proposed a stream-descriptive naming rule for Kafka, so
the repo held two conventions before either had a real topic to name. That skill's
topic-naming and serialization sections are corrected here under the
session-runner-blocking exception — a session cannot follow a skill that
contradicts the ADR it is authoring. The listener-side examples still name a
LocationPing/telemetry.location-pings pairing that never existed; deferred to PR D
as DEBT rather than swapped for differently speculative names.

W006's Document History records the publish-first qualifier as an amendment rather
than a correction: §6.3's failure-mode argument is sound but never states its own
precondition, that the publish outcome is known when the store runs.

Three DEBT rows: wolverine-kafka listener examples, transport-selection's missing
built-vs-modeled status axis, and the aspire skill's AddKafka example, which does
not compile on 13.4.6.
…udit

The topic-naming correction left section 'Convention-based routing' two sections
below still justifying itself with the rejected descriptive-name rule, inside a
file whose new banner claimed that section was reconciled. Rewritten to the real
reason Cab declines convention-based routing: it would bind a wire-visible topic
to a C# type name, so a refactor rename would repoint the producer silently.

Fourth DEBT row from the same audit — service-bootstrap does not sanction the
optional connection-string guard Telemetry has now used twice.
Standards axis, both live rather than theoretical:
- Testcontainers were unnamed. Latent while the project had one container;
  slice 3 added a second Postgres and xUnit runs the two collections in
  parallel, so a fixed name now collides. Both fixtures get unique names.
  Following the skill's own fix required correcting it twice over --
  WithPullPolicy is really WithImagePullPolicy, and PullPolicy lives in
  DotNet.Testcontainers.Images.
- A guard clause inside the UseWolverine lambda would have silently swallowed
  any configuration appended below it on the broker-less path, and the
  connection string was branched on twice in opposite polarity. Read once into
  kafkaEnabled, extracted ConfigureKafkaPublishing.
- StreamAsync/PingAt had been copied into three test classes. Extracted
  ReportLocationsClient; updated the pre-existing slice-2 copy too, since
  leaving one caller off a helper this session introduced is worse than the
  duplication it replaces.

Spec axis: closed deliverable 7 (the proto comment now records shipped state),
and dropped two speculative topic names the skill correction had minted in the
same file whose new banner warns against speculative names.

Two DEBT rows added: testing-integration's wrong API names and its collection
convention that no shipped fixture follows, and the CI-cannot-build-apphost gap.
33/33 green.
@erikshafer erikshafer self-assigned this Jul 25, 2026
@erikshafer
erikshafer merged commit bce99bd into main Jul 25, 2026
1 check passed
@erikshafer
erikshafer deleted the telemetry/slice-3-kafka-publish branch July 25, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant