Appearance
Client Options
Every option that cyclone.NewClient accepts. Cyclone applies options in order. For a scalar option, the last one wins. NewClient rejects an invalid value at startup, not at first use. A misconfigured client fails at startup.
The how-to pages cover step options, enqueue options, and schedule options: Queues and Priority, Retries, Timeouts, Unique Jobs, Sequences, Retention. The ForEach option WithMaxParallel is on Fan Out Over a List. WithMaxWait on a child node is on Run a Child Workflow.
Concurrency and Connections
| Option | Default | Notes |
|---|---|---|
WithWorkers(n) | 4 | Goroutines that claim work and execute handlers, all on the default queue. |
WithWorkerQueues(map[string]int) | {"default": 4} | Per-queue worker counts. The total fleet size is the sum. That sum sets the txSlots default and the pool budget check. |
WithTxSlots(n) | total workers | Cap on concurrent completion transactions. Lower it to separate handler concurrency from pool size. Cyclone honors it in any option order. |
WithClientID(id) | host/pid/rand | Identifier recorded on claims. Set it so lease ownership is readable in logs. |
WithWorkers and WithWorkerQueues write the same field. They do not compose. The last one wins, in either order:
go
// Ends up as {"default": 8}: WithWorkers came last and replaced the map.
cyclone.NewClient(pool, mux,
cyclone.WithWorkerQueues(map[string]int{"priority": 4, "bulk": 2}),
cyclone.WithWorkers(8),
)NewClient rejects a pool whose MaxConns is below txSlots + 6. It rejects a queue name that fails validation. It rejects a per-queue worker count below 1. See Queue Sizing for the formula.
Timing
| Option | Default | Notes |
|---|---|---|
WithLeaseTTL(d) | 30s | Bounds crash-detection delay, not handler length. A per-step heartbeat renews every TTL/3 for the whole handler. |
WithMaxReclaims(n) | 0, unbounded | Bounds expired-lease reclaims per step. Set it. See below. |
WithPollInterval(min, max) | 100ms, 1s | The correctness floor. max bounds how long committed work can sit unclaimed when a wakeup is lost. |
WithMaxSignalWait(d) | 0, no cap | Default hard cap for WaitSignal nodes that set no WithMaxWait. A non-positive value leaves them uncapped. |
The maximum of WithPollInterval is the recovery bound for every lost wakeup. Size it on purpose. See Wakeup Latency and the Polling Floor.
WithMaxReclaims(n int)
A reclaim after an expired lease charges no attempt, because a crash is not a failed execution. Without a bound, a handler that stops its worker before it completes is reclaimed and started again forever. WithMaxAttempts never ends it.
With n >= 1, the sweep can reclaim a step n times. The next time its lease expires, the recovery sweep discards it instead of reclaiming it again:
error_kindisreclaim_limit(cyclone.ErrorKindReclaimLimit). The sweep charges no attempt.- The sweep emits
EventDiscardedand calls theWithErrorHandlerhook withcyclone.ErrReclaimLimitExceeded. - The run fails unless an
OnDepFailure(CancelBranch)dependent contains the discard, as with any other dead letter.
The count is StepStatus.Reclaims. It accumulates across ordinary retries. An operator Retry or RetryAll resets it. 0, the default, keeps the unbounded behavior. NewClient rejects a negative n.
Set a bound wherever a handler can crash its process. The sweep that finds the expired lease applies its own client's bound. Give every client that calls Start against the database the same value.
Notifications
Cyclone never issues pg_notify inside a core transaction. Transitions collect wakeup hints. The transaction owner publishes them after the commit succeeds. A full notification queue cannot fail a state change. Wakeups are latency hints. Polling is what makes delivery correct.
WithNotify(enabled bool)
Default true. It controls listening only: this client's listener goroutine and the LISTEN path behind cyclone.Wait. With WithNotify(false), workers claim on each poll tick instead of waking immediately.
It does not gate publication. A notify-disabled client still publishes wakeups, including the ones PublishWakeups sends after a TrackWakeups commit. Those wakeups wake listeners in other processes. Disabling notify on a poll-only deployment behind transaction-mode PgBouncer does not reduce publication traffic. See Operating with PgBouncer.
WithNotifyCoalesce(d time.Duration)
The default is 0. That value publishes every wakeup immediately. A non-zero window buffers wakeups in memory. It publishes each window as one batched pg_notify. You trade a bounded wake delay for fewer round trips. You also get less contention on the Postgres global notify lock. Every transaction in the cluster that carries NOTIFY takes that lock, not only yours.
An idle publisher never waits. The first wakeup after a quiet window publishes inline. Only a wakeup that arrives inside an active window is batched.
The window applies to every wakeup that the client publishes. That set is queue wakes from Enqueue, Submit, worker transitions, work that a handler enqueued with EnqueueCompletion, and PublishWakeups. It also includes the run-done notifications that cyclone.Wait listens for.
The option is separate from WithNotify. A producer-only fleet with WithNotify(false) is the shape that benefits. It publishes and does not listen.
Producer-only clients must be shut down
Without a coalesce window, a client that only calls Enqueue or Submit and never calls Start starts no goroutine. Much code therefore skips Shutdown. With a non-zero window, Enqueue can return while a wakeup is still buffered and a timer is still armed. Closing the pool then loses the wakeup and logs a failed publish. The work still executes. It waits out the poll interval for no reason.
go
producer, err := cyclone.NewClient(pool, mux,
cyclone.WithNotifyCoalesce(1*time.Millisecond),
)
if err != nil {
return err
}
defer producer.Shutdown(context.Background()) // required, even without StartShutdown seals the buffer. It waits for a publication that is already in flight. It flushes what is left before it returns.
Observability
| Option | Default | Notes |
|---|---|---|
WithLogger(*slog.Logger) | stderr text handler at info (DEBUG env raises it to debug) | Passing nil restores the default. Cyclone logs best-effort publication failures and coalesce-buffer drops here at error level. |
WithOnStepEvent(fn) | unset | Post-commit step lifecycle callback. Cyclone recovers a panic inside it. |
WithErrorHandler(fn) | unset | Called once for each failed attempt, after the failure has committed. |
See Observability and Monitoring and Alerting.
Limits and Retention
| Option | Default | Notes |
|---|---|---|
WithPayloadLimits(l) | inputs 1 MiB, outputs 1 MiB, signals 256 KiB | Cyclone enforces the limit before any write, with a typed *PayloadTooLargeError. Oversized handler output fails the step permanently. |
WithRetentionSweep(cfg) | off | Background purge of terminal runs, inside Start. A non-positive cfg.DefaultAge leaves the sweeper disabled. |
The OutputBytes limit also bounds the collected output of a fan-out node. Submit copies that limit onto the node. The limit of the client that submits the run is the one that applies. cyclone.MaxFanOutItems bounds the item count of one fan-out node. It is a constant, not an option. See Fan Out Over a List.
See Payload Limits and Retention.
Related
- Queue Sizing — worker, pool, and poll-interval sizing
- Engine Design — how claim, notify, and polling fit together
- Graceful Shutdown
- Producer/Worker Split