chronos

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 17 Imported by: 0

README

chronos-go

CI Go Reference

A Redis-backed distributed task queue and scheduler for Go, with a type-safe generic API.

chronos-go is a modern alternative to asynq (now in maintenance mode). It keeps what people loved about asynq — a simple enqueue → handle model, at-least-once delivery, crash recovery — and fixes its biggest gaps: a distributed scheduler that runs each job once across many instances, unbounded stream/dead-letter growth, and unique-lock expiry during long processing.

Status: v0.x — usable and covered by tests against real Redis, but the API may still evolve before v1.0.0.

Highlights

  • Type-safe, generic API — no interface{} payloads, no manual json.Unmarshal. Define a task type, register a Handler[T].
  • Redis Streams + ZSETs — immediate work rides a Streams consumer group; delayed/retry/archived tasks live in sorted sets. Cluster-safe (hash-tagged keys).
  • Reliable — automatic retries with exponential backoff + jitter, crash recovery (XAUTOCLAIM), dead-letter with an OnDeadLetter hook.
  • Distributed scheduler — interval & cron jobs. A Redis leader election ensures only one instance enqueues each trigger; a deterministic task ID fences duplicates during leader hand-off.
  • Delayed execution & de-duplicationWithProcessIn / WithProcessAt, and WithUnique to collapse duplicate work.
  • Weighted priority queues — queue weights are honored via smooth weighted round-robin (no starvation), or set StrictPriority to always drain higher-weight queues first.
  • Heartbeat — refreshes the lease and unique lock of in-flight tasks, so a long-running task is neither reclaimed nor loses its lock mid-processing.
  • Self-cleaning — a janitor trims dead-lettered tasks by age and count, so Redis memory stays bounded.
  • Observable — an Inspector API + a chronos CLI, a runnable tour, and a Prometheus + Grafana stack in contrib/prometheus.

Requirements

  • Go 1.26+
  • Redis 6.2+ (uses XAUTOCLAIM)

Install

go get github.com/kenshin579/chronos-go

Quick start

Define a task, enqueue it with a client, and process it with a server.

package main

import (
	"context"
	"log"

	"github.com/redis/go-redis/v9"
	"github.com/kenshin579/chronos-go"
)

// A task type: any struct with a stable Kind() (value receiver).
type EmailArgs struct {
	UserID string `json:"user_id"`
	Body   string `json:"body"`
}

func (EmailArgs) Kind() string { return "email:send" }

func main() {
	rdb := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})
	ctx := context.Background()

	// --- worker side ---
	mux := chronos.NewMux()
	chronos.AddHandler(mux, func(ctx context.Context, t *chronos.Task[EmailArgs]) error {
		// t.Args is strongly typed — no casting, no json.Unmarshal.
		log.Printf("sending to %s: %s", t.Args.UserID, t.Args.Body)
		return nil
	})

	srv := chronos.NewServer(rdb, chronos.ServerConfig{
		Queues:      map[string]int{"default": 1},
		Concurrency: 10,
	})
	if err := srv.Start(ctx, mux); err != nil {
		log.Fatal(err)
	}
	defer srv.Shutdown(context.Background())

	// --- producer side ---
	client := chronos.NewClient(rdb)
	defer client.Close()

	if _, err := chronos.Enqueue(ctx, client, EmailArgs{UserID: "u1", Body: "hi"}); err != nil {
		log.Fatal(err)
	}

	select {} // keep the worker running
}
Enqueue options
chronos.Enqueue(ctx, client, EmailArgs{...},
	chronos.WithQueue("critical"),          // route to a queue
	chronos.WithMaxRetry(5),                 // retry budget (default 25)
	chronos.WithProcessIn(30*time.Minute),   // run later (delayed)
	chronos.WithUnique(10*time.Minute),      // dedup identical (kind+payload) tasks
	chronos.WithDeadLetterDiscard(),         // drop instead of archive on exhaustion
)
Handler outcomes
  • return nil → success (acked and removed).
  • return an error → retried with backoff until MaxRetry is exhausted, then dead-lettered.
  • return chronos.SkipRetry(err) → dead-lettered immediately (permanent error).
  • panic → recovered, treated as a retryable error.

