Changelog
Snake
All version change logs. Format based on Keep a Changelog.
[0.0.5] - 2026-08-26
Added
- Automatic WorkerID derivation now logs the derived workerID and its source IP at startup for collision troubleshooting
Changed
- On millisecond rollover the sequence now starts from a random offset instead of 0, so replicas whose auto-derived WorkerIDs collide no longer emit duplicate IDs within the same millisecond
Fixed
Generatornow packs(timestamp, sequence)into a single 64-bit atomic word and CASes it as a whole, eliminating the in-process duplicate-ID race where the old two-step atomics let the timestamp advance before the sequence was resetValidate()now rejects aSequenceBitswhose packed state(timestamp << SequenceBits)would overflow the 63-bit atomic word used by the newGenerator
[0.0.4] - 2026-06-19
Added
Validate()now checks bit width: returns an error whenWorkerIDBits + SequenceBitsexceeds 63 to avoid timestamp field overflow- Added full unit tests with 97.8% coverage (concurrent uniqueness, clock backward, sequence overflow, bit-width boundaries, etc.)
- Added CI workflow (
.github/workflows/ci-snake.yml) integrating Codecov coverage reporting
Improved
- Removed two pieces of dead code in
CalculateWorkerID(fnv.Writenever returns an error; the modulo operation guarantees workerID stays within the valid range)
[0.0.3] - 2026-06-04
Dependencies
github.com/zeromicro/go-zerov1.10.0 → v1.10.2go.opentelemetry.io/otel*v1.24.0 → v1.40.0 (indirect; intentionally not upgraded to v1.44+ to avoid forcing go 1.25)- Synced
go mod tidyto clean up unused indirect dependencies godirective remains 1.24.0
[0.0.2] - 2026-03-20
- Upgraded Go version to 1.24.0
- Upgraded go-zero to v1.10.0
- Updated dependencies
[0.0.1] - 2026-01-26
- Initial project release
Cron
All version change logs. Format based on Keep a Changelog.
[0.1.2] - 2026-08-06
Changed
- Scheduled task deduplication moved from a fixed
TaskIDtoasynq.Unique: the previous approach used the task type as a fixedTaskID, while asynq decides an ID conflict purely by whether the task key exists — and the lifetime of that key is decided by how the previous run ended (deleted on success, kept for the retention window whenasynq.Retentionis passed, 90 days by default once archived, the run duration while still active). None of those equals the schedule period.CronAddnow injectsasynq.Unique(TTL)instead: underneath it is a plain RedisSET NX EXlock whose key is derived from the queue name and the task type, and whose TTL is a relative duration that lines up with the period by construction.- The TTL is derived from the spec and the effective timeout:
TTL = max(shortest trigger interval − 1s, effective timeout, 1s); callers do not have to provide it TTL ≥ effective timeoutguarantees that no second copy can be enqueued while the previous run is still going;TTL < shortest trigger interval(when the timeout is smaller than the interval) guarantees that a single failure blocks less than one period- The effective timeout is read back from
asynq.Timeoutinopts; when it is absent, asynq's 30 minute default is assumed - Behaviour change: the overlap protection window goes from "as long as the previous task exists" (up to 90 days) down to a single TTL. When the timeout is not smaller than the trigger interval the lock outlives the next tick and that tick is skipped, which is exactly what "a run may take longer than the period" has to mean
- An explicitly passed
asynq.Uniqueis overridden
- The TTL is derived from the spec and the effective timeout:
Fixed
- A single failed run no longer stops a scheduled task forever: archived tasks no longer take part in the deduplication decision, and the dedup lock carries its own bounded expiry (asynq does not clear that lock when it archives a task), so a failure blocks for at most one TTL and the next tick recovers
- Enqueue failures are no longer silent:
SchedulerOpts.PostEnqueueFunconly counted successful enqueues andEnqueueErrorHandlerwas not set, so asynq returned without a single log line or metric when a tick failed to enqueue. Failures are now logged and counted CronAddno longer writes into the spare capacity of the caller'sopts: the queue and dedup options used to be appended straight onto the incomingopts, which reaches into the caller's backing array whenever the caller passesopts...and that slice has spare capacity. The options are now copied before anything is appended. Plain variadic calls (including theworkers.gogenerated bycztctl) havecap == lenand were never affected
Added
- Metric
cron_server_scheduler_enqueue_error_total{task_type, reason}: number of failed enqueues at tick time.reason=duplicateis the normal outcome of a multi-replica deployment (replicas−1 times per tick) and is only recorded as a debug log;reason=other(for example Redis being unavailable) is reported loudly and is the only entry point for detecting such failures
Dependencies
github.com/zeromicro/go-zerov1.10.2 → v1.10.3github.com/redis/go-redis/v9v9.20.0 → v9.21.0 (raised by the minimum requirement of go-zero v1.10.3)github.com/robfig/cron/v3v3.0.1 promoted from indirect to direct: used to parse the spec and derive the shortest trigger interval, with the same parser asynq's Scheduler uses internallygithub.com/alicebob/miniredis/v2v2.38.0 promoted from indirect to direct: used directly by the unit testsgo mod tidydropped 22 indirect dependencies that are no longer needed (grpc, grpc-gateway, otel exporters, pyroscope, zipkin and others)- Intentionally kept
go.opentelemetry.io/otel*at v1.40.0 (v1.44+ forces go 1.25; not upgraded for compatibility) godirective remains 1.24.0
[0.1.1] - 2026-06-04
Dependencies
github.com/zeromicro/go-zerov1.10.0 → v1.10.2github.com/redis/go-redis/v9v9.17.3 → v9.20.0- Synced
go mod tidyto clean up unused indirect dependencies - Intentionally kept
go.opentelemetry.io/otel*at v1.40.0 (v1.44+ forces go 1.25; not upgraded for compatibility) godirective remains 1.24.0
[0.1.0] - 2026-06-04
Breaking Changes
CronAddsignature adjustment: added required parameterhandler HandlerFunc; completes scheduler registration and handler registration in one step, avoiding repeating the same task type in bothCronAddandAdd- Old usage:
CronAdd(spec, pattern, opts...)+Add(pattern, handler) - New usage:
CronAdd(spec, pattern, handler, opts...)
- Old usage:
- Scheduled task timeout now managed by go-zero
RestConf.Timeout: before v0.1.0, scheduled tasks did not inject a timeout and fell back to the asynq default hard-coded constant of 30 minutes; from v0.1.0, it is recommended to injectserverCtx.Config.Timeout(go-zerorest.RestConf.Timeout) intoCronAddviaasynq.Timeout(d)inworkers.go, so that thectxreceived by the handler carries aDeadline, unified with go-zero HTTP timeout management- The cztctl template will be updated in subsequent versions; when upgrading, business teams need to manually adjust
workers.go. See the migration guide for details
- The cztctl template will be updated in subsequent versions; when upgrading, business teams need to manually adjust
Migration Guide
API Signature Migration
- serverCtx.CronServer.CronAdd("*/1 * * * *", "GDemoB")
- serverCtx.CronServer.Add("GDemoB", demoA.GDemoBHandler(serverCtx))
+ serverCtx.CronServer.CronAdd("*/1 * * * *", "GDemoB", demoA.GDemoBHandler(serverCtx))Integrate go-zero Timeout Management
+ import (
+ "time"
+ "github.com/hibiken/asynq"
+ )
+
func RegisterHandlers(server *service.ServiceGroup, serverCtx *svc.ServiceContext) {
+ var taskOpts []asynq.Option
+ taskOpts = append(taskOpts,
+ asynq.Timeout(time.Duration(serverCtx.Config.Timeout)*time.Millisecond),
+ asynq.MaxRetry(0),
+ )
- serverCtx.CronServer.CronAdd("*/1 * * * *", "GDemoB", demoA.GDemoBHandler(serverCtx))
+ serverCtx.CronServer.CronAdd("*/1 * * * *", "GDemoB",
+ demoA.GDemoBHandler(serverCtx), taskOpts...)
server.Add(serverCtx.CronServer)
}
rest.RestConf.Timeoutdefaults to3000ms, which is short for IO-intensive scheduled tasks; it is recommended to explicitly increase it in yaml (e.g.,Timeout: 30000).
Added
CronAdd/Client.Push*can set an arbitrary timeout per task via theasynq.Timeout(d)opt (not limited by the asynq default 30 minutes), e.g.,CronAdd("*/1 * * * *", "X", h, asynq.Timeout(time.Hour))- README advanced guide added the "Task Timeout Control" section, detailing the timeout injection mechanism for scheduled tasks and pushed tasks
Fixed
ExponentialRetryDelayformula corrected to2^(n+1) - 1: thenpassed by asynq is the number of retries already performed (0-indexed); the original formula2^n - 1returned 0s on the first retry, causing no backoff on the first retry; after correction the retry sequence is stable at1s, 3s, 7s, 15s, 31s, ...
[0.0.9] - 2026-06-04
Added
- Added
SetBaseContext(ctx context.Context)method, supporting injection of a base context; all task handler ctxs will use this as parent
Changed
asynq.Serveris created lazily atStart(), supporting dynamic context configuration after construction and before startup
[0.0.8] - 2026-06-04
Added
- Added
GetTaskID,GetRetryCount,GetMaxRetry,GetQueueNamewrapper functions; the handler layer can obtain task metadata without directly depending on asynq
Changed
- README.md fully refactored: full examples organized by go-zero / standalone scripts + Server / Client quadrants
- Advanced guide added task group aggregation and skip retry usage examples
- Added
asynq.SkipRetrywrapping notes (pkg/errorsincompatible) - Metrics section heading levels corrected
[0.0.7] - 2026-06-04
Added
- Added
WithGroupAggregatorServerOption, supporting injection of a task group aggregator to make Group grouping available - Added
WithRetryDelayFuncServerOption, supporting custom retry delay strategy - Added built-in retry strategy
ExponentialRetryDelay(2^n - 1seconds: 1s, 3s, 7s, 15s, 31s...) CronAddautomatically usesrealPatternas TaskID so scheduled tasks are not duplicated across multi-instance deployments
Fixed
- Removed dead code validation for
GroupGracePeriodinServerConfig.Validate()(int64 cannot be >0 and <1)
Changed
GroupMaxDelaydefault changed from 0 (unlimited) to 300 seconds (5 minutes), preventing tasks from never aggregating when they keep arriving- Server default retry strategy changed from asynq built-in
n⁴exponential backoff toExponentialRetryDelay(2^n - 1seconds) Addlogs unified to[CRON] Worker registered: %s
[0.0.6] - 2026-05-20
Changed
- Replaced
log.Printfwithlogx.Errorfinmetrics.goto unify with go-zero logging conventions - Removed
LoggingMiddlewareto avoid duplication with asynq internal error logs - Removed redundant
logc.ErrorfinRecoveryMiddleware; asynqperformalready has built-in panic recovery logging
[0.0.5] - 2026-05-15
Added
- Added full metrics system
- Server side:
cron_server_consume_total,cron_server_consume_duration_ms,cron_server_consume_bytes,cron_server_active_workers,cron_server_retry_total,cron_server_skip_retry_total,cron_server_panic_total,queue_groups,tasks_aggregating_total,cron_scheduler_trigger_total,cron_scheduler_registered,cron_tasks_enqueued_total,cron_queue_size,cron_queue_latency_seconds - Client side:
cron_client_push_total,cron_client_push_duration_ms,cron_client_push_bytes,cron_client_cancel_total
- Server side:
- Added queue whitelist filtering to avoid metric mixing when multiple services share Redis
- Client defaults to no retries (MaxRetry=0); users can override via opts
Changed
- Metrics namespace changed from
asynq_tocron_ - Server-side metrics added
serversubsystem
[0.0.4] - 2026-04-10
Changed
- Upgraded Go version to 1.24.0
- Upgraded go-zero to v1.10.0
- Updated dependencies
ConfigCenter (Consul)
All notable changes to this project are recorded here. Format based on Keep a Changelog.
[0.1.2] - 2026-06-04
Dependencies
github.com/zeromicro/go-zerov1.10.0 → v1.10.2- Run
go mod tidyto clean up unused indirect dependencies - Pin
github.com/hashicorp/consul/apito v1.25.1 (v1.33+ requires go 1.25; pinned for go 1.24 compatibility) godirective kept at 1.24.0
[0.1.1] - 2026-03-20
- Upgrade Go version to 1.24.0
- Upgrade go-zero to v1.10.0
- Update dependency versions
[0.1.0] - 2025-11-26
- Initial release
RegisterCenter (Consul)
All notable changes to this project are recorded here. Format based on Keep a Changelog.
[0.1.7] - 2026-06-04
Dependencies
github.com/zeromicro/go-zerov1.10.0 → v1.10.2google.golang.org/grpcv1.65.0 → v1.80.0go.opentelemetry.io/otel/tracev1.24.0 → v1.40.0 (indirect)- Run
go mod tidyto clean up unused indirect dependencies - Pin
github.com/hashicorp/consul/apito v1.25.1 (v1.33+ requires go 1.25; pinned for go 1.24 compatibility) godirective kept at 1.24.0
[0.1.6] - 2026-03-20
- Upgrade Go version to 1.24.0
- Upgrade go-zero to v1.10.0
- Update dependency versions
[0.1.5] - 2025-12-01
- Optimization release
[0.1.3] - 2025-12-01
- Optimization release
[0.1.2] - 2025-12-01
- Optimization release
[0.1.1] - 2025-12-01
- Optimization release
[0.1.0] - 2025-12-01
- Initial release
RabbitMQ
All version change logs. Format based on Keep a Changelog.
[0.2.0] - 2026-08-03
A whole-module redesign. 0.1.6 was never released and its content is folded into this version.
Every new configuration field has a default, so existing configurations in your config center need no changes to upgrade.
Breaking Changes
1. Interceptors unified into middleware
Interceptor / SenderInterceptor / SenderFunc / SenderChain are removed in favour of a single Meta / Handler / Middleware / Chain set:
-type Interceptor func(ctx context.Context, queueName string, message []byte, next func(context.Context, []byte) error) error
-type SenderInterceptor func(ctx context.Context, exchange, routeKey string, msg []byte, next SenderFunc) error
+type Handler func(ctx context.Context, meta Meta, body []byte) error
+type Middleware func(next Handler) HandlerNote: Chain and SenderChain were logically identical and had been copied only because their signatures differed. Once Meta describes the message coordinates uniformly (the consumer side fills Queue, the publisher side Exchange and RouteKey), the two isomorphic sets collapse into one. There is no third-party user, so the breakage is limited to custom interceptors.
WithConsumeMiddlewares / WithPublishMiddlewares are added as injection points: the interceptor chain used to be hard-coded inside the constructor with no way for callers to append.
2. Send blocks for the confirm, and errors are classified into five sentinels
err := sender.Send(ctx, exchange, routeKey, body)
switch {
case errors.Is(err, rabbitmq.ErrPublishUnroutable): // wrong routeKey or missing binding
case errors.Is(err, rabbitmq.ErrPublishNacked): // broker refused; already retried internally
case errors.Is(err, rabbitmq.ErrPublishTimeout): // unknown whether the broker got it
case errors.Is(err, rabbitmq.ErrPublishUnknown): // channel closed before the confirm arrived; also unknown
case errors.Is(err, rabbitmq.ErrSenderClosed): // the sender is closed
}Note: previously PublishWithContext returning nil only meant the frame was written to the socket, so the [RABBITMQ_SEND_OK] log did not hold. Now nil means the broker has accepted the message. The cost is higher latency and lower throughput, compensated by PoolSize.
The sentinels are cut along a single line: whether the outcome is known. ErrPublishNacked and ErrPublishUnroutable both carry a complete response from the broker, so the framework can act on them — the former is retried with backoff, the latter is not, because the result would be identical. ErrPublishTimeout and ErrPublishUnknown never got a confirm, and the message may or may not have landed; neither is retried, because retrying would duplicate the message behind the caller's back and only the business knows whether that is acceptable. ErrPublishUnknown is kept distinct from ErrPublishTimeout despite sharing that fate, because the two point in opposite directions when investigating: a timeout points at PublishTimeout and broker load, a closed channel points at network and connection stability. Folding them together would send a rising timeout metric chasing the wrong fix.
3. Publishing defaults to persistent + mandatory
Note: the old implementation never set DeliveryMode, while QueueConf.Durable defaults to true — the classic half-durable trap: after a broker restart the durable queue survives but every message is gone. And mandatory: false let a mistyped routeKey be silently dropped by the broker while the confirm still returned an ack. A routeKey matching no queue now returns ErrPublishUnroutable instead of silently succeeding.
4. ConsumerConf.NoLocal, ChannelQosConf.Global and RabbitListenerConf.ContentType removed
ChannelQos:
PrefetchCount: 10
- Global: false
ListenerQueues:
- Name: demoA
- NoLocal: trueNote: RabbitMQ never implemented no-local; the broker silently ignores the flag. Keeping it was in fact more dangerous: the application believes "consuming my own published messages is disabled" while it still receives them. Global was equally illusory: the flag only scopes the prefetch allowance within one channel (per-consumer vs. shared across the channel) and never across channels, so with one dedicated channel and exactly one consumer per queue, both values resolved to the same allowance — a knob that looked like it tuned flow control but could not. Qos now always passes false. ContentType was a dead field in the consumer config. After removal the extra yaml keys are ignored, so existing configurations are unaffected.
5. Admin gains NewAdmin and Close
-admin := rabbitmq.MustNewAdmin(conf)
+admin, err := rabbitmq.NewAdmin(conf)
+if err != nil {
+ return err
+}
+defer admin.Close()Note: the old implementation had no Close, leaking one TCP connection per call, and log.Fatalf-ed when opening the channel failed, leaving the connection open. MustNewAdmin is kept.
6. MustNewListener returns the module's own Listener interface
-func MustNewListener(c RabbitListenerConf, handler ConsumeHandler) queue.MessageQueue
+func MustNewListener(c RabbitListenerConf, handler ConsumeHandler, opts ...ListenerOption) ListenerNote: Listener's method set (Start() / Stop()) is identical to go-zero's queue.MessageQueue, so it can still be handed straight to service.ServiceGroup and neither listeners.go nor ServiceContext needs a change. The point is to drop the dependency on a go-zero type from the public signature: this module uses nothing else from the queue package, and exposing that type suggests go-zero queue semantics that are not there. NewListener is added alongside, matching NewSender — the old implementation offered only the Must variant, so a configuration error could surface only as a panic.
7. The trace context moves from the message envelope to AMQP headers
RabbitMsgBody is removed; Meta gains Headers:
-type RabbitMsgBody struct {
- Carrier *propagation.HeaderCarrier // OpenTelemetry trace headers
- Msg []byte // business payload
-}
+type Meta struct {
+ Queue string
+ Exchange string
+ RouteKey string
+ Headers map[string]any // the trace context travels here
+}The publisher injects the trace context into Meta.Headers, which ends up as amqp.Publishing.Headers; the consumer extracts it from amqp.Delivery.Headers. Neither end touches the body any more, so rabbitmq_listener_parse_error_total goes away with the envelope — nothing is left that could fail to parse.
Note: jamming the trace context into the body is what kept this module from interoperating with anything but itself. Carrying it in the header table instead — the way the OpenTelemetry messaging convention prescribes for RabbitMQ — settles four things at once: messages published by other systems become consumable (see the corresponding fix below), the body stops paying for an extra layer of JSON plus the 33% base64 inflation encoding/json applies to a []byte field, traces genuinely continue across language boundaries, and message bodies are legible in the management UI.
This is a wire-format change. A 0.2.0 consumer does not unwrap a 0.1.x envelope: it hands the envelope JSON to the handler as-is. Publishers and consumers sharing a queue therefore need to be upgraded together, and the queue should be drained of envelope-shaped messages first.
8. Connection metrics gain endpoint and vhost labels
rabbitmq_sender_reconnect_total, rabbitmq_sender_disconnect_total, rabbitmq_listener_reconnect_total and rabbitmq_listener_disconnect_total carried no labels at all; they now carry endpoint (host:port) and vhost. The two pool metrics introduced in this version (rabbitmq_sender_pool_wait_duration_ms, rabbitmq_sender_pool_in_use) carry the same pair.
Why: one process can hold several Senders and Listeners pointed at different clusters, and a label-less counter piles them all onto one curve — when an alert fires there is no telling which broker is flapping. vhost falls back to the AMQP default / when unset, so one vhost written two ways does not split into two series. Neither label carries credentials: metrics get scraped into a monitoring system and kept for a long time.
This changes the cardinality of existing series. Dashboards and alert rules querying those four metrics need an aggregation added (sum without (endpoint, vhost) (...) or equivalent), otherwise what used to be a single curve becomes one curve per broker.
Added
- Per-queue consume concurrency:
ConsumerConf.Concurrency(default 1, capped at 256). 1 means strict ordering; greater than 1 starts that many workers processing in parallel. RequiresConcurrency <= ChannelQos.PrefetchCount - Processing timeout:
ConsumerConf.Timeout(default 0, unlimited) and theWithConsumeTimeout(d)option. The old middleware chain rooted at a barecontext.Background()with no deadline anywhere. When hosted by go-zero, inject the host'sRestConf.Timeoutexplicitly in the handler - TLS:
RabbitConf.TLS, one-way and mutual authentication. The old implementation hard-codedScheme: "amqp" - Heartbeat:
RabbitConf.Heartbeat(default 30 seconds), to detect dead links and trigger reconnection - Publish tuning:
RabbitSenderConfgainsPoolSize(10) /PublishTimeout(5000) /MaxRetry(3) - Graceful shutdown bound:
RabbitListenerConf.GracefulTimeout(default 10000 ms) - Publish-side graceful shutdown bound:
RabbitSenderConf.GracefulTimeout(default 10000 ms), symmetric with the listener-side field — the boundClosewaits for in-flight publishes to be confirmed by the broker - Middleware injection:
WithConsumeMiddlewares(...)on the consumer side andWithPublishMiddlewares(...)on the publisher side, both appended after the built-in chain and therefore bounded by the timeout. The publisher side sits outside the retry orchestration, so it runs exactly once perSendrather than once per attempt - Publisher-side option:
WithPublishTimeout(d), symmetric withWithConsumeTimeout(d), letting the host framework inject a timeout uniformly; it takes precedence overRabbitSenderConf.PublishTimeout - Five metrics:
rabbitmq_sender_confirm_duration_ms,rabbitmq_sender_pool_wait_duration_ms,rabbitmq_sender_pool_in_use,rabbitmq_sender_retry_total,rabbitmq_listener_in_flight - Tests and CI: unit tests (all
Validatebranches,Chaincomposition order, the timeout middleware, state machine transitions, channel pool checkout/return), integration tests (//go:build integration, real broker), and.github/workflows/ci-rabbitmq.yml. This module used to be the only one without CI
Fixed
- Concurrent
Sendsharing one channel:amqp091-go'sChannelis not thread-safe, yet every concurrent publish shared a single channel, so interleaved protocol frames got the channel closed by the broker. Publishing now goes through a channel pool where each channel handles a single in-flight message at a time —NotifyReturncarries no deliveryTag, which is also the prerequisite for attributing an unroutable return correctly - Data race on the sender's
conn/channel:Sendreadq.channelandreconnectwroteq.channel = nilwith no lock at all, allowing a data race and a nil dereference panic. Both are now guarded by a RWMutex, with the publish path taking only the read lock - Sender reconnection could not be interrupted: it used a fixed 10 × 2s busy wait — a second reconnection philosophy alongside the listener's exponential backoff — and could not be interrupted by a shutdown signal. Both now use interruptible exponential backoff
Adminleaked connections: see breaking change 5 above- WaitGroup cross-generation reuse panic risk:
listenerWg/taskWgare now allocated per generation. Previously, once the reconnect wait timed out, the waiter stayed blocked inWait()while the new generation calledAddon the same wg — when the counter hit zero this could triggersync: WaitGroup misuseand crash the process (trigger: stuck handler + connection flap) Stopworst-case duration doubled: the "wait for old routines" step in reconnect now includes the shutdown signal in its select. It previously used a baretime.After, so a shutdown arriving mid-reconnect had to wait out reconnect'sGracefulTimeoutbefore waiting outStop's own — easily exceeding the shutdown grace period- Messages from other publishers were silently dropped:
consumerTraceunmarshalled every body as an envelope unconditionally, and a parse failure returned an error — which the "acknowledge anything that has been executed" rule then turned into an ack. A message published by a Java service, or by hand from the management UI, was thus discarded without the handler ever running. The body is no longer parsed at all - A panic in the middleware chain itself stalled the queue silently:
handle's panic safety net sat outside the acknowledgement decision, so a panic skipped the acknowledgement altogether — the delivery was neither Acked nor Rejected and held on to a prefetch slot forever. AfterPrefetchCountsuch messages the queue stopped receiving entirely, with nothing but the panic in the logs and no signal of the stall, until a reconnect or shutdown released it. The exposure is not narrow: business middleware injected viaWithConsumeMiddlewaressits outsideconsumerRecovery, so any user middleware panic took this path. The safety net moved one level down (invoke) and turns the panic into an error in place, so the acknowledgement always runs — consistent with the "acknowledge anything that has been executed" rule ErrPublishUnknownsaid nothing about why: a closed confirm channel was reported as "outcome unknown" with no cause attached, yet the most common configuration error of all — a mistyped exchange name — arrives exactly this way, as a404 NOT_FOUNDthe broker delivers on the channel-close notification alone.pubChannelnow registersNotifyCloseand appends the broker's code and reason to the error, so the situation that most needs diagnosing is no longer the least diagnosable- Shutdown turned in-flight publishes into
ErrPublishUnknown:Closetore the connection down without waiting, closing the confirm channel of every publish that had already written its frames — and the shutdown hook is registered byNewSenderitself, so every process exit manufactured a batch of unknown outcomes.Closenow waits outGracefulTimeoutfor in-flight publishes to be confirmed before destroying channels, and logs one[RABBITMQ_STOP]line if any are still unconfirmed when the window elapses SendafterClosewaited out the publish deadline: channel checkout selected only on a free slot and the caller's ctx, so once the pool was closed — slots collected and never returned — the publish sat there untilPublishTimeoutand then reported a misleading timeout. Checkout now also selects on the pool's shutdown signal and returnsErrSenderClosedimmediately
Changed
Validateunified into three rules, exactly one applied per field: a missing required field or an impossible value returns an error; an invalid value that has a safe default falls back and logs one line; exceeding a protocol or resource limit returns an error. The old implementation mixed "fail" and "silently fall back" arbitrarily- The first connection is attempted only once: configuration errors should surface immediately at startup, while runtime connection failures are healed by reconnection. Sender and Listener now agree on this
- A configured timeout is a backstop that applies only when the caller supplied no deadline:
timeoutMiddlewarenow checks whether the incomingctxalready carries a deadline and passes it through untouched if so, instead of wrappingcontext.WithTimeoutunconditionally. The latter takes the earlier of the parent deadline and the configured value, silently truncating a longer deadline the caller explicitly asked for and turning the configuration from a backstop into a hard ceiling. The consumer side is unaffected because its message ctx is derived by the Listener fromcontext.Background()and carries no deadline;Sendis what changes — if the caller's ctx already has a deadline (inside a go-zero rest handler, say),PublishTimeoutno longer intervenes - Files split by responsibility:
conf.go/errors.go/const.go/message.go/middleware.go/metrics.go/conn.go/consumer.go/listener.go/chanpool.go/sender.go/admin.go. The oldRabbitListenerwas a single struct carrying five responsibilities (connection management, channel lifecycle, generation orchestration, message dispatch, acknowledgement decisions) with 4 synchronisation primitives and 3 atomics among its 16 fields - The listener lifecycle is now an explicit state machine:
stateNew → stateRunning → stateStopping → stateStopped, with generations expressed by layered contexts. State checks used to be scattered across 7closed.Load()and 4epoch.Load()call sites
Dependencies
github.com/rabbitmq/amqp091-gov1.11.0 → v1.13.0go.opentelemetry.io/otel/sdkpromoted from an indirect to a direct dependency: the trace-propagation round-trip test drives a realTracerProvidergodirective remains 1.24.0example/is now its own module (example/go.mod, with areplacepointing back at../): the example depends on the config centre and the registry, and while it shared this module, runninggo mod tidyin the library directory wrote the consul and etcd dependency trees into this module's go.mod. Now neithergo mod tidynorgo build ./...in the library directory touches the example- go.mod and go.sum trimmed: indirect requirements drop from 42 to 20, go.sum from 139 to 85 lines. What went is 0.1.x residue that nothing imports any more (gRPC, the Zipkin and OTLP exporters, genproto, pyroscope, gonum, k8s.io/utils and friends). The number of transitive dependencies that actually take part in a build is unchanged (281 packages) — the residue existed only as declarations in go.mod, harmless to the build itself, but enough to make dependency audits and SBOMs count a pile of unrelated packages as this library's surface
Documentation
- README and readme-cn rewritten around the new model, covering publish semantics, consume semantics, timeouts, middleware, the metric catalogue, and the upgrade guide
- Clarified that
PrefetchCountis not a concurrency setting: it is a flow-control parameter (driven by message size and network RTT), while concurrency is set byConcurrency(driven by downstream capacity) - Clarified the cost of at-least-once: retrying inevitably duplicates messages, so consumers must be idempotent
- Clarified that deadlines are cooperative: the handler must pass ctx to downstream blocking calls or the timeout has no effect
- Clarified the difference in how timeouts apply, caused by the consumer and publisher sides obtaining their ctx from different places
Deviations from the design document
Six decisions during implementation differ from the original design proposal:
- Recovery sits innermost in the middleware chain, whereas the design document places it outermost. Outermost, a panic would pass through metrics and logging, so a panicking message would neither be counted in
consume_totalnor produce a single log line — and a panic is precisely what most needs to be observed rabbitmq_listener_worker_in_useis not implemented: the value is derivable fromrabbitmq_listener_in_flightdivided byConcurrency, a separate gauge would only add two more atomic writes to the tail of every message, and two metrics with the same source contradict each other once they drift- No separate
inflightWgwas introduced: the per-generationtaskWgalready expresses "wait for in-flight messages to finish", and a second waitgroup would blur exactly what shutdown is waiting for — the hardest part of the old implementation to read - The middleware injection point was split into a symmetric pair: the design document offered only
WithMiddlewares(consumer side only). The publisher side needs an injection point too, and a single name cannot say which side it applies to; the two also differ in execution semantics (the publisher side sits outside the retry orchestration and runs once perSend), which sharing one name would hide - The cztctl template was left untouched (design document §8, "cztctl integration"): cztctl's handler template is served from a remote repository, so the in-repo
.tplis not the one actually used at generation time and editing it here would have no effect. Host timeout injection is documented as a snippet for the caller to add explicitly in the handler instead - The trace context is carried in AMQP headers, not in a message envelope: the design document keeps the envelope (
message.go's responsibilities, and both trace middlewares, are described in terms of wrapping and unwrapping it). The envelope turned every consumed message into a JSON parse, and a body that was not an envelope got acknowledged away without ever reaching the handler. Its supposed cost — "envelope JSON parsing is not bounded by the deadline, which is acceptable because parsing is pure CPU and bounded" — is moot now that no parsing happens
Migration
- No change needed: existing config-center configuration,
listeners.go(a generated file),ServiceContext - Change needed: custom interceptors, if any;
Sendcallers that need to branch by error type;MustNewAdmincall sites need adefer admin.Close(); publishers and consumers sharing a queue must be upgraded together, because the trace context moved out of the message body (breaking change 7); dashboards and alert rules querying the reconnect / disconnect metrics need an aggregation added (breaking change 8) - Recommended: inject the host timeout into existing handlers, i.e. add the
rabbitmq.WithConsumeTimeout(...)line at theMustNewListenercall by hand (and"time"to the imports). cztctl's handler template is served from a remote repository and is out of this module's scope; besides, handler files are skipped if they already exist during generation, so existing services have to add it by hand either way - Caution:
rest.RestConf.Timeoutdefaults to only 3000ms. Once injected, message processing is implicitly capped at 3 seconds, so consumers usually need to raiseTimeoutexplicitly in the yaml
[0.1.5] - 2026-06-04
Dependencies
github.com/rabbitmq/amqp091-gov1.10.0 → v1.11.0github.com/zeromicro/go-zerov1.10.0 → v1.10.2go.opentelemetry.io/otel*v1.24.0 → v1.40.0 (intentionally not upgraded to v1.44+ to avoid forcing go 1.25)- Synced
go mod tidyto clean up unused indirect dependencies godirective remains 1.24.0
[0.1.4] - 2026-03-20
Breaking Changes
1. MustNewSender Removed the ctx Parameter
- rabbitmq.MustNewSender(ctx, conf)
+ rabbitmq.MustNewSender(conf)Description: Consistent with MustNewListener; ctx is not required during initialization
[0.1.3] - 2026-03-20
New Features
1. Connection Reliability Enhancements
- NotifyClose Listener: Both Sender and Listener listen for Connection and Channel close events
- Auto Reconnect: Automatically attempts to reconnect after disconnection (up to 10 times)
- Thread Safety: Uses
atomic.Boolto ensure thread safety of theclosedflag - Double-Checked Locking: Avoids duplicate reconnections (pre-lock check + post-lock check)
- Graceful Shutdown: Stops reconnecting after receiving the go-zero proc stop signal
2. Sender Graceful Shutdown
- Automatically registers
proc.AddShutdownListenershutdown hook - Marks
closedon shutdown, preventing further reconnections
3. Greatly Expanded Metrics
Sender Metrics (5):
| Metric Name | Type | Labels | Description |
|---|---|---|---|
rabbitmq_sender_send_total | Counter | exchange, route_key, status | Total sends |
rabbitmq_sender_send_duration_ms | Histogram | exchange, route_key | Send duration |
rabbitmq_sender_send_size_bytes | Histogram | exchange, route_key | Message size |
rabbitmq_sender_reconnect_total | Counter | - | Number of reconnections |
rabbitmq_sender_disconnect_total | Counter | - | Number of disconnections |
Listener Metrics (9):
| Metric Name | Type | Labels | Description |
|---|---|---|---|
rabbitmq_listener_consume_total | Counter | queue, status | Total consumed |
rabbitmq_listener_consume_duration_ms | Histogram | queue | Consumption duration |
rabbitmq_listener_consume_size_bytes | Histogram | queue | Message size |
rabbitmq_listener_in_flight | Gauge | queue | Messages currently being processed |
rabbitmq_listener_parse_error_total | Counter | queue | Number of parse failures |
rabbitmq_listener_panic_total | Counter | queue | Number of panics |
rabbitmq_listener_ack_total | Counter | queue, type | ACK/Reject count |
rabbitmq_listener_reconnect_total | Counter | - | Number of reconnections |
rabbitmq_listener_disconnect_total | Counter | - | Number of disconnections |
4. Interceptor Optimization
- Order adjusted to:
recovery → prometheus → logging → trace - Ensures all interceptors execute even if trace parsing fails
5. Other Improvements
StartConsumerSpanadds carrier nil protection- Message parsing moved to traceInterceptor to avoid duplicate parsing
- ACK/Reject logic unified to the outer processMessage layer
Removed
- Removed
client.go - Removed
parseMessagemethod (message parsing moved to traceInterceptor) - Removed
requeueMessageduplicate consumption logic
1. Metric Name Changes
- mq_consume_total
+ rabbitmq_listener_consume_total
- mq_consume_duration_ms
+ rabbitmq_listener_consume_duration_msImpact: Prometheus queries and Grafana dashboards need to be updated
2. client.go Removed
- client, err := rabbitmq.NewClient(ctx, conf)
+ sender, err := rabbitmq.NewSender(conf)
// or
+ sender := rabbitmq.MustNewSender(conf)3. Sender Auto-Registers Shutdown Hook
NewSenderandMustNewSenderautomatically registerproc.AddShutdownListener- In the go-zero environment, there is no need to call
Close()manually - Outside the go-zero environment,
Close()still needs to be called manually
4. MustNewListener Removed the ctx Parameter
- rabbitmq.MustNewListener(ctx, conf, handler)
+ rabbitmq.MustNewListener(conf, handler)Description: The consumer internally derives from context.Background(); the externally passed ctx has no practical effect
Aliyun Gateway
All version change records. Format based on Keep a Changelog.
[0.0.3] - 2026-06-14
Fixed
MultipartBuilder.FileignoringCreateFormFileerror could cause nil panic; changed to error accumulation modeMultipartBuilder.Build()signature changed to(contentType string, body []byte, err error), uniformly returning build errors
Improved
- Replaced
uuid.New().String()withuuid.NewString()to reduce intermediate UUID struct allocation
[0.0.2] - 2026-06-14
Improved
sortedQuerynow usesstrings.IndexBytefor zero-allocation iteration, eliminatingstrings.Split[]stringallocationsortedQueryeliminates redundantresultslice, usesstrings.Builderfor direct output; map/slice pre-allocated with capacitysignOptionleveragesGetBodyfor form/multipart body to skip full buffering, avoiding unnecessary memory copies during large file uploads[]byte(AppSecret)is pre-computed once outside the closure atNewClienttime, no longer converted on every signaturestrings.Join(signHeaders)promoted to package-levelsignHeadersValue, computed only once
Fixed
signRequestnow shares a singletime.Now()call for Date and Timestamp, eliminating the risk of inconsistency across millisecond boundaries
[0.0.1] - 2026-06-13
Added
- Alibaba Cloud API Gateway Go client, built on go-zero httpc, with automatic HMAC-SHA256 v1 signing
Domethod: structured requests withpath/form/json/headertag auto-mappingDoRawmethod: raw byte requests for file uploads, XML, plain text, and custom bodyWithClientoption: inject a custom*http.Client(TLS, connection pool, etc.)Parsefunction: wraps go-zerohttpc.Parse, accepts(resp, err)fromDo/DoRaw, auto-parses JSON response and closes BodyNewMultipart()/MultipartBuilder: chain-build multipart/form-data request bodyConf.Validate()automatically strips trailing/from Host- Built-in go-zero circuit breaker integration, auto-shared per Host
Kong HMAC Auth
All version change records. Format based on Keep a Changelog.
[0.0.1] - 2026-06-18
Added
- Kong HMAC Auth Go client, built on go-zero httpc, with automatic HMAC signing, following the official Kong HMAC Auth plugin specification
Domethod: structured requests withpath/form/json/headertag auto-mappingDoRawmethod: raw byte requests for file uploads, XML, plain text, and custom bodyWithClientoption: inject a custom*http.Client(TLS, connection pool, etc.)Parsefunction: wraps go-zerohttpc.Parse, accepts(resp, err)fromDo/DoRaw, auto-parses JSON response and closes Body- Support for 5 HMAC algorithms:
hmac-sha1,hmac-sha224(Kong 3.14+),hmac-sha256,hmac-sha384,hmac-sha512 @request-targetpseudo-header support, value ismethod /path?query(method in lowercase)Conf.Headersallows custom header list for signing, defaults to["date", "@request-target"]Digestheader auto-computation (SHA-256=base64(sha256(body))), supports empty body zero-length digest, form / multipart skippedDateheader auto-injection (GMT format), used for Kong clock skew validationUser-Agentheader auto-injection (Go-Kong-HmacAuth-Client), can be overridden by the callerHostheader injection and signing value uniformly user.Host, ensuring consistency between signed and actually sent valuesConf.AlgorithmandConf.Headerssupport go-zerooptional/defaulttags, can be omitted in YAML configuration- Built-in go-zero circuit breaker, tracing, and metrics integration, auto-shared per Host
cztctl
All notable changes to this project are recorded here. Format based on Keep a Changelog.
[1.10.11] - 2026-08-03
Changed
- BREAKING
cztctl api rabbitmqnow generates handlers and logic againstczt-contrib/mq/rabbitmqv0.2.0+. Code generated by this version does not compile against earlier releases of the library- The handler closure and the logic constructor both take a new
meta rabbitmq.Metaparameter. Business code readsl.meta.Queueto tell which queue a message came from, which is what makes a single handler able to serve multiple queues - The handler now always injects
rabbitmq.WithConsumeTimeout(time.Duration(svcCtx.Config.Timeout)*time.Millisecond), so MQ consumption and HTTP requests share one deadline fromRestConf.Timeout. Note this option outranks the per-queueListenerQueues[].Timeoutin the config file, which therefore has no effect in generated services - When the DSL declares a message body type (e.g.
order.created(GOrderOrderCreatedReq)), the handler does thejson.Unmarshaland the logic method receives*types.XxxReq. A route with no declared type (e.g.payment.success) now generates a logic method that takes no parameter at all: its signature goes fromfunc(req []byte) errortofunc() error, so business code that used to read the raw bytes has to declare a type in the DSL
- The handler closure and the logic constructor both take a new
- Upgraded
github.com/zeromicro/go-zeroto v1.10.3 andgithub.com/zeromicro/go-zero/tools/goctlto v1.10.2api/swaggerbuilds on goctl'sapi/specandpkg/parser/api/parser; neitherspec.Typenorparser.Parsechanged shape in this release, so no call site needed adapting- The
--versionandcztctl envsample outputs in the README were refreshed to match (1.10.11,go-zero v1.10.3)
Fixed
- Fixed generated code failing to compile when the DSL declared a message body type:
internal/typeswas imported by both the handler and the logic but never referenced, tripping Go's unused-import checkinternal/typesis now imported only when the route actually declares a request type
- Fixed parser error messages being mangled when the offending DSL text contained a
%: the joined text was passed tofmt.Errorfas a format string, turningFoo%sBarintoFoo%!s(MISSING)Bar. It now goes througherrors.Newand is reported verbatim (errorManager.error,Parser.CheckErrors)- This also clears the
non-constant format string in call to fmt.Errorffinding that Go 1.24's vet reports as an error, which had been failinggo vet ./...andgo test ./...
- This also clears the
[1.10.10] - 2026-06-18
Fixed
- Fixed an issue in
cztctl rpc sdkwhere generating.kong.protofrom a CRLF (Windows) line-ending proto file caused rpc line semicolons to not be removed and annotations to be misaligned- Root cause:
strings.Split(data, "\n")left trailing\rat the end of each line, causingTrimSuffix(";")to match\rinstead of;, preserving the semicolon and shifting{ - Fix: Normalize
\r\n/\rto\nat read time to eliminate\rinterference on suffix matching at the source
- Root cause:
Changed
rpcMethodReregex now uses thereturnskeyword to identify rpc method declarations, which is more semantic than the original\(bracket matching (a line withreturnsis a true rpc method)
[1.10.9] - 2026-06-18
Fixed
- Fixed an issue in
cztctl rpc sdkwhere generating.kong.protowith an rpc line using an empty{}block caused annotation misalignment- The original check
strings.Contains(line, "{")incorrectly treated{}as an already-opened brace, causing theoption (google.api.http)annotation to float outside the block with an extra closing brace - Fixed by first stripping the closing
}of{}, then usingHasSuffix("{")for detection, covering all three rpc line styles: semicolon / open brace / empty block
- The original check
[1.10.8] - 2026-06-16
Added
cztctl rpc sdkautomatically generates.kong.protofiles (Kong gRPC-gateway HTTP annotations)- Automatically generates a
.kong.protovariant with the same name as the proto in the_sdk/directory whencztctl rpc sdkis executed - Automatically adds
import "google/api/annotations.proto" - Generates
option (google.api.http)annotations for each rpc method - Path rule:
/{ServiceName}/{RpcMethodName} - HTTP method is uniformly POST with
body: "*" - Skips inserting the annotations import if the proto already contains it
- Automatically generates a
[1.10.7] - 2026-06-04
Changed
cztctl api crongenerator adapted to the new API of cron v0.1.0CronAddunified signature: pass the handler directly at call time; registration + scheduling are completed internally, no additionalAddcall required- Scheduled tasks now default-inject
asynq.Timeout(time.Duration(serverCtx.Config.Timeout) * time.Millisecond); timeout is managed by go-zeroRestConf.Timeout - When there are multiple scheduled tasks, a
timeoutOptlocal variable is extracted to avoid repeating the timeout expression MaxRetryis still configured per-task viaCronRetry- Non-scheduled tasks (external dispatch consumers) remain unchanged, registering only the handler
Dependencies
github.com/zeromicro/go-zerov1.10.1 → v1.10.2github.com/zeromicro/go-zero/tools/goctlstays at v1.10.1 (upstream has not released v1.10.2)
[1.10.6] - 2026-05-27
Added
cztctl rpc sdk— automatically generate RPC client code as a standalone Go module and publish it to a Git repository- Automatically clone/initialize the SDK repository with HTTPS + Token authentication support
- Recursively parse proto import dependencies and automatically copy all related proto files
- Call goctl to generate client code; clean up server-side code to keep only
client/ - Automatically run
go mod tidyto tidy dependencies - SemVer version management: auto-increment patch, auto-carry minor when patch reaches 99
- Automatic Git commit, tagging, and push (branch + tag)
- Support
--remote/--branchgoctl remote template pass-through - Support
--multiplemulti-service mode - Support
--goproxycustom proxy - Support
--repo-branchto specify the SDK repository branch (defaultmain) - goctl version pre-check (not installed is a hard error; version too low is a soft warning)
[1.10.5] - 2026-04-09
Fixed
- Fixed an issue in
cztctl api cronwhere generating workers.go included a handler argument inCronAddincorrectly- Scheduled tasks (
@cron) now correctly generate two lines:Add(pattern, handler)to register the handler, followed byCronAdd(cronExpr, pattern, opts...)to register the schedule - Pure async tasks (without
@cron) still generate onlyAdd(pattern, handler)
- Scheduled tasks (
- Fixed a go.sum checksum mismatch for goctl causing
go installto fail
Changed
- Upgrade goctl dependency v1.10.0 → v1.10.1
- Upgrade go-zero dependency v1.10.0 → v1.10.1
[1.10.3] - 2026-04-07
Added
.cronroute names support-(hyphen) and:(colon) separators, e.g.sync-order,email:send.rabbitmqroute names support-(hyphen) separator, e.g.payment-refund,order.pay-callback- Route name separators are semantically isolated by file type:
.crononly allows-:,.rabbitmqonly allows.-
[1.10.2] - 2026-03-22
Added
cztctl api swagger— generate Swagger 2.0 documentation from a .api file- Full info block property mapping (title / description / version / host / basePath / schemes, etc.)
- Support @server annotations (tags / summary / prefix / group / deprecated / operationId / authType)
- Support automatic
validatetag parameter constraint annotations - Support multi-line field header comment parsing
- Support
useDefinitionsmode ($refreferences) - Support
wrapCodeMsgresponse wrapping - Support both JSON and YAML output formats
cztctl api cron— generate a distributed cron job service from a .cron file- Based on the czt-contrib/cron framework
- Support internal scheduled tasks (
@cronexpression +@cronRetryretries) - Support externally triggered tasks (no
@cron, triggered via asynq.Client.Enqueue) - Generate complete directory structure: etc / config / handler / logic / svc / types / worker / main
- Support
@docin both string and KV forms - Support
@servergrouping (group / tags / summary / middleware) - Support
--remote/--branchremote templates - Support
--stylefile naming style (gozero / go_zero / goZero)
cztctl api rabbitmq— generate a RabbitMQ consumer service from a .rabbitmq file- Based on the czt-contrib/mq/rabbitmq framework
- Support dot-separated queue names (e.g.
order.created,payment.refund.success) - Support optional message parameter types
- Generate complete directory structure: etc / config / handler / logic / svc / types / listener / main
- Support
--remote/--branchremote templates - Support
--stylefile naming style
cztctl env— environment variable management- View current environment variables (CZTCTL_OS / CZTCTL_ARCH / CZTCTL_HOME / CZTCTL_CACHE / CZTCTL_VERSION)
- Edit environment variables (
cztctl env -w KEY=VALUE) CZTCTL_EXPERIMENTALtoggle:offuses ANTLR4 parser,onuses the handwritten recursive-descent parser
- DSL syntax parsing
.cronand.rabbitmqshare the base syntax: syntax / info / import / type / @server- ANTLR4 parser (default) + handwritten recursive-descent parser (experimental)
- Full type system support: primitive types, slices, maps, pointers, nested structs, struct tags
- Support cross-file type references via import
- Versioning rule:
v<go-zero-major-version>.<micro-version>(currently based on go-zero v1.10.0)