Repository navigation
Replies: 1 comment
Revision 1: no replay, a worker that never stops, and WireMockOpen questions 1 to 3 are answered as recommended:
The Kafka processor walkthrough's revisions 1 and 2 made choices that hold here too:
Two more changes:
The charter's revisions 14 and 15 change the rest 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. SummaryIn the bullet "It is what RabbitMQ is for", the parking lot's sentence becomes: "A request that can never succeed goes to a parking-lot queue, with why. What to do with it is the reader's to plan." In the bullet on the three levels, the second becomes "the whole App against RabbitMQ and WireMock in containers, on The bullet on failure injection becomes:
BackgroundThe bullet "Trust in a RabbitMQ message comes from its The application
The topology
Processing a requestThese rows of the table change:
In the dead letters' bullet, "So a replay starts counting again" is dropped. A dead letter still leaves out Calling the provider"The stand-in" is replaced by: WireMock stands in for the provider. The provider is fictional, so the walkthrough defines its API, and owns WireMock's mappings. They are in the kind overlay, and the container tests load the same files:
WireMock keeps no state between requests, so it never answers 409. The provider client's unit tests serve handlers of their own on humustest's
|
| How | |
|---|---|
| No request stops the worker | malformed requests go to the parking lot at once. The rest are parked in mail.send.retry, an event that can't be published included, and dead-lettered after MAIL_MAX_ATTEMPTS. Only a request that can go to neither mail.retry nor mail.dead stops the worker, and the delivery limit dead-letters it after 5 deliveries |
| Least privilege | no configure permission on the broker; write and read on its own exchanges and queue only, granted with rabbitmqctl set_permissions. No service account token. A non-root, read-only container |
The row "A message that can never succeed doesn't block the queue" is replaced by the first.
Operating it
This section is dropped. Running a mailer 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, and what a rollout does moves to the page on deploying.
Observing it
"RabbitMQ's own" becomes: each queue's depth and consumers, from the Collector's RabbitMQ receiver, which reads the management API with a login tagged monitoring.
The alerts change:
| Alert | When |
|---|---|
MailRetrying |
requests have been parked for 10 minutes, from mail.requests{mail.outcome="retried"}, as a ticket |
MailCrashLooping |
a mailer container restarted more than 3 times in 15 minutes, which is what a request that can go nowhere looks like |
MailRetrying is added. MailBacklog reads mail.send only, since the messages in mail.send.retry wait on purpose.
The tests
These rows change, and the last is added:
| Level | Where it runs | What it covers |
|---|---|---|
| The provider client | go test, inside synctest |
the client against handlers of the test's own on humustest's Host: the idempotency key, the 409 retried, Retry-After, the breaker opening, and the API key |
| The shared cases, in-process | testcontainers tag, in the "Mailer walkthrough (testcontainers)" job, not required |
the whole App on humus.OSHost() and humustest's process, against RabbitMQ in a container with the definitions imported, and WireMock in a container with the walkthrough's mappings |
| The App, in process | testcontainers tag, in the same job |
a request that can go to neither mail.retry nor mail.dead, under a scenario that aborts the provider and the publish to mail.retry. The App exits 1 on each delivery, and the test restarts it after each exit, as the kubelet does. After the delivery limit, the request is dead-lettered with x-first-death-reason: delivery_limit. In a deployed environment this is a crash loop of every replica that takes it, so it isn't a shared case |
The Target:
- "the stand-in's URL and an HTTP client" becomes "WireMock's URL and an HTTP client, to read its request journal";
ProviderPoint's example becomeshttp:wiremock.mail.svc:8443;Replayis dropped.
What the provider was sent. Each case sends to an address of its own. It finds what the provider was sent in WireMock's request journal, POST /__admin/requests/find, by that address. Under "The barrier is the request's outcome", "what the provider holds for the key is final" becomes "what WireMock's journal holds for the address is final".
The cases change:
- Sent: WireMock was sent one request, with the
MessageIdas itsIdempotency-Key, and a bearer token. - Sent once: every request WireMock was sent carries the same
Idempotency-Key, and every event names the same provider ID. - Malformed: WireMock was sent nothing.
- The provider down replaces "The provider down, then replayed". Under a scenario that aborts
ProviderPoint, each processing fails and parks the request, scenario and all. AfterMAIL_MAX_ATTEMPTSprocessings, it is dead-lettered withgave-upand without its scenario, and WireMock was sent nothing. - A lost connection: every request WireMock was sent carries the same
Idempotency-Key, the event names the ID that key gets, and a request published after it is sent too. - A result that can't be published: under a scenario that aborts
rabbitmq:mail.events:mail.sent, each processing calls the provider, fails to publish the event, and parks the request, scenario and all. AfterMAIL_MAX_ATTEMPTSprocessings, it is dead-lettered withgave-upand without its scenario. Every request WireMock was sent carries the sameIdempotency-Key, nomail.sentarrives, and the worker never exits. It still runs alone, since it opens the events Publisher's breaker.
The deployment
- The Dockerfile builds static binaries of
mailerandmailrequest. - The kind overlay's RabbitMQ has its management plugin only.
- WireMock, with the walkthrough's mappings, replaces the stand-in.
up.shgenerates the logins and creates them withrabbitmqctl: the mailer's; the cases', which is inMAIL_TRUSTED_PUBLISHERS;mailrequest's, which may writemailonly; and the Collector's, taggedmonitoring.
The pages
This section is replaced.
They are under docs/content/walkthroughs/mailer/. Each page is a list of steps. A step is a sentence or two, then the code it arrives at, quoted from the walkthrough's files. A reason is given only where the code would be wrong without it, and in one sentence. For example, a reason explains why a transient failure is parked rather than returned. There is no history, no alternatives and no asides.
- Overview: what the mailer does, its contract, its exchanges and queues, and the files it ends with.
- The topology:
definitions.jsonand its policies, importing it, and creating the logins and their permissions withrabbitmqctl. - Calling the provider: the
httpclientclient, theIdempotency-Key,Classify,Retry-After, the API key, and WireMock's mappings. - Processing a request: decoding, the decision table, publishing with confirms,
ErrUnroutable, and acking. - Retries and dead letters: the delay queue, the attempt header, the dead letters and their headers, the delivery limit, and a note that what to do with
mail.send.deadis the reader's to plan, from their business requirements. Init: the settings, the secrets as files, the two Publishers andUserID,TrustScenario, the probes,BuildTelemetry,Faults, andmain's grace period.- Testing: a unit test of the Processor, the shared cases with their
Targetand barrier, and the failure injection cases. - Deploying: the Dockerfile, the kustomization, the disruption budget, the grace period, and the kind overlay. Running it on kind with
up.sh, and publishing requests withmailrequest. - Observing: a request's trace in Grafana, its logs and metrics, the alerts and the dashboard.
- Production-ready: the checklist.
A gap in humus
This section is replaced.
The gap is closed. #953 reported that a humus Publisher couldn't set a message's user-id. Its fix, #962, added UserID to rabbitmq.Publishing, which RabbitMQ validates against the Publisher's login. The retry Publisher sets it to the mailer's login, so a parked request keeps a trusted scenario. No case waits for it any longer.
Design decisions
These are replaced:
- "A delay queue, and a count the worker keeps" ends: "One fixed delay, rather than one that grows with each attempt: a message waiting in a queue holds no worker and never holds up a shutdown."
- "The delivery limit is a backstop, not the retry" becomes: it counts what crashes the worker, which is a request that can go to neither
mail.retrynormail.dead, or a pod killed while it works. Those restart a pod per delivery, so 5 is enough. The quorum queue's default of 20 would crash-loop for an hour. - "Two Publishers" ends: "the worker's own retry queue takes part in the scenario, but other teams' consumers and the parking lot don't."
- "The stand-in implements an API the walkthrough defines" becomes: WireMock, with mappings the walkthrough owns. The provider is fictional, so WireMock stands in for no other project's server. A real provider's sandbox takes its place in staging.
"Dead letters lose their attempt count" is dropped. These are added:
- A worker never stops on a request. A request that can never succeed is dead-lettered at once. Everything else is parked, an event that can't be published included, and dead-lettered after its attempts. Only a request that can go to neither
mail.retrynormail.deadstops it. - No replay. Whether a dead letter can be replayed depends on why it was dead-lettered, and that is learned by running the system. So it is each end user's to plan, and the walkthrough ships no tool for it.
Testing
"The replay" is dropped. This is added: the prototype ran before #962. It didn't try a retry Publisher that sets UserID, or WireMock.
Open questions
None. Questions 1 to 3 are answered above.
Out of scope
These are added:
- Replaying dead letters, with a shovel, the management UI or a runbook. What can be replayed depends on why requests are dead-lettered in each end user's system.
- Operating it in production: runbooks for queue depth, redeliveries, the parking lot, a broker's rolling restart, or rollouts. They're for an end user who ships a mailer like this one.
"Dashboards" is dropped, since the charter's revision 15 gives every walkthrough one.
References
The shovel is dropped. These are added:
- Proposal: Kafka processor walkthrough, its revisions 1 and 2
- #953 and its fix, #962
- RabbitMQ's
rabbitmqctl - WireMock's response templating and verifying
- The OpenTelemetry Collector's RabbitMQ receiver
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 1 answers every open question, and takes the Kafka processor walkthrough's changes. It is a mailer, its topology is a definitions file, and failures wait in a delay queue with one fixed delay. There is no replay: the pages tell the reader to plan for
mail.send.deadfrom their business requirements. The worker never stops on a request, and an event it can't publish parks the request. Operating it is out of scope. The logins are created withrabbitmqctl, and a small Go CLI publishes requests by hand. WireMock replaces the stand-in, and the cases read its request journal. #953 is fixed, so no case waits for it.Note
Part of the humus re-evaluation: wave 6 of Proposal: humus on the bedrock process model, which plans one proposal per walkthrough, each keeping the points of its revision 13. This is the RabbitMQ processor that #318 asked for. The reset closed #318.
It builds on proposals that have all landed:
It found one gap in humus: a humus
Publishercan't set a message'suser-id, so no consumer can trust a scenario that a humus service forwards. Revision 13's first point makes a gap a bug report, not a workaround, so it is reported as #953, and the one case that needs its fix waits for it. See A gap in humus.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 RabbitMQ work queue: a mailer, which sends transactional email through an email provider's HTTP API.
Other services ask for an email by publishing a request to the
mailexchange. The mailer consumes themail.sendqueue, calls the provider once per request, and publishes the result tomail.events:mail.sentwith the provider's ID, ormail.refusedwith the provider's reason.It is what RabbitMQ is for. Replicas compete for one quorum queue, and each message is acked once its result is the broker's. Transient failures wait in a delay queue and come back. A request that can never succeed goes to a parking-lot queue, where an operator inspects it and replays it. The queue's delivery limit is the backstop for a message that crashes the worker. The Kafka processor walkthrough, beside this one in wave 6, is the event stream, ordered per key. This one is the opposite case: no order, and work per message.
It uses most of
queue/rabbitmq,httpclientandfault:AtLeastOnce, withPrefetchandConcurrencytuned for I/O-bound work;Publishers, one per trust boundary;Consumingas readiness;httpclient's retries for aPOSTwith anIdempotency-Key, aClassifyof the provider's own,Retry-Afterand the breaker;TrustScenariobyUserID,ForwardScenario, and injection points in both clients.It owns its topology, because the Service declares nothing. A RabbitMQ definitions file of exchanges, quorum queues, bindings and policies is shipped for the broker's team to import. The tests and the kind job import the same file.
It is tested at three levels, with the cases written once:
synctest;humus.OSHost();RabbitMQ has no committed offset to wait on, so each case's barrier is its request's outcome: an event, or a dead letter.
Failure injection is part of the tests. A scenario that aborts the provider shows the outage end to end: retries, delays, giving up, the parking lot and the replay. A scenario that cuts the worker off from the broker shows a lost connection and a redelivery that sends nothing twice. A scenario that fails the event's publish shows the crash loop that the delivery limit ends.
{"to": "ada@example.com", "subject": "Your order shipped", "text": "Order 1234 is on its way."}Published to
mailwith the routing keymail.send, aMessageId(its idempotency key) and an optionalCorrelationId, it becomes amail.sentevent with the same IDs and a body of{"provider_id":"msg_…"}.Background
story(docs): create walkthrough documentation for a prod ready rabbitmq processor #318 asked for a production-ready RabbitMQ processor. Proposal: RabbitMQ #765 built
queue/rabbitmqand left the walkthrough to wave 6. It said this walkthrough's App tests run against RabbitMQ in a container, onhumus.OSHost(), rather than insidesynctest, and that its test cases would be written once and run against a deployed environment too.humus leaves the hard parts of a work queue to the end user, on purpose. The RabbitMQ page says so in three places:
AtLeastOnce, a processing error stops the Service and exits 1.So a worker whose dependency has an outage of a minute must not return an error for it, or every replica crash-loops. Where the message waits instead, how long, and when to give up, are what a walkthrough has to show.
An HTTP dependency fits
httpclientwhen it is one origin. One client per dependency ishttpclient's rule. An email provider is one origin, with idempotency keys, rate limits,Retry-After, and refusals that aren't outages, so it uses all of the client.Trust in a RabbitMQ message comes from its
user-id. RabbitMQ validates it against the publishing connection's user, so failure injection trusts a delivery byUserID. Butrabbitmq.Publishinghas noUserIDfield, and its conversion to amqp091's sets noUserId(publisher.go#L42-L118). So a message that a humus service publishes arrives with an emptyUserID, and a consumer strips any scenario it forwards (faults.go#L34-L44). The prototype below confirms it.v0.15.0 had no RabbitMQ example. Its walkthroughs were an orders REST API and a 1BRC job, and they ran under podman-compose with no deployment. The reset deleted them.
Proposal: Kubernetes controller walkthrough is the first walkthrough proposed. It is outside the waves, and this proposal shares its shape: the production-ready checklist, the three levels and the shared cases.
Proposed API
A walkthrough adds no API to humus. What it adds is an application, its topology, its manifests, its tests and its pages.
The application
It lives in the
examplemodule, atexample/mailer. The module gainsgithub.com/z5labs/humus/queue/rabbitmq, replaced with../queue/rabbitmqas the core module is.The contract
A request is published to the
mailexchange with the routing keymail.send:MessageIdis required, and is the provider's idempotency key, so a publisher that publishes a request twice gets one email;CorrelationIdis optional, and the events carry it back;to,subjectandtext, up to 256 KiB.The sender's address isn't in the request. It is the mailer's setting, so no publisher can send as anyone else.
An event is published to the
mail.eventstopic exchange, with the request'sMessageIdandCorrelationId:mail.sent, with the provider's message ID;mail.refused, with the provider's reason, for a request that the provider will never accept, such as an address that doesn't exist.A service that wants the results binds a queue of its own. An event that no queue is bound to receive is returned as
rabbitmq.ErrUnroutable, which the mailer treats as no error, because nobody subscribes to it yet.The topology
deploy/rabbitmq/definitions.jsonis a RabbitMQ definitions file. It holds no users and no passwords:mailmail.sendmailbymail.sendmail.retrymail.send.retrymail.retrymailwithmail.sendmail.deadmail.send.deadmail.deadmail.eventsmail.sentandmail.refusedThe behaviour is in policies, not queue arguments, so an operator can change a delay or a limit without deleting a queue:
mail.send:delivery-limit5,dead-letter-exchangemail.dead,dead-letter-strategyat-least-once, andoverflowreject-publish, which at-least-once dead-lettering requires.mail.send.retry:message-ttl5 minutes,dead-letter-exchangemail,dead-letter-routing-keymail.send, and the same strategy and overflow.The broker's team imports the file with
rabbitmqctl import_definitions, or the management API. The kind overlay and the container tests import it the same way, into a running broker, and add a policy of their own, of higher priority, with a delay of 1s and a delivery limit of 2. The prototype found that importing at boot, withload_definitions, needs the vhost in the file and skips creating the default user. The pages say so.Least privilege. The mailer's RabbitMQ user is granted:
^$), since it declares nothing;^mail\.(retry|dead|events)$;^mail\.send$.Processing a request
The Processor runs under
queue.AtLeastOnce, so a request is acked only once its result is the broker's. It decides on each delivery like this:MessageId, not JSON, a field missing, over 256 KiBmail.dead, reasonmalformedmail.sentmail.refusedMAIL_MAX_ATTEMPTSmail.retry, withx-mail-attemptone higherMAIL_MAX_ATTEMPTSgave-up, with the last errorProcessreturns the error. The Service stops, the pod restarts, and the delivery is redelivered and counted toward the delivery limit.x-mail-dead-reason,x-mail-dead-attemptsandx-mail-dead-error, withoutx-mail-attempt. So a replay starts counting again.MessageId, the outcome and the attempt only.Calling the provider
httpclientclient forPROVIDER_URL, trustingPROVIDER_CA_FILEor the system's roots.POST /v1/messageswith anIdempotency-Key, the request'sMessageId. Its body is a*bytes.Reader, so the client may retry it (Which requests are retried).PROVIDER_API_KEY_FILE, throughs.Host.FS, and set asAuthorization: Bearerby the provider client. Outbound credentials are service code's, as Proposal: authentication and authorization #870 left them.Classifyis the default, plus one answer. The provider answers 409 while another request with the same key is in flight. That happens when a lost connection redelivers a request to a second replica while the first is still sending it. A 409 is then retried, and counts for the provider rather than against it, as in Writing aClassify.Retry-Afterlonger thanBackoffMaxends the call, as the client does. The request then waits in the delay queue instead, so a rate limit holds no worker.The stand-in implements the provider's API, which the walkthrough defines, since the provider is fictional:
POST /v1/messages: 202 with an ID; the same key again gets the same ID; 409 while that key is in flight; 422 for an address in.invalid, a domain that can't exist (RFC 2606);GET /v1/messages?idempotency_key=…: what it accepted for a key, which is how the cases check that an email was sent once.The stand-in is an
http.Handler. The unit tests serve it on humustest's Host, the container tests on the OS Host withhttpserver, and the kind overlay as a Deployment of its own, from the same image. It is the walkthrough's own stand-in for a dependency it invents, not a fake of another project's server.InitIt sources every setting with
config.Struct. A bad one exits 78, naming its variable:RABBITMQ_ADDR,RABBITMQ_USERNAME,RABBITMQ_PASSWORD_FILERABBITMQ_VHOST,RABBITMQ_CA_FILE/; TLS when the CA file is setRABBITMQ_PREFETCH,RABBITMQ_CONCURRENCYMAIL_FROMMAIL_MAX_ATTEMPTSMAIL_TRUSTED_PUBLISHERSPROVIDER_URL,PROVIDER_API_KEY_FILEPROVIDER_CA_FILE,PROVIDER_TIMEOUT,PROVIDER_ATTEMPT_TIMEOUT,PROVIDER_MAX_ATTEMPTShttpclient.DefaultSettingsPROBES_ADDR,PROBES_TLS_PEM_FILEFAULT_MAX_DELAYPasswords and keys are read from files, mounted from Secrets, never from the environment.
It builds two Publishers, one per trust boundary.
ForwardScenariois a Publisher's setting, and the two kinds of destination differ:mail.retry, withForwardScenarioset, so a parked request keeps its scenario;mail.events, whose consumers are other teams, andmail.dead, whose consumers are operators and their tools. These strip the scenario.TrustScenariotrusts a delivery whoseUserIDis inMAIL_TRUSTED_PUBLISHERS, or is the mailer's own user. The mailer's own messages carry a scenario only when a trusted publisher's did, since an untrusted one was stripped first. With the default, an empty list, production trusts no publisher. Trusting its own user needs the fix for the gap.It returns the Service and an
httphealthprobes server, withhealth.Readiness(svc.Consuming()). It has no liveness monitor: the Service reconnects on its own.Faultsbuilds failure injection withjsonscenario, under the keyacme.fault, the Failure injection page's key, which every walkthrough uses.mainsets a grace period of 25s withhumus.WithShutdownGracePeriod. A delivery in flight at SIGTERM may need the provider'sTimeoutand a publish'sTimeout, 10s each, andhumus.Main's 20s would cut it short. The manifest'sterminationGracePeriodSecondsis 35: the grace period, the telemetry's 5s shutdown, and a margin.Production-ready, as a checklist
The pages close with this list, and the mailer meets each item:
config.Structand the packages' checkss.Host.FSGetCertificate, by a small helper of the walkthrough's own that reads its files again when they changeminAvailable: 1Consumingas readiness, on a probes serverterminationGracePeriodSeconds: 35MAIL_MAX_ATTEMPTS. The breaker sheds calls while the provider keeps failingdeploy/alerts.yamlmail.refusedeventTrustScenariotrusts no publisher; the sender's address is a settingOperating it
The pages give operators a runbook:
Queue depth and age. The backlog of
mail.send, and howConcurrencyand replicas drain it. Autoscaling on queue depth is out of scope.Redeliveries. What raises them: a lost connection, a rollout cut short, a crash. Each counts toward the delivery limit.
The parking lot. How to read a dead letter's reason headers, and how to replay
mail.send.deadback tomail, with a dynamic shovel or the management UI's Move messages. The prototype found three things the pages state:x-delivery-count;user-idis kept.A replay moves every dead letter, so one that can't succeed comes straight back. A replay later than the provider's idempotency window, 24 hours for the stand-in, can send an email the provider had already accepted. The pages say to replay within it.
A broker's rolling restart. Every connection is closed once per node, with
320 CONNECTION_FORCED. The workers loglost the connectionand reconnect, and nothing restarts.Rollouts. What the drain does, and what
HUMUS_SHUTDOWN_GRACE_PERIODchanges.Observing it
process mail.send, a child of its publisher'ssend mailspan, with the provider's attempts and thesendspan of its result under it. The worker setsmail.attemptandmail.outcomeon the process span.mail.requests, a counter of requests bymail.outcome:sent,refused,retried,gave_upormalformed. It is the end user's own instrument, named for its domain.humus.circuit.*andhumus.retry.decisionsfor the provider's client and both Publishers.deploy/alerts.yamlholds the rules, each explained on the pages:MailBacklogmail.sendhas had more than a threshold of ready messages for 10 minutesMailNotConsumingmail.sendhas had no consumer for 5 minutesMailParkedmail.send.deadisn't emptyMailGivingUpmail.requests{mail.outcome="gave_up"}rose in the last 15 minutesMailProviderBreakerOpenhumus.circuit.statehas beenopenfor 5 minutesMailRetriesThrottledhumus.retry.decisionswasthrottledin the last 5 minutesMailCrashLoopingThe tests
go test, insidesynctest, in the Build jobgo test, insidesynctestRetry-After, the breaker opening, and the API keyInitgo test, insidesynctestgo testdefinitions.jsondeclares every exchange and queue thatmailnames, with the policy keys the design relies onpromtool check rulesandpromtool test rules, in the kind jobtestcontainerstag, in the "Mailer walkthrough (testcontainers)" job, not requiredhumus.OSHost()and humustest's process, against RabbitMQ in a container with the definitions imported, and the stand-in on the OS Hostfunctionaltag, in the "Mailer walkthrough (kind)" job, not required, and against an end user's stagingThe shared cases are in
cases. It imports neither the application nor humustest, so the compiler keeps them black-box. Each case is a function of atesting.TBand aTarget:TargetfieldProviderPointhttp:standin.mail.svc:8443, which a port-forward's address differs fromScenarioKeyCloseConnectionsReplaymailwith a shovel, as the runbook does. nil skips the case that needs itTimeoutMAIL_MAX_ATTEMPTStimes the delayThe barrier is the request's outcome. RabbitMQ has no committed offset to wait on. So before it publishes, each case binds queues of its own, exclusive and server-named, to
mail.eventsand tomail.dead. It then waits for the event or the dead letter with itsMessageId.mail.deadis a fanout exchange, so a case's own queue gets a copy of every dead letter, the delivery limit's included, and the cases run in parallel against one broker.The cases:
mail.sent, with itsMessageIdandCorrelationId, and the provider holds one email for its key..invalidbecomesmail.refused, at the first attempt, and nothing is dead-lettered.malformed, no event follows, and the provider holds nothing for its key.ProviderPointby 2s, underAttemptTimeout, the request is still sent at the first attempt.ProviderPoint, each processing fails and parks the request, scenario and all. AfterMAIL_MAX_ATTEMPTSprocessings, it is dead-lettered withgave-upand without its scenario, and the provider holds nothing for its key.Replaythen moves it back, and it is sent at the first attempt.CloseConnectionscloses every connection while the request is in flight. Its ack fails, it is redelivered, and the provider is called again with the same key. The provider holds one email, the event arrives, and a request published after it is sent too.rabbitmq:mail.events:mail.sent, the event's publish fails, soProcessreturns its error, and the worker exits 1 on each delivery. After the delivery limit, the request is dead-lettered withx-first-death-reason: delivery_limit, and the provider still holds one email. The in-process harness restarts the App after each exit, as the kubelet does.The cases mutate the broker and the provider, so they never run against production. In staging, the alone cases are an outage of the mailer that runs them. The pages say to run them against instances of their own, as Containing a scenario in production describes.
The deployment
deploy/baseis a kustomization:scheme: HTTPS, resources,terminationGracePeriodSeconds: 35, and a non-root, read-only security context;automountServiceAccountToken: false, since the mailer never calls the Kubernetes API;The kind overlay adds:
MAIL_TRUSTED_PUBLISHERS.The Dockerfile builds static binaries of the mailer and the stand-in onto a distroless, non-root base.
The pages
docs/content/walkthroughs/mailer/, in the order the mailer was built:httpclient, idempotency keys,Classify,Retry-After, the breaker, the API key, and the stand-in.ErrUnroutablemeans for each destination, and idempotency.Init, the settings, secrets from files, the two Publishers, the probes, telemetry,FaultsandTrustScenario, and the grace period.terminationGracePeriodSeconds, and handing the definitions to the broker's team.Target, and the barrier.UserID, and running them in staging.A gap in humus
RabbitMQ validates a message's
user-idagainst the publishing connection's user, which is why failure injection trusts a delivery byUserID. But a humusPublishercan't set it:rabbitmq.Publishinghas noUserIDfield, and its conversion to amqp091's sets noUserId(publisher.go#L42-L118). So every message a humus service publishes arrives with an emptyUserID, and a consumer strips any scenario it carries (faults.go#L34-L44). The prototype confirms it.That breaks more than this walkthrough. The failure injection page's own example trusts the end user's services by their
UserID, which no humus service can send today. Revision 13's first point makes such a gap a bug report, not a workaround:UserIDonPublishing, which RabbitMQ then validates against the Publisher's login.user-idof its own.Changes to other files
example/go.mod: thequeue/rabbitmqmodule, replaced with../queue/rabbitmq; amqp091-go; and testcontainers' RabbitMQ module..github/workflows/build.yaml:testcontainersandfunctionaltags ofexample/mailer.The Walkthroughs section and the docs test's rule for its samples come from #629's revision 13. This walkthrough's pages wait for them.
Design decisions
mail.send.retry, whose TTL is the delay, and counts attempts in a header of its own. One fixed delay, not one queue per delay or a TTL per message: a quorum queue expires messages only from its head, so a long TTL would hold shorter ones behind it. Open question 3.ForwardScenariois per Publisher, and the trust boundary differs per destination: the worker's own retry queue takes part in the scenario, but other teams' consumers and the operators' parking lot don't. Two Publishers are two connections, which RabbitMQ handles easily.Testing
The walkthrough is its own test: see The tests.
Prototyped against RabbitMQ 4.3.6 in a container, with humus at
1bb3bb53, undergo test -race -count=3:rabbitmqctl import_definitions, andlist_queuesshows each policy applied. Imported at boot, withload_definitions, the file needs its vhost, and the broker then creates no default user, so the testcontainers login is refused.mail.retrycame back tomail.sendafter the 1s TTL (1.03s), withx-deathreasonexpired.x-delivery-count1 and 2 on the redeliveries. It was then dead-lettered through the fanoutmail.deadto both the parking lot and a case's own exclusive, server-named queue, withx-first-death-reason: delivery_limitand itsuser-idkept.mail.send.deadtomailmoved the dead letter back, with itsuser-idkept. Its first delivery carried the stalex-delivery-count3, and it was then delivered 3 more times before it was dead-lettered again, so a replay gets the whole limit back.rabbitmq.NewService, anhttpclientclient to a stand-in on the OS Host,humus.WithFaultsand aTrustScenarioonUserID, was given messages carrying a scenario that aborts the stand-in's point:user-idwas trusted reached the Processor with its scenario, and its call failed with*fault.InjectedError;PublisherwithForwardScenarioset published again, carried the scenario in itsbaggageheader but arrived with an emptyUserID, so it was stripped.Migration from today
Nothing migrates. humus had no RabbitMQ example before the reset, and the site has no Walkthroughs section yet.
Open questions
Which worker.
Prefetch,Concurrencyand resources tuned for it. But it calls no API throughhttpclient, and an S3 SDK brings retries of its own.httpclientis one client per origin, built inInit. So it would teach working around the client.Recommendation: (a).
How the topology reaches the broker.
Exchange,Queue,BindingandPolicy, applied with the mailer's kustomization. They are Kubernetes-native, but need RabbitMQ's Cluster Operator and Topology Operator in the cluster, and in the kind overlay.Recommendation: (a).
How transient failures wait.
Processreturns the error, and the delivery limit bounds it. Each failure restarts a pod, so an outage of the provider crash-loops every replica, and readiness can't stop it.Recommendation: (a).
Out of scope
Concurrency1.References
publisher.go#L42-L118andfaults.go#L34-L44All reactions