Set an OnDeadLetter hook on ServerConfig to alert on / inspect exhausted tasks.

Queue priority

ServerConfig.Queues maps queue name → weight. While every queue has work, a queue with weight 6 is dequeued about 6× as often as a queue with weight 1 (smooth weighted round-robin — lower-weight queues never starve). When the queue chosen for a round is empty, that round falls through to the highest-weight queue that does have work, so an idle high-priority queue never holds up lower ones. Weights <= 0 are treated as 1.

srv := chronos.NewServer(rdb, chronos.ServerConfig{
	Queues: map[string]int{
		"critical": 6,
		"default":  3,
		"low":      1,
	},
	// StrictPriority: true, // always drain critical first, then default, then low
})

With StrictPriority: true, higher-weight queues are always drained first; a lower-weight queue runs only while every higher one is empty.

Scheduling (interval & cron)

Register periodic jobs on a Scheduler. Every instance may call Start; only the elected leader enqueues, so a job fires once cluster-wide.

sched := chronos.NewScheduler(rdb, chronos.SchedulerConfig{})

// every 30s (interval must be >= 1s)
chronos.RegisterInterval(sched, 30*time.Second, HealthCheckArgs{})

// standard 5-field cron
chronos.RegisterCron(sched, "0 0 * * *", DailyReportArgs{})

sched.Start(ctx)          // safe on every instance
defer sched.Shutdown(ctx)

Missed triggers (after a leader-election gap) are handled by a per-job WithMisfirePolicyMisfireSkip (default) or MisfireFireOnce.

Observability

chronos-go is a headless library, so it ships tools to see what it is doing:

  • Runnable tour — every feature end-to-end, printed as it happens:
    go run ./examples/tour
    
  • CLI — inspect and administer queues/tasks:
    go run ./cmd/chronos queue ls
    go run ./cmd/chronos task ls default archived
    go run ./cmd/chronos task run default <id>   # re-run a dead-letter
    go run ./cmd/chronos task rm  default <id>
    
  • Inspector API — the same data programmatically (chronos.NewInspector).
  • Prometheus + Grafana — a Metrics hook (core, dependency-free) plus a ready stack in contrib/prometheus:
    cd contrib/prometheus/deploy && docker compose up --build
    # Grafana: http://localhost:3000  (dashboard "chronos-go")
    
  • Web console — a browser task-management UI (inspect dead-letters, re-run / delete) in contrib/webui:
    cd contrib/webui && go run ./cmd/webui
    

See docs/OBSERVING.md for Redis-level inspection.

How it works

  • Immediate queue: a Redis Stream per queue with one consumer group. Workers XREADGROUP (blocking), run the handler, then XACK + XDEL. Queues are selected by smooth weighted round-robin (or strictly by weight with StrictPriority).
  • Delayed / retry / archived: sorted sets scored by run-at / retry-at / died-at; a forwarder promotes due entries back into the stream.
  • Crash recovery: a recoverer XAUTOCLAIMs entries whose worker went silent and re-queues or dead-letters them (attempts tracked in the task hash).
  • Scheduler: a leader (Redis SET NX PX lock + pub/sub resignation) runs the tick loop; each trigger is enqueued under a deterministic dedup key (<schedule>:<trigger-unix>) so a split-brain hand-off cannot double-enqueue.
  • Heartbeat: refreshes in-flight tasks' PEL idle (XCLAIM ... JUSTID) and unique-lock TTL so long-running work is safe.
  • Keys are wrapped in a {queue} hash tag so every multi-key Lua script is Redis Cluster-safe.

Delivery semantics

chronos-go is at-least-once: a task can run more than once (e.g. a worker crashes after finishing but before acking). Make handlers idempotent.

vs. asynq

asynq chronos-go
Payload API []byte + manual unmarshal generic Task[T] (type-safe)
Scheduler across instances app must ensure a single scheduler built-in leader election + deterministic dedup
Unique lock during long processing can expire (TTL only) heartbeat renews it
Stream / dead-letter growth trimmed (XDEL) + janitor retention
Backend Redis Redis

