Repository navigation
Replies: 3 comments
Revision 1: no redrive job, and code-first pagesOpen question 2: no redrive job, and no redrive runbook. Whether a dead letter can be replayed depends on why it was dead-lettered. That is learned only by running the system and looking at what fills Two more changes:
The charter's revisions 14 and 15 change this walkthrough as their tables say: a module of its own, Each section below replaces or adds to the part of the same name in the post. Everything else is unchanged. SummaryThe bullet on operators becomes:
The application
Operating it"Replaying dead letters" is replaced by:
The testsIn the row "The App, in process", "the redrive job against the same cluster" is dropped. The deployment
The pagesThis section is replaced. They are under
Design decisionsThe bullet "An operator's tools are tested like the processor" is replaced by:
Testing"The redrive job" is dropped from what wasn't prototyped. Open questionsQuestion 2 is answered above. In question 1, option (b) becomes:
The recommendation stays (a). Questions 1 and 3 are still open. Out of scopeThis is added:
ReferencesProposal: jobs is dropped, here and from what the walkthrough builds on. |
Revision 2: a retry topic, order enforced, and no operatingOpen question 1: an order-status projection, as recommended. Open question 3 no longer arises. The consumer never stops on a record. A record that fails for a reason that isn't its own goes to a retry topic, Three more changes:
Each section below replaces or adds to the part of the same name in the post, as revision 1 left it. Everything else is unchanged. SummaryThe bullet on operators is dropped. The bullets on dead letters, failures and failure injection become:
BackgroundThe bullet "A record's scenario can't be taken back" becomes:
The application
The topics and their recordsThis row is added:
Projecting an eventSteps 1 to 3 become:
"Failures that aren't the record's" is replaced by: Retries go through
Dead letters add
|
| Variable | |
|---|---|
KAFKA_RETRY_TOPIC |
required |
RETRY_ATTEMPTS, RETRY_MIN_DELAY, RETRY_MAX_DELAY |
10, 1s and 5m by default |
- The Service consumes
orders.eventsandorders.retry, in one group. - It builds two
Producers. The one fororders.retrysetsForwardScenario, so a retried record keeps its scenario. The one fororders.statusandorders.dlqdoesn't, so a scenario never reaches other teams' consumers. - The in-process tests and the kind overlay set
FAULT_TRUSTED_TOPICStoorders.eventsandorders.retry.
Its own instruments are orderstatus.dead_letters and orderstatus.retries, counters by orderstatus.reason.
Operating it
This section is dropped. Operating a processor like this one in production is the business of the end user who ships it. The note on dead letters moves to the page on retries and dead letters.
Production-ready, as a checklist
These rows change:
| How | |
|---|---|
| A rollout loses nothing | the revoke commits the record in flight. A record's work fits in 15s, under the 20s grace period, and terminationGracePeriodSeconds is 30. A retried record still waiting at SIGTERM is cut short when the grace period runs out, and processed again by its partition's next owner. The rollout case checks it |
| No record stops it | records that are malformed or of an unknown type are dead letters at once. The rest go through orders.retry, and then to orders.dlq |
| An outage blocks neither topic, and keeps order | its records wait in orders.retry. An order's later events follow its earlier ones there, since an event out of order is retried too |
| Least privilege | mTLS with ACLs that let it read orders.events and orders.retry, and write orders.status, orders.retry and orders.dlq. The rest is unchanged |
| Safe by default | no record's scenario trusted. Only the producer to orders.retry forwards one |
The rows "A record it can't apply doesn't stop it" and "An outage doesn't lose order or records" are replaced by the second and third above.
The tests
These rows change:
| Level | Where it runs | What it covers |
|---|---|---|
| The projector | go test, in the Build job, inside synctest |
decoding, the transitions, the guard, produce-then-store, the retry headers and the backoff, dead letters after the last attempt, and the deadline. Its Store and producers are the walkthrough's own interfaces, faked in the test |
| The App, in process | go test, in the Build job |
what only a test that controls the App can show: a retried record waiting at SIGTERM, cut short when the grace period runs out and processed again; a record that can go to neither orders.retry nor orders.dlq stopping the App, and processed after the restart; bad settings; the spans, the instruments and the breaker's |
The Target adds the retry topic, and the deployment's retry settings, so a case knows how long a record's attempts can take. The barrier covers orders.retry too: a case polls the group's committed offsets on both topics until neither holds a record of its own that isn't committed. The kind overlay and the in-process tests set short retry delays, so a record's attempts take seconds.
The cases change:
- Out of order is added.
placed,shipped, thenpaid:shippedis retried, and the order endsshipped, with three status events in order and no dead letter. - Dead letters becomes: a record that isn't JSON, and one of an unknown type, are dead letters at once. An event that never becomes valid, such as
deliveredfor an order that wascancelled, is a dead letter,invalid-transition, afterRETRY_ATTEMPTS. The partition moves on throughout. - Failure injection adds aborting
postgresql:orders. The record goes through its attempts toorders.dlq,unavailable, with no scenario. Nothing is projected for its order, the next event on its partition is projected, and the consumer never stops.
"Aborts stay in process" is dropped. The in-process tests of an abort that stops the consumer, and of untrusting the topic, go with it.
The deployment
- The ConfigMap adds the retry settings.
- The kind overlay's Job creates the four topics and their ACLs with
kafka-topics.shandkafka-acls.sh. - The kind overlay sets short retry delays, and trusts
orders.eventsandorders.retry.
The alert rules
OrderStatusRetryingis added: records have been retried for 10m, fromorderstatus.retries, as a ticket.- The lag rules read
orders.eventsonly, since records inorders.retrywait on purpose.
The pages
This section is replaced. The rules for a page are revision 1's.
- Overview: what the processor does, its topics, and the files it ends with.
- The topics: creating them and their ACLs with Kafka's CLI, keys, partitions and retention.
- The projection:
schema.sql, and the store's upsert. - Projecting an event: decoding, the transitions, the guard, and producing before storing.
- Retries and dead letters: the retry topic, the attempt header and the backoff, dead letters, and a note that what to do with
orders.dlqis the reader's to plan, from their business requirements. Init: the settings, the secrets as files, mTLS, the pool, theProducers, the probes,BuildTelemetry,Faultsandmain.- Testing: a unit test of the projector, the shared cases with their
Targetand barrier, and the failure injection cases. - Deploying: the Dockerfile, the kustomization, and the kind overlay. Running it on kind with
up.sh, and producing events withorderevents. - Observing: a record's trace across the topics in Grafana, its logs and metrics, the alerts and the dashboard.
- Production-ready: the checklist.
Design decisions
"Dead letters for the record's faults, a stop for everyone else's" and "Only delays are shared" are dropped. These are added:
- A consumer never stops on a record. Records that are malformed or of an unknown type are dead letters at once. Everything else is retried through
orders.retryand then dead-lettered, so neither topic is blocked. Only a record that can't be stored anywhere stops it. - Order is enforced, not trusted. The service owns order status, so it checks every transition, whatever its producers promise. An event out of order is retried rather than dead-lettered, since the event before it may be in
orders.retry. - Retries keep no state. The attempt is a header and the wait derives from it, so one
Processhandles both topics, and any replica can take any record.
"No retry around Produce" adds: a status event that fails sends the record to orders.retry, as any outage does.
Testing
This is added: the prototype's abort stopped the App, as the post designed. The retry topic, its wait and the guard on retried records weren't prototyped.
Open questions
Questions 1 and 3 are answered above. This one is added:
-
The guard against redelivery. It keeps the partition and offset of the last event applied, and skips any record at or before them. With a retry topic, one case gets past the transition check unseen.
paidwaits inorders.retry, thencancelledarrives, andplaced→cancelledis valid, so it's applied. Whenpaidcomes back, it is below the guard and skipped, soorders.statusnever says the order was paid.- (a) Keep every event applied, by its partition and offset on
orders.events, rather than only the last. A latepaidis then seen as never applied, failscancelled→paid, and goes through its attempts toorders.dlq. - (b) Keep only the last, as the post does, and accept the case.
Recommendation: (a).
- (a) Keep every event applied, by its partition and offset on
Out of scope
This is added:
- Operating it in production: runbooks for lag, a record that won't process, or rebuilding the projection. They're for an end user who ships a processor like this one.
References
These are added:
- queue: when should a Processor's context be cancelled?
- Kafka's topic and ACL tools
Revision 3: an order's status is folded from its eventsOpen question 4: the projection keeps every event it applied, and an order's status is folded from them. A late event takes its place in the order's history rather than being skipped or dead-lettered. Without a real product's business case, this is a decision made for now. Each section below replaces or adds to the part of the same name in the post, as revision 2 left it. Everything else is unchanged. SummaryThe bullets on order and on redelivery become:
Projecting an eventSteps 2 to 4 become:
The projection in PostgreSQLThe schema becomes: CREATE TABLE order_events (
source_partition integer NOT NULL,
source_offset bigint NOT NULL,
order_id text NOT NULL,
type text NOT NULL,
at timestamptz NOT NULL,
PRIMARY KEY (source_partition, source_offset)
);
CREATE INDEX order_events_by_order ON order_events (order_id, at);
CREATE TABLE order_status (
order_id text PRIMARY KEY,
status text NOT NULL,
at timestamptz NOT NULL
);"One upsert stores a change, and only moves forward" is replaced by:
The role's privileges become Production-ready, as a checklistThis row changes:
The testsThe projector's row covers the fold in place of the guard: an event at the end, a late event and its changes, a refused history, and a skipped redelivery. This case is added:
The pagesPages 3 and 4 become:
Design decisions"The offset is stored with the state" is replaced by:
Open questionsNone. Question 4 is answered above. Out of scopeThese are added:
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Important
Revision 3 answers open question 4, the last one. The projection keeps every event it applied, and an order's status is folded from them in the order of their
at. A redelivery is found by its offset and skipped. A late event takes its place in the history, andorders.statusgets its change and every later one again, so a consumer that keeps the latest status may see it flicker. It's a decision for now, without a real product's business case.Important
Revision 2 answers open questions 1 and 3, and adds question 4. It is an order-status projection. The consumer never stops on a record: one that fails for a reason that isn't its own goes to
orders.retry, with its attempt in a header and a wait derived from it, and toorders.dlqonce its attempts run out, so the abort cases are shared. An event out of order is retried, not trusted. Operating it is out of scope. The topics and their ACLs are created with Kafka's CLI, and a small Go CLI produces events by hand. Question 4, the guard against redelivery, is open.Important
Revision 1 answers open question 2 and corrects question 1. There is no redrive job or runbook: whether a dead letter can be replayed depends on why it was dead-lettered, which each end user learns by running their system, so the pages tell the reader to plan for it from their business requirements. Question 1's option (b) would run its in-process cases against WireMock in a container. The pages are code-first steps. Questions 1 and 3 are still open.
Note
Part of the humus re-evaluation: wave 6 of Proposal: humus on the bedrock process model, which plans one proposal per walkthrough. This is the Kafka processor that #317 asked for. It keeps the points every walkthrough keeps, from revision 13, and doesn't decide them again.
It builds on proposals that are done:
It depends on no other wave-6 proposal. Its pages wait for the story of revision 13 that adds the Walkthroughs section.
Summary
A walkthrough is a production-ready application, kept in this repository, with pages that walk a reader through how it was built and why. This one is a Kafka processor of an event stream, the thing Kafka is for:
placed,paid,shipped,deliveredandcancelled, toorders.events, keyed by the order's ID. The processor keeps each order's status in PostgreSQL, and produces each change of status toorders.status, for services such as notifications to consume.shippedbeforepaidis refused.orders.dlqwith its reason, and the partition moves on.synctestagainst kfake, and against the deployed image in kind.baggageheader carries a scenario slows PostgreSQL or the status topic. Its status is still projected, once, and the scenario doesn't go on toorders.status.Background
kafka-at-least-once,kafka-at-most-onceandkafka-mtls-at-least-once. Each one ran with podman-compose beside a Grafana stack and a dashboard, and was tested by anintegration_test/main.goagainstlocalhost:9092. The reset deleted them. They showed the API, but not a service someone operates:processor.go), which a restart or a second replica forgets;Processor, stored beforeProcessreturns. So once the group has committed past a record, the dead-letter topic holds its dead letter. This walkthrough also produces its status events before it returns, so the same barrier covers them.kafka_consumergroup_lag.Target. The same cases run in process, and against a deployed environment as functional tests. They poll with plain loops, which work in both.Proposed API
A walkthrough adds no API to humus. What it adds is an application, its manifests, its tests and its pages.
The application
It lives in the
examplemodule, atexample/orderstatus. The module gainsgithub.com/z5labs/humus/queue/kafka, replaced with../queue/kafkaas the core module is, and franz-go'skadmandkfakefor its tests. It already has pgx and PostgreSQL's testcontainers module.The topics and their records
orders.events{"type": "placed" | "paid" | "shipped" | "delivered" | "cancelled", "at": RFC 3339}orders.status{"order_id", "status", "previous", "at"}orders.dlqorders.eventsnever gains partitions: adding some moves keys to other partitions, which breaks both the order and the projection's offset guard. A topic that needs more partitions is a new topic.kafka.Settingsleaves them. The pages list what each needs: partitions,min.insync.replicas, and retention.orders.eventskeeps its records as long as the projection might need rebuilding from them. The kind overlay creates its own topics.Projecting an event
The projector is the
queue.Processor[kafka.Message]thatkafka.NewServiceruns, underqueue.AtLeastOnce. For each record:atis a dead letter,malformed. An unknowntypeis a dead letter,unknown-type, so that a producer that ships a new type before the processor doesn't stop it.placedcomes first.paidfollowsplaced,shippedfollowspaid,deliveredfollowsshipped, andcancelledfollowsplacedorpaid. An event that repeats the current status changes nothing. Any other event is a dead letter,invalid-transition.orders.status, and only then store the projection, with the record's offset. A crash between the two produces the status event again on redelivery, and never loses one. The consumers oforders.statustell duplicates apart by the order and the status, as any consumer under at-least-once must.Dead letters are produced synchronously, before
Processreturnsnil, as the Kafka page's Dead letters requires. They keep the record's key, value and headers, and add headers of their own:dlq.reason,dlq.error,dlq.topic,dlq.partitionanddlq.offset. TheProducerreplaces the trace context, so a dead letter'ssendspan is in the consumed record's trace.Failures that aren't the record's are retried, and then stop the consumer:
retry: 5 attempts, backoff from 100ms to 2s, and a 2s timeout per attempt.DeliveryTimeout, and the Kafka page explains why a retry aroundProducewould duplicate and reorder records.humus.Main's 20s grace period, so a rollout's SIGTERM never finds a record in flight that can't finish.Process's error. The Service stops, and the App exits 1 with the record named. The partition's later records wait until the outage ends, and the lag alert fires. Dead-lettering instead would send an outage's every record toorders.dlq, and apply each order's later events without its earlier ones.The projection in PostgreSQL
store/schema.sqlis the schema. The kind overlay applies it before the processor starts, and production uses the end user's migration tool, as the API walkthrough shows for the shoppingcart's. The processor's role needsSELECT,INSERTandUPDATEonorder_statusand nothing else.Initbuilds the pool's config fromPOSTGRES_URL, which holds no password. The password comes fromPOSTGRES_PASSWORD_FILE, and the CA fromPOSTGRES_TLS_CA_FILE, both read throughs.Host.FS. The pool dials throughs.Host.Dial, and nothing is left for pgx to read from the real environment or filesystem, as the shoppingcart'sappdoc explains.postgresql:orders, named<system>:<database>as Points of your own suggests. It wraps theStoreinterface rather than living in the PostgreSQL code, so the in-memory store the in-process tests use has the point too.InitIt sources every setting with
config.Struct, and a bad one exits 78, naming it:KAFKA_BROKERS,KAFKA_GROUP_IDKAFKA_EVENTS_TOPIC,KAFKA_STATUS_TOPIC,KAFKA_DEAD_LETTER_TOPICKAFKA_TLS_CA_FILE,KAFKA_TLS_CERT_FILEKAFKA_SESSION_TIMEOUT,KAFKA_DELIVERY_TIMEOUT,KAFKA_BREAKER_OPEN_FORkafka.DefaultSettingsandkafka.DefaultProducerSettingsby defaultPOSTGRES_URL,POSTGRES_PASSWORD_FILE,POSTGRES_TLS_CA_FILEPROBES_ADDR,PROBES_TLS_PEM_FILEhttphealth.SettingsFAULT_MAX_DELAY,FAULT_TRUSTED_TOPICSfault.DefaultSettings's 10s, and no topicOTEL_SERVICE_NAME,OTEL_*_EXPORTER,OTEL_EXPORTER_OTLP_ENDPOINTBuildTelemetry, as on the Telemetry pageIt opens the pool, and registers its close with
s.OnStop.It builds the
Producer, for the consumer's own cluster, with the same brokers and TLS.It builds the projector on the store, wrapped by its injection point, and the Service, with
TrustScenarioset to trust the topicsFAULT_TRUSTED_TOPICSnames.It returns the Service and a probes server,
httphealth.NewService(s, probes, health.Readiness(consumer.Joined())).mainishumus.Main(app.Init, humus.WithTelemetry(app.BuildTelemetry), humus.WithFaults(app.Faults)). The process has no server that a load balancer routes to, so it stays onhumus.Main's defaults, as the Kafka page's WhichMainsays.Faultsbuilds failure injection withjsonscenario, under the edge's key,acme.fault, the Failure injection page's key, which every walkthrough uses.FAULT_TRUSTED_TOPICSis empty by default, so production trusts no record's scenario. The in-process tests and the kind overlay set it toorders.events. TheProducerkeepsForwardScenariooff, so a scenario never goes on toorders.statusororders.dlq, whose consumers are other teams' trust boundaries.Its own instrument is
orderstatus.dead_letters, a counter of dead letters byorderstatus.reason. Everything else it shows comes from humus, kotel and the semantic conventions.Operating it
The pages give each task a runbook, and the tests run what can be run in process:
Lag. Alert on the group's lag growing, and on lag with no commits. Then look at
messaging.process.duration, PostgreSQL, and theProducer's breaker. More replicas help only up to the partition count.A record that stops the consumer. The App's last log record names the topic, partition and offset, and so does each restart's. Either fix forward and deploy, or skip the record:
kafka-consumer-groups.sh --reset-offsets --to-offset <offset+1> --topic orders.events:<partition> --execute;The skipped record stays in
orders.eventsuntil its retention ends. A scenario that stops the consumer is recovered from by removing its topic fromFAULT_TRUSTED_TOPICS. The rollout then strips the scenario, and the record is processed.Replaying dead letters. Once a fix has shipped, such as support for a new event type, an operator runs the redrive job, a Kubernetes Job from
deploy/redrive/:job.NewServicerun onhumus.Main, in the same image.orders.dlqfromREDRIVE_SINCEup to the end offsets it finds at start, by partition, with a franz-go client that dialss.Host.Dial. A finite read is a job's, notqueue/kafka's Service.dlq.reasonis inREDRIVE_REASONSback to itsdlq.topic, with its key, value and headers, minus thedlq.ones, through akafka.Producer.A record redriven twice is a duplicate, which the projector handles. One that is still invalid returns to
orders.dlq.Rebuilding the projection.
order_status.The status events are produced again, and their consumers dedupe them. Without the truncate, the replay changes nothing, which is how the offset guard is tested.
Production-ready, as a checklist
The walkthrough's pages close with this list, and the application meets each item:
config.Structand the packages' checkss.Host.FS. The record's key, an order's ID, isn't recorded, asqueue/kafkadoesn't record itGetClientCertificate, and the probes' throughGetCertificate, by a small helper of the walkthrough's own that reads their files again when they changemaxUnavailable: 1Joinedas readiness, on an HTTPS probes portterminationGracePeriodSecondsis 30, as the Kafka page's budget shows. The rollout case checks itorder_statusorders.eventsand write the other two. A role with three privileges on one table. No service account token. A non-root, read-only containerorders.status, logs and metrics over OTLP, and rules for lag, a stalled group, a crash loop, dead letters, an open breaker and slow processingForwardScenariooffThe tests
go test, in the Build job, insidesynctestStoreand producer are the walkthrough's own interfaces, faked in the testtestcontainerstag, in a CI job that isn't requiredstoreagainst PostgreSQL in a container: the forward-only upsert, a race between two writers, TLS, and the injection pointgo test, in the Build jobInitandFaultsincluded, on humustest's Host insidesynctest, against kfake over mTLS with certificates fromhumustest.NewCA. Its store is in memory, as the shoppingcart's unit tests swap theirs ingo test, in the Build jobpromtool test rules, in the kind jobfunctionaltag, in a CI job that isn't required, and against an end user's staging clusterThe shared cases are in
cases, and take aTarget:Rolloutfunc, which replaces the processor's replicas and returns once the old ones are gone.Each case keys its records by order IDs from
crypto/rand, so it only ever reads its own. Its barrier is the group's committed offset: it pollskadm'sFetchOffsetsuntil the group has committed past its last record. Then it readsorders.statusandorders.dlqby partition, from a minute before it began, which allows for clock skew, up to the end offsets. After the barrier, what it finds, and what it doesn't, is settled. The cases:placed,paidandshippedgive three status events, in order, and no dead letter.Rolloutare each projected once. In process,Rolloutstarts a second App on the same cluster, waits for the group to hold both, and stops the first with SIGTERM. Deployed, it iskubectl rollout restartandkubectl rollout status. With noRollout, the case is skipped.baggageheader carries a scenario:postgresql:ordersby 1s, under the store's 2s attempt timeout: the status is projected once, with no dead letter, and the status event's baggage doesn't carry the scenario;kafka:orders.statusby 1s: the status is projected once.Every case mutates, so the cases never run against production. A shared staging topic is fine: the cases read only their own keys.
Aborts stay in process. An abort on
postgresql:ordersstops the consumer, and the record still carries its scenario after every restart. Against a shared cluster, it would hold that partition for everyone until an operator untrusted the topic. So the abort, the crash it causes, the records before it committed and the one it stopped on not committed, and the recovery after the topic is untrusted, are tests of the App in process. Each of those tests has a cluster of its own. Open question 3.The deployment
deploy/is a kustomization:maxSurge: 1andmaxUnavailable: 0, the HTTPS probes, resources,terminationGracePeriodSeconds: 30and a security context;alerts.yaml, the Prometheus rule file;redrive/, the redrive Job, which nothing applies until an operator does.The kind overlay adds:
apache/kafka-nativeas the Kafka module's container tests run it, with an mTLS listener;schema.sqlapplied before the processor starts;The kind job generates a CA and every certificate and password on each run, so none is kept in the repository. The Dockerfile builds
orderstatusandredriveas static binaries, onto a distroless, non-root base.The alert rules
deploy/alerts.yamlholds six rules, each explained on the Observing page:OrderStatusLagHighkafka_consumergroup_lagOrderStatusStalledmessaging_client_operation_duration_seconds_count{messaging_operation_name="commit"}OrderStatusCrashLoopingkube_pod_container_status_restarts_totalOrderStatusDeadLettersorderstatus_dead_letters_total, by reasonOrderStatusBreakerOpenProducer's breaker is openhumus_circuit_state{humus_circuit_state="open"}OrderStatusSlowProcessingmessaging_process_duration_secondsThe first and third read metrics from outside the App, because a consumer that has crashed reports nothing. The pages say which exporter each assumes, and how to change it.
The pages
They are
docs/content/walkthroughs/orderstatus/, in the order the processor was built:Init, the settings, mTLS, the probes, telemetry andFaults.Changes to other files
.github/workflows/build.yaml:example/go.mod: thequeue/kafkamodule, replaced with../queue/kafka, and franz-go'skadmandkfake.Design decisions
kafka.Producerdoesn't offer them, and they can't reach PostgreSQL anyway.orders.dlq. A failure of PostgreSQL or the cluster would dead-letter every record of an outage, and leave orders half applied. So it is retried, and then stops the consumer, which is the loud failurequeue's contract chose.Produce. franz-go retries insideDeliveryTimeout, and a retry around it would store records twice and out of order.synctestkeeps the cases fast and in the required Build job, as revision 13's second level asks.Testing
The walkthrough is its own test: see The tests.
A prototype checked the claims the cases depend on, at main
1bb3bb53, in a module of its own that replaceshumusandhumus/queue/kafkawith this repository. It has a projector over an in-memory store with thepostgresql:orderspoint, the realkafka.Serviceandkafka.Producer, acasespackage that imports neither, and anapp_test.gorunning them insidesyncteston humustest's Host against kfake.go test -race -count=3 ./...passes:kadm'sFetchOffsetswith plain sleeps, inside the bubble, returns once the group has committed past the case's record.orders.statusandorders.dlq, read byListOffsetsAfterMilli,ListEndOffsetsandConsumePartitions, then hold exactly the case's records.dlq.reasonand its source offset, and the next record of its key is projected.baggageheader reachespostgresql:orderswhenFAULT_TRUSTED_TOPICSnames the topic, and the record is still projected once. The status event'sbaggagecarries noacme.fault.serve: kafka: processing orders.events, partition 2, offset 1: reading order …: fit: injected failure at postgresql:orders. Nothing is produced for that order. A second App withFAULT_TRUSTED_TOPICSunset processes the same record.Not prototyped: the PostgreSQL store, mTLS on kfake (which the Kafka page's tests already cover), the redrive job, the rule tests and the kind job.
Migration from today
Nothing migrates. The reset deleted v0.15.0's Kafka examples, and the site has no Walkthroughs section yet.
Open questions
Which processor.
synctestwith nothing swapped. But it has no state, so there is no idempotency, replay or rebuild to show, and order per key barely matters.queue.AtMostOnce. It shows the otherDelivery, but most processors need at-least-once, and its aggregation across rebalances would be the harder half of the walkthrough.Recommendation: (a).
The operator's tools.
dlq.headers and keeps each key is then a shell pipeline, first run during an incident.Recommendation: (a).
Aborts in the shared cases.
Targethook that untrusts the topic by rolling the processor out withFAULT_TRUSTED_TOPICSunset. Staging would then see the crash loop that production would, but every abort case would cost a rollout, and would hold its partition for everyone else's traffic until the hook ran.Recommendation: (a).
Out of scope
References
example/queuekafka-consumer-groups.shAll reactions