Repository navigation
Replies: 2 comments
Revision 1: the open questions answeredThe open questions:
Each section below replaces or adds to the part of the same name in the post. Retries and the breaker, Shutdown, Refused credentials, The seam for failure injection, Changes to the rest of the module, Migration from today and References are unchanged. SummaryThe second bullet becomes:
BackgroundThese are added under "franz-go's produce path". They were read in franz-go v1.22.2's source, not prototyped:
Proposed API
// Produce produces records, and returns nil once the cluster has
// acknowledged every one. Records to one partition are stored in the order
// they are given.
//
// It returns an error, naming each record that failed by its topic, when a
// record failed or ProducerSettings.DeliveryTimeout passed; an error that
// matches circuit.ErrRejected, at once and with nothing sent, while the
// breaker is open; and an error that wraps ctx's error, at once, when ctx is
// done first. A call whose ctx is already done sends nothing. Records handed
// to the client before ctx is done go on to their result, within
// DeliveryTimeout, and never fail another call's records. A record that
// failed may still have been stored, so a caller that produces it again may
// store it twice.
//
// Produce rejects a record with no Topic before it sends anything. With no
// records, it returns nil.
func (p *Producer) Produce(ctx context.Context, records ...Record) errorProducingIn A call, step 1 becomes:
and step 5 becomes:
Records outlive a cancelled call becomes: Records outlive a cancelled call. The records don't follow
One This is added to DeliveryTimeout bounds what is in flight: When SettingsThe bullet on the connection fields becomes:
TelemetryThis is added after the paragraph on the two instruments: A call whose Design decisionsThe bullets "Synchronous only" and "Records outlive a cancelled call" become:
These are added:
TestingThese bullets are added:
Open questionsAll three are answered above. None are left. Out of scopeThis is added:
|
Closing: the Kafka Producer has landedThe revision's stories are done:
The revision left no open questions. 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: the humus App lifecycle, Proposal: configuration for humus apps, Proposal: OpenTelemetry without globals, Proposal: humustest, Proposal: humus.Main and Proposal: queue processing and the Kafka runtime, whose module, connection settings, TLS dialer and header carrier it uses. It depends on no other wave-4 proposal. It is discussed beside Proposal: outbound HTTP clients and Proposal: outbound gRPC clients, and keeps to the three points the charter's revision 7 sets for the clients. It blocks Proposal: failure injection (wave 5), which adds an injection point at the seam named below.
Summary
queue/kafkagets a producer, next to its consumer:kafka.Producerproduces records to one Kafka cluster. The end user'sInitbuilds it withNewProducer, fromProducerSettings, and any Service uses it: aProcessorthat writes to another topic or to a dead-letter topic, an HTTP handler that publishes an event, a job. It is not a Service. It registers its flush and close withs.OnStop, so it outlives every Service that uses it.Produce(ctx, records...)is synchronous. It returns once the cluster has acknowledged every record, with acks from all in-sync replicas, or once one has failed. AProcessorthat produces before it returnsnilhas the consumed record committed only after the produced one is stored.Brokers,TLSandSASLmean what they mean inSettings, with the same checks, and the client dials throughs.Hostwith the consumer's TLS dialer.retry. franz-go's idempotent producer retries a batch on the sequence numbers that let the broker drop duplicates, and keeps each partition in order. A retry around it would send the records again under new sequence numbers.DeliveryTimeout, 10s by default, bounds each call, retries included.circuitbreaker guards the cluster. One perProducer. While the cluster keeps failing,Producefails at once with an error matchingcircuit.ErrRejected, rather than holding every caller for the delivery timeout.PRODUCERspan,send <topic>, whose context goes into the record's headers through the Setup's propagator, so the consumer'sprocessspan is its child.messaging.client.sent.messagesandmessaging.client.operation.durationare recorded, kotel's produce counters fill in, and each breaker transition is a log record.Background
doc.go#L189).kgo.NewClientand calledProduceSyncthemselves, on the real network and with no telemetry (example/queue/kafka-at-least-once/tool/main.go).Processor's.queue's contract and the Kafka page leave them to the end user: aProcessorthat would rather skip a record than stop on it "stores the record wherever it keeps dead letters, such as a table or a topic of its own, and returnsnil". Under at-least-once that store has to finish beforeProcessreturns. The walkthroughs (wave 6) rely on it: their tests treat the group's committed offset as a barrier after which the dead-letter topic holds every dead letter.Brokers,TLSandSASLwith their checks (settings.go#L31-L63); the unexportedtlsDialer, which runs the TLS handshake over theHost's connection because franz-go refuseskgo.Dialerbesidekgo.DialTLSConfig(dial.go#L25); and the unexportedheaderCarrier, which the Service extracts through and a producer injects through (carrier.go#L16). "How a producer's settings share these fields, and whether they move into a type of their own, is that proposal's decision."retryandcircuitship the models with no I/O, and leave the protocol wiring, the instruments and the settings to humus (circuit, retry).retry's doc says it doesn't replay requests, which "belong to the framework that wraps a transport".v0.0.0-20261013022759-7b6cac30a57a, insidesynctest.Teston a humustestHost, in a prototype that isn't kept:acks=all. It retries a partition's batch without limit, on the same sequence numbers, so the broker drops a batch it already stored, and the partition's order holds.ProduceSyncwhose context ran out after 2s, against a broker holding its produce request, was still blocked 5 minutes later.kgo.AllowIdempotentProduceCancellation, the same call returned at 2s withcontext.DeadlineExceeded. The broker stored the record once it was released, so the caller was told of a failure for a record that landed.kgo.RecordDeliveryTimeoutis checked only at safe points. With a 10s timeout and a broker holding its request, the record failed after 20s: the 10s produce timeout plus the 10s request overhead. A record context with a 10s deadline, against a broker answering every produce with the retriableNOT_ENOUGH_REPLICAS, failed each call at exactly 10s.Flush(ctx)returnsctx's error whenctxis done first, andClosethen fails what is left withkgo.ErrClientClosed.Proposed API
The
Produceris in packagekafka, in thequeue/kafkamodule. The one new dependency is bedrock'scircuitmodule,github.com/z5labs/bedrock/circuitv0.24.0, which has no dependency outside the standard library.Producing
The client.
NewProducerbuilds one franz-go client withkgo.SeedBrokers(settings.Brokers...),kgo.Dialerof the consumer'stlsDialerovers.Host.Dialandsettings.TLS,kgo.SASLwhensettings.SASLis set, the consumer's franz-go logger, kotel'sMeteras hooks, a connect hook described under Refused credentials, andkgo.AllowIdempotentProduceCancellation(). Every other producer option keeps franz-go's default, among them:RequiredAcksAllISRAcks()RecordRetriesRetryBackoffFnRecordPartitionerUniformBytesPartitioner(64KiB, adaptive, keys, murmur2)ProducerLingerProduceSync, whichProduceuses, stops lingering on the partitions it produced toProducerBatchCompressionProducerBatchMaxBytesmax.message.bytes; a larger record fails at once withMESSAGE_TOO_LARGEMaxBufferedRecordsProducewaits for room, or forctxA call.
Produce:Topic, and returns at once, with nothing sent, when the client was closed or the cluster refused the credentials;ctx: one that keepsctx's values, isn't cancelled with it, and is done afterDeliveryTimeout;ctx's baggage into the record's headers through the Setup's propagator, after removing any header the propagator writes;ProduceSyncdoes, and waits for every record's result or forctx, whichever comes first.Records outlive a cancelled call. When
ctxis done first,Producereturns an error that wrapsctx's error, at once. Its records stay in the client, with the call's context, until the cluster acknowledges them orDeliveryTimeoutfails them, and their spans and measurements are recorded then. So a handler whose request is cancelled returns at once, and the grace period reaches aProcessblocked inProduce, as the lifecycle requires. And one caller's cancellation never fails another's records: franz-go fails every buffered record of a partition when it fails one, and decides from the context of a batch's first record, so records handed over with their callers' contexts would fail with whichever caller gave up first.DeliveryTimeoutbounds what is in flight.kgo.AllowIdempotentProduceCancellationlets the call's context fail a record that was sent and not answered. Without it, as the prototype showed, a hung broker holds a call, and the App's shutdown, until it answers. With it, a record can fail and still be stored. A caller that produces it again may store it twice, which is at-least-once, and is what aProcessorunderqueue.AtLeastOncealready handles. franz-go bumps its producer epoch before producing again, so its own sequence numbers stay valid.Order. franz-go keeps each partition's records in the order it was handed them, across its retries. Within one call that is the order of
records. Between calls on different goroutines it is the order they reach the client. When a record of a partition fails, franz-go fails the records buffered after it on that partition too, so a later record is never stored after an earlier one failed. A caller that produces a failed call again sends its records after any that other calls have produced since.Errors. When every record was acknowledged,
Producereturns nil. Otherwise it returns one error joining each failed record's error, with its topic, and its partition when one was chosen, soerrors.Isfindskgo.ErrRecordTimeout,context.DeadlineExceeded,circuit.ErrRejectedor akerrcode in it. A failed call doesn't say which of its records were stored. Kafka returns no partial result a caller could act on safely, and the records that failed may have been stored anyway.Settings
Brokershost:portCheckBrokersTLSnil, plaintextSASLnil, no authenticationDeliveryTimeout10sCheckDeliveryTimeoutBreaker.Window20circuit.MaxCountCheckBreakerWindowBreaker.Probes1circuit.MaxCountCheckBreakerProbesBreaker.OpenFor10sCheckBreakerOpenForBreaker.ProbeTimeout10sCheckBreakerProbeTimeoutBreakerPolicyTripandProbesetNewProducerBrokers,TLSandSASLare the consumer's fields, repeated. They mean the same, take the same check, and dial through the same unexported dialer. A process that consumes and produces on one cluster sources them once and copies the three intoProducerSettings.Settingsis unchanged, so no Init breaks. Open question 2 asks whether they should move into a type of their own instead.Breakeris bedrock's owncircuit.Config, with a check for each of its fields, so an operator's badOpenForfails startup naming the variable it came from, as #631 requires.NewProduceralso passes it tocircuit.New, which rejects the same values.BreakerPolicyis acircuit.Policy, set in code. A policy is two functions, so it can't come from a variable, asSASLcan't. bedrock ships no policy, because "how many failures are too many is the caller's decision", so humus ships its default inDefaultProducerSettingsand the end user replaces it in their Init. A policy whoseTripalways returns false turns the breaker off.MaxAttempts, backoff orretry.BudgetConfig, because franz-go retries. See Design decisions.Sourced in the end user's Init, with the consumer's variables reused for the cluster:
envandlistare the end user's helpers, as on the Kafka page.Retries and the breaker
franz-go's retries, and no
retry.Retrier. The charter's rule for the clients is that each picks the library's retries or bedrock's, never both. For Kafka it is franz-go's:Producewould hand the records to the client again, under new sequence numbers, and a record whose first send landed would be stored twice.Producesends its records after whatever other calls produced in the meantime.retry.Budgetexists to stop doesn't happen. There is no budget.DeliveryTimeoutis the one retry setting: how long a call lets franz-go keep trying.What the breaker guards. The dependency is the cluster: one
Produceris one client to one cluster, with one breaker, built inNewProducer. A process that produces to two clusters builds two Producers. Each call toProduceis one call through the breaker, admitted before any of its records reach franz-go and settled once all of them have a result, which may be afterProducehas returned. The ticket is handed off to the records' results, so the guard is the flagcircuit's doc shows for a result settled after the function returns.While the breaker is open,
Producefails at once with a*circuit.RejectedError, which says how much longer it stays open. Without the breaker, every call during an outage would wait outDeliveryTimeout: an HTTP handler that publishes an event would hold its request for 10s before answering 503. With it, the first few calls wait, and the rest fail at once until a probe gets through.Producedoesn't retry a rejected call. Whether to wait and try again is the caller's, asqueue's contract leaves retries to theProcessor.What counts. The breaker's classifier sees each call's result with the call's own context, which a caller giving up doesn't cancel:
MESSAGE_TOO_LARGE,INVALID_RECORDorTOPIC_AUTHORIZATION_FAILED, orUNKNOWN_TOPIC_OR_PARTITIONonce franz-go has given up on the topic. One topic's mistake doesn't open the breaker for the others.DeliveryTimeoutpassing,kgo.ErrRecordTimeout, a retriable error code franz-go stopped on, or a connection that failed.The defaults follow the charter's starting values for the clients.
ProbeTimeoutmatchesDeliveryTimeout: a probe's result comes within the delivery timeout, so a probe holds its slot no longer than the call does.A partition's leader going down. One breaker covers every partition. When one broker of three is down, the calls to the partitions it leads fail and the rest succeed, which the default policy may or may not trip on. franz-go moves unkeyed records away from backed-up partitions, and a keyed record has only one partition. A breaker per broker or per partition would follow Kafka's failures more closely, but franz-go picks the partition inside the client, after the breaker has admitted the call.
Shutdown
The
Produceris not a Service, so the lifecycle doesn't stop it. The Services that use it stop first. The App calls theirShutdown, waits for everyServeto return, and only then runs theOnStopfunctions, so no Service is producing when theProducercloses.NewProducerregisters one function withs.OnStop. It callsFlushwith the grace period's context, which waits for the records still in the client: those of a call that returned when itsctxwas done, at mostDeliveryTimeoutold. Then it callsClose. When the grace period runs out first,Closefails what is left withkgo.ErrClientClosed, those records are lost, and the function returns an error that wraps the context's error, which the lifecycle reports as a*humus.GracePeriodError. AProduceafter that, from a goroutine the end user left running, fails with an error wrappingkgo.ErrClientClosed.DeliveryTimeout's default fits the budgets the runtime packages set. It is half ofhumus.Main's 20s grace period, which a consumer runs on, so aProcessblocked inProduceat SIGTERM finishes inside it. It equalshttpserver.Main's 10s grace period, and a handler'sProduceends sooner anyway when the grace period cancels the request.If the end user's Init fails after
NewProducer, the lifecycle runs theOnStopfunctions registered so far, and the client is closed. Building the client starts franz-go's goroutines, and closing it stops them, so nothing outlives the App.Telemetry
The
Producertakes everything froms.Telemetry, under the module's scope,github.com/z5labs/humus/queue/kafka, and never reads or sets OpenTelemetry's globals.A span per record. Named
send <topic>, of kindPRODUCER, a child of the span inProduce'sctx. Its attributes aremessaging.system=kafka,messaging.operation.type=send,messaging.operation.name=sendandmessaging.destination.name, and, once the record is acknowledged,messaging.destination.partition.idandmessaging.kafka.offset. The key is not recorded, as the consumer doesn't record it. It ends when the record is acknowledged or fails, which may be afterProducereturns. A failure, a rejection by the breaker included, is recorded on it as an exception event, witherror.typeand an error status.The span's context goes into the record's headers. The Setup's propagator injects it, with
ctx's baggage, throughheaderCarrier. With the default propagator that istraceparent,tracestateandbaggage. Before injecting, theProducerremoves every header whose key is one of the propagator'sFields(). A dead letter that copies the consumed record's headers would otherwise carry twotraceparentheaders, and the carrier'sGetreturns the first, the old one. So the consumer'sprocessspan on any topic is a child of thesendspan that produced its record. Inside aProcess, thesendspan is a child of theprocessspan, and one trace runs from the first producer through every topic a record passes through.Two instruments of the messaging conventions, as semconv v1.43.0 names them, the version the consumer pins:
messaging.client.sent.messagescounts each record once its result is known, withmessaging.operation.name=send, anderror.typewhen it failed;messaging.client.operation.durationis how long each record took, fromProducetaking it to its result, in seconds, withmessaging.operation.name=send,messaging.operation.type=send, anderror.typewhen it failed.Each also has
messaging.system,messaging.destination.nameand, once known,messaging.destination.partition.id. A record the breaker rejected, or thatProducerejected for having no topic, was never sent, so neither instrument counts it. The conventions say a sent-messages count "MUST NOT count messages that were created but haven't yet been sent". Its span records the rejection.kotel's produce counters fill in. The
Producerbuilds kotel'sMeterwith the Setup'sMeterProvider, as the consumer does.messaging.kafka.produce_records.countandmessaging.kafka.produce_bytes.count, by broker and topic, now count, next to the connection counters by broker. The consumer's doc and the Kafka page change where they say those counters stay empty.Logs. franz-go's logs go through the consumer's adapter, under
github.com/twmb/franz-go/pkg/kgo, so a retried produce request and a broker it can't reach are logged as the consumer's are. TheProducer's own records are:circuit breaker openat WARN, andcircuit breaker half-openandcircuit breaker closedat INFO, the bodies the other two client proposals use. Its attributes aremessaging.system=kafka,circuit.previous_state, and the counts of the state it left:circuit.successes,circuit.failures,circuit.consecutive_successes,circuit.consecutive_failuresandcircuit.timed_out;There are no instruments of humus's own for the breaker. Its transitions are in the logs, and a rejected call is an error on its span.
Refused credentials
franz-go retries a connection whose credentials a broker refuses, and the produce waits until its context ends with no sign of why. That is the consumer's problem too, and its Service stops on the first refusal (
service.go#L437). AProducercan't stop the App, so it fails loudly instead. The same connect hook watches forSASL_AUTHENTICATION_FAILED,UNSUPPORTED_SASL_MECHANISMandILLEGAL_SASL_STATE. On the first refusal, theProduceremits one ERROR record naming the broker, fails the calls in flight with the refusal, and from then on everyProducereturns it at once, before the breaker, so the breaker doesn't count it. Credentials don't fix themselves while the process runs: aProcessorthat produces fails its record, and the consumer stops and is restarted with whatever the secret holds then.The seam for failure injection
Proposal: failure injection (wave 5) adds an injection point to
Produce. Its seam is step 3 of a call: once each record's span has started and its headers carry the trace context and the baggage, and before the call goes through the breaker. An injected abort there fails the records' spans like a real failure. Whether it counts against the breaker, and the point's name, are wave 5's to decide, and its stories add them here.Scenarios cross Kafka with no further change. The
Producerinjectsctx's baggage into the record's headers, and the consumer extracts it beforeProcess. Stripping scenarios from records produced to a cluster another organisation owns happens at the same step, and is wave 5's too.Changes to the rest of the module
This proposal's stories make them:
doc.go's first sentence says the package consumes and produces, and its paragraph on kotel's produce counters changes. A section, Producing, describes theProducer.headerCarrieris unchanged. TheProducerremoves the propagator's fields from a record's headers itself, beforeInject.Producer.go.modaddsgithub.com/z5labs/bedrock/circuitv0.24.0.queue/kafkaconsumes Kafka topics, say it produces too.Processorproducing a dead letter with theProducer.Design decisions
kafka. The module already has franz-go, the dialer and the carrier, and a sub-package would need those exported. AProducerbeside theServicereads askafka.NewProducerandkafka.NewService.Producer, not a Service. Producing has no loop of its own to serve. It is called from the Services that do, so it lives as long as they do, and closes inOnStop, after them. The charter sets the same rule for every client.Producereturns, which is what at-least-once needs: aProcessorthat produced before returningnilnever commits a record whose output was lost. franz-go's promise, the asynchronous alternative, runs on the client's own goroutine, one at a time, and blocks the client when it blocks. A caller that wants not to wait runsProducein a goroutine of its own. Open question 1.Recordof its own.Messagehas a partition, an offset and a timestamp, which a producer doesn't set.Headeris shared, so a dead letter copiesm.Headersas it is. The record's timestamp is the time it is produced, which franz-go sets.acks=alland idempotence are not settings. They are franz-go's and Kafka's defaults since Kafka 3.0, and turning either off trades durability for latency in a way that is invisible until a broker fails.ConsumeResetOffsetand the others.DeliveryTimeoutis a context on the call, notkgo.RecordDeliveryTimeout. franz-go checks its own timeout only at safe points, which in the prototype made a 10s timeout take 20s. The call's context fails the records when it says.Producercan't stop the App, so it says so on every call.Produce's caller is the caller's.circuit. It is bedrock's, with no dependency of its own, and the charter assigns it to the clients.retryisn't used, so it isn't added.Testing
These tests are sketches and have not been run, apart from the franz-go behaviour listed in Background, which the prototype checked. Every test runs inside a
testing/synctestbubble, on humustest'sHostandStart, with a kfake cluster built in the bubble withkfake.ListenFn(h.Listen), and asserts withtestify/require. kfake runs in the bubble, as the consumer's tests already show. The helperscluster,committedandsettingsare the consumer's test helpers.readAllis a franz-go client that dialsh.Dialand polls a topic to its end.spanNamedfinds a recorded span by name.A dead letter is stored before the consumed record is committed, and the trace runs through it:
The breaker opens after five failed calls, and the sixth fails at once:
Producereturns when its context is done, and the record is flushed inOnStop:The tests also show the following:
DeliveryTimeoutbounds a call against a hung broker. With the broker holding every produce request and no deadline onctx,Producefails after exactlyDeliveryTimeout, not the 20s franz-go's own timeout took.crypto/rand.Textlike the right one, the firstProducefails with an error matchingkerr.SaslAuthenticationFailed, naming the broker, rather than its deadline. One ERROR record is emitted, and a second call fails at once.ProducerBatchMaxBytesfail withMESSAGE_TOO_LARGE, and an eleventh, valid call succeeds.OpenFor, three successful probes close it, with an INFO record for half-open and one for closed.messaging.client.sent.messagesandmessaging.client.operation.durationare recorded per record with their attributes, and witherror.typefor a failed one. A rejected record is in neither. kotel'smessaging.kafka.produce_records.countcounts the records by broker and topic.HUMUS_SHUTDOWN_GRACE_PERIOD=2s, theOnStopflush is cut short, the record fails withkgo.ErrClientClosed, and the App exits 1 with a*humus.GracePeriodError.Initfailing afterNewProducer. The client is closed by itsOnStopfunction, and synctest finds none of franz-go's goroutines left when the bubble ends.NewProducernames every bad field together.DefaultProducerSettings()withBrokersset passes. The package still reads no environment and no file, and the hygiene tests cover the new files.answerProduce,holdProduce,readAll,spanNamedandlogsWithBodyare a few lines each in the tests, like the consumer'sanswerOffsetCommit.Against Kafka. The container tests of #731, in their own CI job, add one test: a
Producerproduces toapache/kafka-nativewith idempotence andacks=all, and the consumer's Service consumes the records back, in order, each with itsprocessspan the child of itssendspan. kfake accepts idempotent produce requests, but the producer ID, the sequence numbers and the epoch bump after a cancelled record are worth one run against the real broker.The package's example becomes a consumer that produces dead letters, and the module stays under the 80% coverage gate.
Migration from today
kgo.NewClientandProduceSynckafka.NewProducer(s, settings)andProduce(ctx, records...)s.Host.Dial, with the consumer's TLS dialersend <topic>span per record, its context in the headers, the messaging conventions' instruments, kotel's produce countersDeliveryTimeout, behind a circuit breakerkafka.Settingsandkafka.Serviceare unchangedOpen questions
Produce. Keep one synchronous call, or also offer one that takes a callback for each record's result? An asynchronous call produces more per goroutine, but its callback runs on the client's goroutine, where blocking stalls every producer of the process, and aProcessorthat used it could commit a record whose output is later lost. Recommendation: synchronous only. A caller that wants not to wait runsProducein a goroutine of its own.Brokers,TLSandSASLinProducerSettings, as proposed, or move them into a type, sayConnection, thatSettingsandProducerSettingsboth embed? Embedding keepssettings.Brokersworking, but breaks everykafka.Settings{Brokers: …}literal and every message that namesSettings.Brokers, to save the end user three assignments. Recommendation: repeat them.ctxis done first,Producereturns at once and its records stay in the client until they are acknowledged orDeliveryTimeoutpasses, as proposed. The alternative is to hand franz-go the caller's context, so the records fail when the caller gives up. But franz-go fails a batch, and every record buffered behind it on that partition, by the context of the batch's first record, so one caller's cancelled request would fail other callers' records. Recommendation: records outlive the call. Either way, a record that failed may have been stored.Out of scope
queue's contract. AProcessorgets at-least-once end to end by producing before it returnsnil, and handles duplicates downstream as it already does.Producer. franz-go can consume and produce on one client, but the consumer's client lives for oneServeand theProducer's for the whole App. Two clients cost a second set of connections.KeyandValueare bytes, as inMessage.UNKNOWN_TOPIC_OR_PARTITION, unless the cluster creates topics itself.References
Settings,DefaultSettings,Check<Field>telemetry.SDK, and outbound instrumentation left out of scopeOnStopafter every Servicesettings.go,dial.go,carrier.go,service.go,logger.gocircuitandretryat v0.24.0, and their discussions, A circuit breaker should be provided by bedrock bedrock#595 and A retry package owned by bedrock bedrock#606;fitand Feature request: request-scoped failure injection (Netflix FIT) on OpenTelemetry baggage bedrock#578 for wave 5kgoproducer options, among themAllowIdempotentProduceCancellation,RecordDeliveryTimeoutandProduceSync, and kotel v1.7.1'sMeterexample/queue/kafka-at-least-once/tool/main.goAll reactions