Known limitations / roadmap

  • Scheduler fencing relies on a dedup-key TTL (no monotonic fencing token); a leader paused longer than the TTL could theoretically re-enqueue a trigger. All instances must share the same Location and reasonably synced clocks.
  • The unique lock is heartbeat-renewed only while a task is actively processing; while it waits in the scheduled/retry set, it is covered by its TTL — set the TTL comfortably above expected waiting time.
  • Not yet built: completed-task retention, a web UI, workflows (chains/groups).

Development

Tests run against a real Redis (skipped if none is reachable at $REDIS_ADDR, default 127.0.0.1:6379). They share a logical DB, so run packages serially:

make check        # gofmt + vet + go test ./... -race -p 1 + contrib tests

License

MIT

Documentation

Overview

Package chronos is a Redis-backed distributed scheduler and task queue.

Index

Constants

View Source
const DefaultMaxRetry = 25

DefaultMaxRetry is the retry budget used when WithMaxRetry is not given.

View Source
const DefaultQueue = "default"

DefaultQueue is the queue used when none is specified.

Variables

View Source
var (
	ErrTaskNotFound = errors.New("chronos: task not found")
	ErrInvalidState = errors.New("chronos: invalid task state")
)

Errors returned by the Inspector, exposed so callers (e.g. the web console) can map them to the right response.

View Source
var ErrDuplicateTask = rdb.ErrDuplicateTask

ErrDuplicateTask is returned by Enqueue when WithUnique is used and an identical task already holds the unique lock.

Functions

func AddHandler

func AddHandler[T TaskArgs](mux *Mux, fn func(ctx context.Context, task *Task[T]) error)

AddHandler registers a strongly-typed handler for tasks of type T. The Kind is read from the zero value of T, so T's Kind method must use a value receiver. Registering two handlers for the same Kind panics.

func DefaultRetryDelay

func DefaultRetryDelay(retried int, _ error) time.Duration

DefaultRetryDelay is the default backoff: an exponential cap (base * 2^retried, clamped to retryMaxDelay) with full jitter — the actual delay is uniformly random in [0, cap]. Full jitter spreads retries to avoid thundering herds.

func RegisterCron

func RegisterCron[T TaskArgs](s *Scheduler, spec string, args T, opts ...Option) error

RegisterCron registers args to be enqueued on a standard 5-field cron spec.

func RegisterInterval

func RegisterInterval[T TaskArgs](s *Scheduler, interval time.Duration, args T, opts ...Option) error

RegisterInterval registers args to be enqueued every interval (>= 1s). Any sub-second component is truncated to whole seconds (e.g. 1500ms behaves as 1s). Register all schedules before calling Start (registration is not concurrency- safe with the running tick loop).

func SkipRetry

func SkipRetry(err error) error

SkipRetry wraps err so that returning it from a handler dead-letters the task immediately, bypassing the remaining retry budget.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client enqueues tasks.

func NewClient

func NewClient(r redis.UniversalClient) *Client

NewClient returns a Client backed by the given Redis client.

func (*Client) Close

func (c *Client) Close() error

Close releases the client's resources. The underlying Redis client is owned by the caller and is not closed here.

type Inspector

type Inspector struct {
	// contains filtered or unexported fields
}

Inspector provides read and administrative access to queues and tasks. It is the foundation the CLI (and any future UI) is built on.

func NewInspector

func NewInspector(r redis.UniversalClient) *Inspector

NewInspector returns an Inspector backed by the given Redis client.

func (*Inspector) DeleteTask

func (i *Inspector) DeleteTask(ctx context.Context, qname, taskID string) error

DeleteTask removes a scheduled/retry/archived task and releases its unique lock.

func (*Inspector) GetTask added in v0.3.0

func (i *Inspector) GetTask(ctx context.Context, qname, taskID string) (*TaskInfo, error)

GetTask returns full detail for a single stored task (scheduled/retry/archived).

func (*Inspector) ListTasks

func (i *Inspector) ListTasks(ctx context.Context, qname, state string, limit int) ([]*TaskInfo, error)

ListTasks returns up to limit tasks in the given state (scheduled|retry|archived).

func (*Inspector) Queues

func (i *Inspector) Queues(ctx context.Context) ([]*QueueInfo, error)

Queues lists all known queues with their per-state counts.

func (*Inspector) RunTask

func (i *Inspector) RunTask(ctx context.Context, qname, taskID string) error

