Repository navigation
Replies: 2 comments
Revision 1: the open questions answeredThe open questions:
Three decisions made in other wave-4 proposals reach this one, and this revision records them:
Each section below replaces or adds to the part of the same name in the post. Background, Connecting, Settings, What SummaryThe bullets on
Proposed API
// Consuming returns a monitor that is healthy once the Service has started
// consuming its queue: unhealthy until the broker first answers its
// basic.consume, and healthy from then until Serve returns. A lost
// connection after that doesn't make it unhealthy. It never returns an
// error. A consumer is made ready once it consumes with
//
// health.Readiness(consumer.Consuming())
//
// It is for readiness only: restarting the pod doesn't bring the broker
// back, and an error that a restart would fix already ends Serve.
func (svc *Service) Consuming() health.Monitor
Order and concurrencyThe last sentence becomes: A The PublisherIn the second bullet, "humus adds no metrics of its own for them" becomes: humus records no instruments of its own for them. Whether it should, for every client at once, is Proposal: humus's own metrics (wave 5). This is added after the table of attempt results: The table is fixed. The HTTP client's Running on
|
Closing: RabbitMQ has landedThe revision's stories are done:
The revision left no open questions. Tests that need a broker run against RabbitMQ in a container, and humus writes no fake broker, as the charter's revision 10 records. The Service declares nothing, it reconnects with backoff, and the Publisher stayed in this proposal. The rest is out of scope or belongs to other proposals, as "Out of scope" lists:
|
Uh oh!
There was an error while loading. Please reload this page.
Note
Part of the humus re-evaluation; see Proposal: humus on the bedrock process model, wave 4. It builds on Proposal: queue processing and the Kafka runtime, whose
queuecontract it implements, on Proposal: humus.Main, and on the wave-1 proposals: the lifecycle, configuration, OpenTelemetry without globals and humustest. It depends on no other wave-4 proposal. It blocks Proposal: failure injection (wave 5), which strips scenarios at this Service and adds an injection point to its Publisher, and Proposal: walkthroughs and a humus MIGRATING.md (wave 6), whose RabbitMQ processor (#318) is built on it.Summary
queue/rabbitmqis a new nested module. ItsServiceconsumes one RabbitMQ queue with amqp091-go, RabbitMQ's own Go client, and hands each delivery to aqueue.Processor[rabbitmq.Message]. It implementsqueue's contract exactly asqueue/kafkadoes, and shares no code with it.AtLeastOncea delivery is acked onceProcessreturns nil, and a processing error rejects it and ends the Service, so the App exits 1 and the broker delivers it again. UnderAtMostOncea batch of deliveries is acked, and the ack confirmed, before any of it is processed. Neither uses auto-ack, so the prefetch count always bounds what the client holds.Serve's context the Service cancels its consumer, lets the deliveries in flight finish and acks them, and returns everything it received but didn't process with onebasic.nack, which RabbitMQ doesn't count toward a quorum queue's delivery limit.Host. The Service dialss.Host.Dial, runs the TLS handshake itself and opens AMQP on that connection with amqp091'sOpen, so no URL, nonet.Dialand noos.Argsare involved. When the connection drops, the Service reconnects with bedrock'sretry, as franz-go keeps retrying for Kafka.Consumingis a readiness monitor. It is false until the Service first consumes its queue, so a pod with a wrong broker address never turns ready, and stays true after that.Publisherpublishes with confirms. It is a client built inInit, with its own connection, publisher confirms and mandatory routing, bedrock'sretryandcircuit, and a span and two instruments per publish, following the conventions the charter sets for the wave-4 clients.Host, insidesynctest. No in-memory RabbitMQ exists, so humus writes one,rabbitmqtest. A prototype of the Service and a consume-only fake pass whole-App tests under-race. Container tests run against RabbitMQ in a CI job of their own, as for Kafka.app.Initsources therabbitmq.Settings, builds theProcessor, and returns the Service thatrabbitmq.NewService(s, settings, p)builds.Background
humus has never had RabbitMQ. story(queue): implement a runtime for rabbitmq #310 asked for it before the reset, and the reset closed it. The charter's revision 7 put it in wave 4, on the
queuecontract: "It decides whether it also publishes."What wave 3 settled.
queueis the contract between the end user's code and any broker (queue/doc.go):AtLeastOnceends the Service, and the message is delivered again when the App restarts;AtMostOnceis logged and counted, and processing goes on;Process's context is cancelled only when the grace period runs out;Processor's.The Kafka proposal sketched how RabbitMQ would fit: ack after
Process, stop on an error so the broker requeues, and auto-ack forAtMostOnce. This proposal keeps the first two and replaces auto-ack (see Design decisions).The shape
queue/kafkaset. A nested module, aServicebuilt byNewService(s, settings, p)that opens no connection, plainSettingswith a check per field, telemetry froms.Telemetrywith semconv's messaging conventions, and no App, noMainand no defaults of its own, becausehumus.Main's 20s grace period and no drain fit a consumer (its revision). This proposal follows it wherever RabbitMQ allows.A lesson from Kafka. queue/kafka: a fenced-out member's late commit can move the new owner's committed offset #757 found that a fenced-out member's late commit can move the new owner's offset. RabbitMQ's acks can't do that: a delivery tag belongs to one channel, and an ack sent on a channel that has closed fails in the client and never reaches the broker.
amqp091-go v1.15.0 is the client: maintained by the RabbitMQ team, pure Go, and with one requirement, goleak, for its own tests (
go.mod). Five of its properties shape this design:amqp.Openruns AMQP over a connection the caller has already opened.DialConfigtakes a URL, and itsConfig.Dialhas no context.Config.Recovery, marked experimental, which works only throughDialConfig's URL.consumers.go#L74). The prefetch count is the only bound, and "the server ignores this option when consumers are started with noAck" (channel.go#L912-L914).os.Args[0](consumers.go#L19-L21). Its logger is a package-level variable,amqp091.Logger, a no-op by default (log.go).net.Pipenetwork they run inside asynctestbubble.Quorum queues count failed deliveries. Since RabbitMQ 4.0 a quorum queue has a delivery limit of 20 by default. A message delivered more often than that is dead-lettered, if the queue has a dead-letter exchange, or dropped.
basic.rejectand a channel or connection that closes with the message unacked count toward the limit.basic.nackwith requeue doesn't (quorum queues, Poison Message Handling).Proposed API
amqpisgithub.com/rabbitmq/amqp091-go.Settingsnames its types in two places:SASL, asqueue/kafka's names franz-go'ssasl.Mechanism, andMessage.Headers, whose nested tables and decimals are amqp091's.Connecting
The Service and the Publisher connect the same way, with unexported code they share:
Addrwiths.Host.Dial, under a context bounded byConnectTimeout.TLSset, run the handshake over that connection withtls.Client(...).HandshakeContext, on a clone of the config whoseServerNameisAddr's host when the config leaves it empty.queue/kafka's dialer does the same.ConnectTimeout, and callamqp.OpenwithSASL,Vhost,Heartbeatand the locale.Opentakes no context, so the deadline bounds the AMQP handshake. amqp091 clears it once the connection is open.So humus never calls
DialConfig: no URL, which would carry the password and be logged by mistake, and noDefaultDial, which callsnet.DialTimeout. An end user who keeps an AMQP URI parses it withamqp.ParseURIin theirInitand fillsConnectionfrom it.Settings
Connection.Addrhost:port, port 1 to 65535CheckAddrConnection.Vhost"/"CheckVhostConnection.TLSnil, plaintextConnection.SASLCheckSASLConnection.Heartbeat10sCheckHeartbeatConnection.ConnectTimeout30sCheckConnectTimeoutQueueCheckQueueDeliveryqueue.AtLeastOnceat-least-once,at-most-oncequeue.CheckDeliveryPrefetch10CheckPrefetchConcurrency1PrefetchCheckConcurrency, andNewServicefor the boundAddr,SASLandQueueare required. humus imposes no port, as forhttpserverandgrpcserver, and AMQP has no anonymous login.HeartbeatandConnectTimeoutkeep amqp091's defaults, 10s and 30s, whichDialConfigapplies. Heartbeats can't be turned off: a dead connection would then go unnoticed until TCP gave up.Prefetchof 10 andConcurrencyof 1 keep a queue's order by default, hide the round trip of each ack behind nine more deliveries, and hold back at most ten messages from the queue's other consumers.ConcurrencyabovePrefetchwould leave workers that never get a delivery, soNewServicerejects it, naming both fields.Deliveryis the end user's design decision, as for Kafka, so mostInits set it in code, andDefaultSettingsgives the safe one.What
Servedoesbasic.qostoPrefetchand consumesQueuewith manual acks and a consumer tag of its own, so amqp091 never builds one fromos.Args[0].Concurrencyworkers. Each processes one at a time, with a context that keeps the values ofServe's context, carries the delivery's span, and is cancelled only whenShutdown's context is done.Deliverysays (next section).Delivery
AtLeastOnce. A worker acks its delivery onceProcessreturns nil. WhenProcessreturns an error:basic.reject, which counts toward a quorum queue's delivery limit;Servereturns the error, naming the queue and the message's ID when it has one, and the App exits 1.So a message that always fails crash-loops the pod until the queue's delivery limit dead-letters or drops it: 20 deliveries on a quorum queue, by default since RabbitMQ 4.0. A classic queue has no delivery limit, and the crash loop is what shows the failure, as it does for Kafka. Nothing else counts toward the limit, so a message that was merely prefetched when a sibling failed is never dropped for it.
AtMostOnce. The Service takes a delivery and every delivery already buffered behind it, up toPrefetch, and acks them with onebasic.ackwithmultipleset. Then it waits for the reply to abasic.qoson the same channel, and only then hands the batch to the workers. RabbitMQ handles a channel's methods in order, so the reply means the ack was processed. An AMQP ack has no reply of its own. If the connection drops before the reply arrives, the batch isn't processed: the broker either had the ack, and the batch is lost, or requeues it for someone else. Either way, nothing is processed twice.This is
queue/kafka's at-most-once, which commits a batch before processing any of it, withPrefetchin place ofMaxPollRecords. A crash loses at most one batch, and a clean shutdown loses nothing: the Service processes the whole batch it acked, and nacks the rest. A processing error is logged at ERROR and counted, and processing goes on.Order and concurrency
A queue delivers its messages to one consumer in order, so with
Concurrency1 the Service processes them in the order they were enqueued. Three things break that order, as with any RabbitMQ consumer: a redelivery, more than one replica, andConcurrencyabove 1. AProcessorthat needs per-key order uses a single-active-consumer queue, which is topology (see open question 2), andConcurrency1.Shutdown
When
Serve's context is done, orShutdownis called, the Service:basic.cancel, so the broker sends nothing more;AtMostOncebatch it already acked;basic.nackwithmultipleset, every delivery it received but didn't process;Nothing load-balances a consumer, so there is nothing to drain for: it stops taking work at once, as
queue/kafkadoes. WhenShutdown's context is done first,Shutdowncancels the context the deliveries in flight are processed with, closes the connection at once, waits forServe, and returns an error wrapping its context's error, which the lifecycle reports as a*humus.GracePeriodError. The broker then requeues every unacked delivery, and that counts toward a quorum queue's delivery limit, because the grace period ran out.Connection loss
A connection drops when the network does, when 1.5 heartbeat intervals pass with nothing read, or when the broker closes it. A node that shuts down for an upgrade closes every connection with
320 CONNECTION_FORCED. The broker requeues every delivery that was unacked on it. The Service then:retry:Exponential(100ms, 30s), noBudget, andmath.MaxIntattempts, so that onlyServe's context ends it. That is howretry's docs describe retrying a connection at startup. Each failed attempt is logged at WARN. The first connection is made the same way, so a broker that is down at startup is waited for, as franz-go waits for Kafka's.Some errors aren't fixed by reconnecting. The Service stops, and
Servereturns the error, when the broker:ACCESS_REFUSED, or amqp091'sErrCredentialsorErrSASL) or the vhost (ErrVhost);404 NOT_FOUND,403 ACCESS_REFUSEDor405 RESOURCE_LOCKED, for a queue another connection consumes exclusively;406 PRECONDITION_FAILED, such as a delivery held past the broker'sconsumer_timeout.The broker cancelling the consumer, because its queue was deleted, is handled as a lost connection. The reconnect then finds the queue missing, and the Service stops.
Failures
AtLeastOnceAtMostOnceProcessreturns an errorServereturns the error; exit 1ProcesspanicsServereturns the error; exit 1Telemetry
Everything comes from
s.Telemetry, under the scopegithub.com/z5labs/humus/queue/rabbitmq, and follows semconv v1.43.0's RabbitMQ conventions, which humus pins asqueue/kafkadoes.A span per delivery, named
process <queue>, of kindCONSUMER, whose contextProcessis given. Its parent is whats.Telemetry.Propagatorextracts from the message's headers, through an unexported carrier overMessage.Headersthat reads string values. A message whose headers carry no trace context starts a new trace. Its attributes are:messaging.system(rabbitmq), andmessaging.operation.typeandmessaging.operation.name, bothprocess;messaging.destination.name, which semconv defines as{exchange}:{routing key}:{queue}, leaving out what is empty and the routing key when it equals the queue. A message published toorderson the default exchange isorders;messaging.rabbitmq.destination.routing_keywhen it isn't empty, andmessaging.rabbitmq.message.delivery_tag;messaging.message.idandmessaging.message.conversation_id, the correlation ID, when they are set;server.addressandserver.port, fromAddr.A processing error is recorded on the span, with
error.typeand an error status. The body is never recorded.Two instruments from semconv's
messagingconv:messaging.client.consumed.messages, counting the deliveries handed to theProcessor, andmessaging.process.duration, witherror.typewhenProcessfails. Each hasmessaging.system,messaging.operation.name,messaging.destination.name,server.addressandserver.port. An ack has no reply, so it has no duration to record. semconv's destination name includes the routing key, so a publisher that puts identifiers in routing keys gets a series per key.Logs. At INFO,
consumingon each connection, with the queue andserver.address. At WARN, each failed connection attempt and each lost connection. At ERROR, a processing error underAtMostOnce. An error that ends the Service is returned, and the lifecycle logs it.amqp091 logs nothing of its own here. Its logger is a package-level variable, which humus never sets, so its few lines stay on its no-op default. They are about its experimental recovery, which humus doesn't use, and two failures to answer the broker, which then surface as a lost connection. A forbidigo rule bans
SetLoggerin humus's code, beside OTel's globals.The Publisher
The charter's three conventions for the wave-4 clients apply to it:
Setup.Hostand reports throughSetup.Telemetry.NewPublisheropens nothing. The firstPublishconnects, as described under Connecting, on a connection of its own, as RabbitMQ advises, so that TCP back-pressure on publishing never holds up a consumer's acks.NewPublisherregisters a function withs.OnStopthat closes the connection within the grace period. The Publisher stays usable until then, so aProcessorfinishing its delivery during the drain can still publish.retryandcircuitas bedrock's docs do. EachPublishranges over oneRetrier's attempts, withinTimeout, and each attempt goes through oneBreaker, which stands in front of the broker. The Publisher builds one of each from itsRetryBudget,BreakerandBreakerPolicysettings, which have the names, defaults and checks the HTTP and gRPC clients' settings have. Each transition is one log record, in the context of the publish that made it:circuit breaker openat WARN, andcircuit breaker half-openandcircuit breaker closedat INFO, withserver.addressandserver.portfromAddr,circuit.previous_state, and the counts of the state it left:circuit.successes,circuit.failures,circuit.consecutive_successes,circuit.consecutive_failuresandcircuit.timed_out. humus adds no metrics of its own for them.An attempt takes a channel from a pool of
Channels, connecting first if needed, and publishesmwithmandatoryset. Then it waits for the broker's confirm, or forctx. Each channel is in confirm mode and carries one publish at a time. amqp091 hands abasic.returnto the channel's listener before it handles the confirm that follows it, so a return that is waiting once the confirm arrives belongs to that publish.retrycircuitErrUnroutable404 NOT_FOUNDfor a missing exchange, or403ctx's deadlineretryctxcancelled by the callercircuit.ErrRejectedA retry after a confirm that was lost with its connection can publish the message twice, so publishing is at least once. A consumer of it is idempotent anyway, under
queue's contract.Publishwrites a copy ofm.Headerswith the trace context injected bys.Telemetry.Propagator, so a consumer continues the trace. The caller's map is never changed. EachPublishhas a span of kindPRODUCER, namedsend <exchange>, orsend amq.defaultfor the default exchange. It carries the attributes above, withmessaging.operation.typeandmessaging.operation.namebothsend, andmessaging.destination.nameas semconv's{exchange}:{routing key}. Each attempt recordsmessaging.client.sent.messages, andmessaging.client.operation.durationfrom the publish to its confirm, each witherror.typewhen it failed.Running on
humus.MainThe Service runs on
humus.Main's defaults, a 20s grace period and no drain, which fit a consumer for the reasons the Kafka revision gives. So the package has no App, noMainand no defaults of its own. A consumer next to an API runs on that server package'sMain. Two queues, or twoDeliveryvalues, are two Services returned from oneInit. Like the wave-3 runtimes, the Service serves no health probes. Proposal: exposing health probes, in wave 4, decides how a consumer's pod serves them.Readiness
Nothing routes traffic to a consumer, so its readiness only gates rollouts. A login, vhost or queue the broker refuses already ends the Service, and the pod's crash loop stops the rollout. A wrong address doesn't: the Service keeps retrying, as it does through a broker's upgrade, and without a monitor its pod turns ready and replaces a pod that consumed.
Consumingcloses that gap. In the health Service's readiness, it holds a new pod not ready until it has consumed from its queue once. After that, a lost connection doesn't make it unready: a broker outage hits every pod at once, and turning them all unready would change nothing but block the rollout that might fix it.It needs nothing of humus's but
health.MonitorFunc, which landed in wave 1. Proposal: exposing health probes makes the same choice for the Kafka consumer, withJoined.In use
publisherSettingsis the end user's helper:DefaultPublisherSettings()with the sameConnection.Design decisions
AtMostOnce. The Kafka proposal sketched auto-ack, which is strictly at most once. But RabbitMQ ignores the prefetch count for an auto-ack consumer and sends as fast as TCP allows, and amqp091 buffers every delivery without a bound. A backlog then fills the process's memory, and every crash or rollout loses everything buffered, already acked. A batch acked and confirmed before it is processed is just as strict, and bounds the loss toPrefetch, as Kafka'sMaxPollRecordsdoes.queue/kafkacreates no topics either. A missing queue fails the Service withNOT_FOUND. Open question 2.amqp.Open, notDialConfig. See Connecting. It is what puts the broker on humustest's network in tests, and it keeps credentials out of URLs.DialConfig's URL, and it redeclares topology. The Service's reconnect is a short loop ofretrythat humus tests.Deliveryvalues are.Concurrencyworkers on one channel. One channel keeps one prefetch window and one ack stream. Deliveries are handed to workers in delivery order.os.Args[0].Publishing's zero value is persistent.Transientturns persistence off, so a message isn't lost on a broker restart because a field was left out.ErrUnroutableis exported. A caller branches on it to fall back, for example to a dead-letter store. It is the only error a caller can act on that isn't amqp091's own*amqp.Error.github.com/rabbitmq/amqp091-gov1.15.0, in the nested module, which brings nothing else into the module graph but goleak. bedrock'sretryandcircuitv0.24.0. testcontainers' RabbitMQ module only behind the build tag.Testing
A fake broker on humustest's
HostNo in-memory RabbitMQ exists for Go. garagemq, a broker in Go, listens with
net.ListenTCPand logs through logrus's global logger, stores messages in badger, and has no release since 2024. wabbit's fake is archived, and it fakes a Go interface rather than the wire, so nothing would go through theHost. amqp091's own test server is unexported.So humus writes one:
queue/rabbitmq/rabbitmqtest, a fake AMQP 0-9-1 broker that listens on a humustestHost, as kfake does for Kafka:It covers what the Service and the Publisher use: PLAIN logins, channels,
basic.qos, consumers, acks, nacks and rejects, requeueing on close, heartbeats both ways, the default exchange, direct, fanout and topic exchanges, publisher confirms and mandatory returns. It counts toward a delivery limit as a quorum queue does: rejects and closes, not nacks. Topology is declared by the test, not over AMQP, since the Service declares none. It has no policies, no TTLs, no dead-lettering and no consumer timeout. Its goroutines are per connection and end with it, so it works insidesynctest. No login is fixed:NewBrokergenerates one, and the test hands it to the App withhumustest.WithEnv.The prototype
A prototype of the Service and a consume-only fake broker, in a scratch module against humus at
4289e89a, amqp091-go v1.15.0 and bedrockretryv0.24.0, passesgo test -race -count=3, covering 87.1% of the Service. The fake is about 750 lines: a 200-line codec and a 550-line broker. It has no exchanges, confirms or publishing over AMQP, and the prototype has no telemetry and no Publisher. Its tests run whole Apps withhumustest.Startinsidesynctest, and show:Processsees it, and a processing error doesn't stop the Service.320 CONNECTION_FORCED, the Service reconnects and processes what is published next.NOT_FOUNDor amqp091'sErrCredentialsin the cause.humustest.NewCA, over a TLS listener on theHost, is consumed as over plaintext.humustest.TestService, including the leak check, so amqp091's reader, heartbeater and buffer goroutines end withServe. The broker is closed by anOnStopfunction, and, in the other tests, byt.Cleanup, which runs inside the bubble.A test of the end user's
Init, sketched onrabbitmqtest:The stories add what the prototype leaves out:
NewServiceandNewPublishernaming every bad field, andDefaultSettingsandDefaultPublisherSettingspassing them with the required fields set;humustest.Telemetry;Concurrencyabove 1 processes deliveries at once, and 1 keeps their order;Consumingis false before the broker's firstbasic.consume-ok, and while the address is unreachable, true after it, through a reconnect, and false onceServehas returned;Processexits 1 with a*humus.GracePeriodError;ErrUnroutablewithout a retry, a retry after the broker closes the connection, the breaker opening after 5 failures in a row and logging at WARN, and aPublishcut short by its context;rabbitmqtest's own tests, and a runnableExample.Container tests
Tests against RabbitMQ in a container, behind the
testcontainersbuild tag, run in a CI job of their own, asqueue/kafka's do. They don't count toward the coverage gate. They check what the fake can only claim:basic.qosreply follows the ack before it;320 CONNECTION_FORCEDfrom a broker that stops.Migration from today
humus has no RabbitMQ package to migrate from. For a consumer written on amqp091-go directly:
amqp.Dial(os.Getenv("AMQP_URL"))Settings.Connection, sourced inInit, dialled throughs.Hostch.Qos(n, 0, false)Settings.Prefetchch.Consume(q, "", autoAck, ...)and a loop over the deliveriesSettings.Queue,Settings.Delivery, and aqueue.Processor[rabbitmq.Message]d.Ack(false),d.Nack(false, true)Deliverysaysconn.NotifyCloseand a reconnect loopch.PublishWithContextPublisher.Publish, with confirms, returns, retries and a breakeramqp.SetLogger, OTel's globalss.TelemetryTwo things change for such a consumer: a processing error under at-least-once stops the App, and auto-ack is no longer how at-most-once is done.
Open questions
How are RabbitMQ consumers tested?
rabbitmqtest, a fake broker humus writes and keeps. Every App that uses RabbitMQ runs whole insynctest, as the charter's principle 6 requires. Fidelity is humus's to keep, and the container tests catch where it drifts.synctest, every test starts a broker, CI needs Docker, and the coverage gate can't count them.Recommendation: (a), with container tests in their own CI job, as for Kafka.
Topology.
NOT_FOUND, asqueue/kafkacreates no topics.Settingsdeclare the queue and its bindings. That grows into RabbitMQ's whole topology API: queue types, arguments, exchanges, dead-lettering.Declare func(context.Context, *amqp.Channel) errorfield, called on each connection before consuming. It gives full control, at the price of putting amqp091'sChannelin humus's API.Recommendation: (a).
A lost connection.
queue/kafkakeeps retrying. A RabbitMQ rolling upgrade closes every connection with320 CONNECTION_FORCED.Servereturns the error and the pod restarts. That is louder and has no reconnect code, but every consumer pod restarts on every broker upgrade, with Kubernetes' crash-loop back-off of up to 5 minutes once restarts repeat.Recommendation: (a).
The Publisher.
s.Host.Dial, without humus's telemetry, retries or breaker, and wave 5's failure injection has no RabbitMQ client to add its point to.Recommendation: (a).
Out of scope
Processor's own store. The contract has no way to dead-letter a message on purpose.basic.get, consumer priorities and arguments, and direct reply-to.Addris one address, such as a DNS name or a load balancer in front of the cluster.connection.update-secret). A token is aPlainAuthpassword, read at each connection.Process, and the Publisher's injection point: Proposal: failure injection (wave 5).What the stories change outside the module:
.github/workflows/build.yamlgets a "RabbitMQ (testcontainers)" job, which isn't a required check, and a step that vets the tagged tests..claude/backlog.jsonneeds nothing:.github/scripts/go-modules.shfinds the new module..golangci.yamlgets forbidigo's rule against amqp091'sSetLogger.queuepackage doc namesqueue/rabbitmqbesidequeue/kafka, and the testing page coversrabbitmqtest. The root package doc's "Being rebuilt" says RabbitMQ has landed. The lifecycle page's ambient leaks gain amqp091's package-level logger.References
queueandqueue/kafkaas landed; queue/kafka: a fenced-out member's late commit can move the new owner's committed offset #757OpenandDialConfig, the heartbeater,Qos,Consume, the consumer bufferretryandcircuitv0.24.0All reactions