RunTask promotes a scheduled/retry/archived task so it runs immediately.

type Metrics

type Metrics interface {
	ObserveTask(queue, kind string, outcome TaskOutcome, dur time.Duration)
}

Metrics receives one observation per processed task. Implementations MUST be safe for concurrent use (the server calls it from worker goroutines). The zero/nil Metrics disables observation. The Prometheus implementation lives in the contrib/prometheus module so the core stays dependency-free.

type MisfirePolicy

type MisfirePolicy int

MisfirePolicy decides what happens when a schedule's triggers were missed (e.g. during a leader-election gap or downtime).

const (
	// MisfireSkip (default) discards missed triggers and resumes on schedule.
	MisfireSkip MisfirePolicy = iota
	// MisfireFireOnce fires exactly one catch-up trigger if any were missed.
	MisfireFireOnce
)

type Mux

type Mux struct {
	// contains filtered or unexported fields
}

Mux routes tasks to handlers by their Kind.

func NewMux

func NewMux() *Mux

NewMux returns an empty Mux.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option customizes a single Enqueue call.

func WithDeadLetterDiscard

func WithDeadLetterDiscard() Option

WithDeadLetterDiscard discards the task on retry exhaustion instead of storing it in the archived ZSET. The OnDeadLetter hook still fires.

func WithMaxRetry

func WithMaxRetry(n int) Option

WithMaxRetry sets the maximum number of retries before the task is dead-lettered. Defaults to DefaultMaxRetry.

func WithMisfirePolicy

func WithMisfirePolicy(p MisfirePolicy) Option

WithMisfirePolicy sets how a scheduled job handles missed triggers (after a leader-election gap or downtime). Only meaningful for RegisterInterval / RegisterCron; ignored by a plain Enqueue. Defaults to MisfireSkip.

func WithProcessAt

func WithProcessAt(t time.Time) Option

WithProcessAt schedules the task to first become available at t. A non-future time enqueues immediately.

func WithProcessIn

func WithProcessIn(d time.Duration) Option

WithProcessIn schedules the task to first become available after d. A non-positive d enqueues immediately.

func WithQueue

func WithQueue(name string) Option

WithQueue routes the task to a specific queue.

func WithTaskID

func WithTaskID(id string) Option

WithTaskID sets an explicit task ID. Enforced deduplication is provided by the unique lock introduced in a later milestone; in M1, re-enqueueing with the same ID is not guaranteed to prevent duplicates. When omitted a random UUID is generated.

func WithUnique

func WithUnique(ttl time.Duration) Option

WithUnique deduplicates tasks by (kind + payload): while a matching task is anywhere in the pipeline (pending, retrying, scheduled), enqueueing another returns ErrDuplicateTask. The lock is released when the task reaches a terminal state (completed / archived / discarded).

While a task is actively being processed, the server's heartbeat renews the lock's TTL, so a single attempt that runs longer than ttl still holds the lock. ttl mainly bounds the lock for the time a task spends waiting (pending / scheduled / retry backoff) where no worker is renewing it; set it comfortably above the expected total waiting time. For a delayed task the lock TTL is automatically extended to cover the delay.

type QueueInfo

type QueueInfo struct {
	Queue     string
	Pending   int64
	Active    int64
	Scheduled int64
	Retry     int64
	Archived  int64
}

QueueInfo is a queue's per-state task counts.

type RetryDelayFunc

type RetryDelayFunc func(retried int, err error) time.Duration

RetryDelayFunc computes how long to wait before the next retry, given the number of retries already performed and the error that caused the failure.

type Scheduler

type Scheduler struct {
	// contains filtered or unexported fields
}

Scheduler registers periodic jobs and, on whichever instance is elected leader, enqueues their due triggers. Every instance may call Start; only the leader enqueues, and deterministic dedup keys prevent double-enqueue during leader handover.

func NewScheduler

func NewScheduler(r redis.UniversalClient, cfg SchedulerConfig) *Scheduler

NewScheduler returns a Scheduler backed by the given Redis client.

func (*Scheduler) Shutdown

func (s *Scheduler) Shutdown(ctx context.Context) error

Shutdown stops the scheduler; if this instance is the leader it resigns so a follower can take over immediately.

func (*Scheduler) Start

func (s *Scheduler) Start(ctx context.Context) error

Start launches leader election and the tick loop. Safe to call on every instance; only the leader enqueues. Register all schedules before Start.

For correct cross-instance behavior, every instance must register the same schedules with the same SchedulerConfig.Location and have reasonably synchronized clocks: the deterministic dedup key is derived from the computed trigger instant, so divergent timezones or badly skewed clocks could let two instances treat the same logical trigger as different ones.

type SchedulerConfig

type SchedulerConfig struct {
	// Location is the timezone for cron schedules. Defaults to time.Local.
	Location *time.Location
	// Logger receives operational logs. Defaults to slog.Default().
	Logger *slog.Logger
	// LeaderTTL is how long a leadership term lasts before it must be renewed.
	// Defaults to 5s. Failover happens within ~LeaderTTL of a leader dying.
	LeaderTTL time.Duration
}

SchedulerConfig configures a Scheduler.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server fetches tasks from Redis and dispatches them to handlers.

func NewServer

func NewServer(r redis.UniversalClient, cfg ServerConfig) *Server

NewServer returns a Server backed by the given Redis client.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown stops fetching and waits for in-flight tasks to finish, bounded by ctx. In-flight tasks that do not finish before ctx expires are left unacked and will be recovered by another instance in a later milestone.

func (*Server) Start

func (s *Server) Start(ctx context.Context, mux *Mux) error

Start ensures consumer groups exist and launches the fetch loop. It returns once startup is complete; processing continues in the background until Shutdown is called or ctx is cancelled.

type ServerConfig

type ServerConfig struct {
	// Queues maps queue name to weight. While every queue has work, a queue with
	// weight 6 is dequeued about 6x as often as a queue with weight 1 (smooth
	// weighted round-robin — no queue starves). When the queue picked for a round
	// is empty, that round falls through to the highest-weight queue that does
	// have work, so an idle high-priority queue never blocks lower ones. Weights
	// <= 0 are treated as 1; very large weights are capped.
	Queues map[string]int
	// StrictPriority, if true, always drains higher-weight queues first: a
	// lower-weight queue is served only while every higher-weight queue is
	// empty. Ties are broken by queue name for determinism.
	StrictPriority bool
	// Concurrency is the max number of tasks processed simultaneously.
	Concurrency int
	// Logger receives operational logs. Defaults to slog.Default().
	Logger *slog.Logger

	// RetryDelayFunc computes the backoff before a retry. Defaults to
	// DefaultRetryDelay (exponential + full jitter).
	RetryDelayFunc RetryDelayFunc
	// OnDeadLetter is invoked when a task exhausts its retries (or returns a
	// SkipRetry error). It fires whether the task is archived or discarded.
	//
	// The heartbeat keeps an actively-processing task's lease fresh, so the
	// recoverer does not normally reclaim it. It may still fire more than once in
	// pathological cases (the server/heartbeat unavailable long enough for the
	// lease to lapse), so make the hook idempotent (the archived ZSET entry is
	// deduplicated by task ID).
	OnDeadLetter func(ctx context.Context, info *TaskInfo, err error)

	// Metrics, if set, receives one observation per processed task. Use the
	// contrib/prometheus implementation, or your own. Defaults to nil (disabled).
	Metrics Metrics

	// ForwardInterval is how often the retry ZSET is scanned for due tasks.
	// Defaults to 1s.
	ForwardInterval time.Duration
	// RecoverInterval is how often stuck PEL entries are reclaimed. Defaults to 15s.
	RecoverInterval time.Duration
	// RecoverMinIdle is how long a PEL entry must be idle before it is treated as
	// abandoned. Defaults to 30s when unset (<= 0). The heartbeat refreshes the
	// lease of in-flight tasks every HeartbeatInterval, so a task that runs longer
	// than RecoverMinIdle is safe as long as this server (its heartbeat) is alive;
	// RecoverMinIdle is the window after a worker actually dies before its tasks
	// are reclaimed.
	RecoverMinIdle time.Duration

	// HeartbeatInterval is how often the server refreshes the lease and unique
	// lock of in-flight tasks. Defaults to RecoverMinIdle/3. Must be shorter than
	// RecoverMinIdle so an actively-processing task is never reclaimed.
	HeartbeatInterval time.Duration

	// ArchivedRetention is how long a dead-lettered (archived) task is kept
	// before the janitor deletes it. Defaults to 7 days (168h).
	ArchivedRetention time.Duration
	// MaxArchived caps the number of archived tasks per queue; the janitor
	// deletes the oldest beyond this even within the retention window. Defaults
	// to 10000. Set negative to disable the size cap.
	MaxArchived int
	// JanitorInterval is how often the janitor runs. Defaults to 1 minute.
	JanitorInterval time.Duration
}

ServerConfig configures a Server.

type Task

type Task[T TaskArgs] struct {
	Args T
	// contains filtered or unexported fields
}

Task is a strongly-typed task delivered to a handler.

func (*Task[T]) ID

func (t *Task[T]) ID() string

ID returns the task's unique identifier.

func (*Task[T]) Queue

func (t *Task[T]) Queue() string

Queue returns the queue the task was enqueued to.

type TaskArgs

type TaskArgs interface {
	Kind() string
}

TaskArgs is implemented by every task payload type. Kind returns a stable identifier used to route the task to its handler; it MUST be defined on a value receiver so it can be called on the zero value during registration.

type TaskInfo

type TaskInfo struct {
	ID    string
	Kind  string
	Queue string

	// The following are populated by Inspector.ListTasks / GetTask for tasks
	// stored in a state ZSET (scheduled / retry / archived).
	State         string    // "scheduled" | "retry" | "archived" | ...
	Payload       []byte    // raw task payload
	Retried       int       // retries already attempted
	MaxRetry      int       // retry budget
	LastErr       string    // most recent failure message ("" if none)
	NextProcessAt time.Time // ZSET score as a time: scheduled-for / retry-at / died-at
}

TaskInfo describes an enqueued or stored task. Enqueue returns one with only ID/Kind/Queue set; the Inspector fills the rest for stored tasks.

func Enqueue

func Enqueue[T TaskArgs](ctx context.Context, c *Client, args T, opts ...Option) (*TaskInfo, error)

Enqueue serializes args and makes the task available for immediate processing. It is a package-level function rather than a method because Go methods cannot have type parameters.

type TaskOutcome

type TaskOutcome string

TaskOutcome is the terminal result of processing one task, reported to Metrics.

const (
	// OutcomeSuccess: the handler returned nil; the task was acked and removed.
	OutcomeSuccess TaskOutcome = "success"
	// OutcomeRetry: the handler failed and the task was scheduled for retry.
	OutcomeRetry TaskOutcome = "retry"
	// OutcomeDeadLetter: the task exhausted retries (or returned SkipRetry) and
	// was archived or discarded.
	OutcomeDeadLetter TaskOutcome = "dead_letter"
)

Directories

Path Synopsis
cmd
chronos command
Command chronos is a CLI for inspecting and administering chronos-go queues.
Command chronos is a CLI for inspecting and administering chronos-go queues.
examples
jobscheduler
Package jobscheduler is a verification adapter proving chronos-go can back operator-review's string+[]byte JobScheduler interface.
Package jobscheduler is a verification adapter proving chronos-go can back operator-review's string+[]byte JobScheduler interface.
tour command
Command tour is a narrated, runnable walkthrough of everything chronos-go can do: the core queue, reliability (retry / dead-letter), delayed + unique tasks, the Inspector, the distributed scheduler with leader failover, the retention janitor, the heartbeat, and weighted priority queues.
Command tour is a narrated, runnable walkthrough of everything chronos-go can do: the core queue, reliability (retry / dead-letter), delayed + unique tasks, the Inspector, the distributed scheduler with leader failover, the retention janitor, the heartbeat, and weighted priority queues.
internal
base
Package base defines the Redis key layout, task states, and message serialization shared across chronos-go internals.
Package base defines the Redis key layout, task states, and message serialization shared across chronos-go internals.
rdb
Package rdb implements the Redis operations backing chronos-go: enqueueing tasks, dequeueing via a consumer group, and acking completion.
Package rdb implements the Redis operations backing chronos-go: enqueueing tasks, dequeueing via a consumer group, and acking completion.
testutil
Package testutil provides shared test helpers for chronos-go.
Package testutil provides shared test helpers for chronos-go.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL