caerus-framework-valkey-queues

module
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2026 License: Apache-2.0

README

caerus-framework-valkey-queues

CI codecov License

Caerus Framework Valkey queue machines. One module, several claim/ack components that share a valkey peer (Client() per use, Key() for names, logs, soft-init). The fridge is caerus-framework-valkey.

This is not valkey-state (sessions / cache / counters). It is not a River/asynq wrap (caerus-framework-jobs when a product needs that).

Package Machine
vpq Weighted priority queue (hottest id wins)
delayed Delayed / retry / dead-letter jobs (fold of caerus-framework-valkey-jobs). Do not tag that old module anymore

There is no parent component that always starts every machine. The app constructs the queue it needs in New and returns it from Subcomponents().


vpq — weighted priority queue

Atomic Lua: add / claim / ack / requeue / recover. The app owns what the payload means. The component owns fairness, claim, deadlock recover, depth Health, metrics.

Keys go through the valkey peer’s Key() (squeue, zqueue, pqdeadlocks, …). Put the instance prefix on valkey (WithKeyPrefix), not on VPQ.

Handler is func(context.Context, *BGetObject) error. Honour ctx for shutdown. A failed handler requeues (weight +1).

Not a general job queue (no DLQ/cron/dashboard). For retries and scheduling use the delayed package in this module, or River/asynq — not VPQ.

delayed — run-at / retry / dead letter

Same fridge and chassis as VPQ. Different Lua: ready / inflight / dead ZSETs. Handler is func(context.Context, Job) error. Keys go through valkey Key("jobs", …). Construct in the app’s New and return from Subcomponents() when the product enqueues; do not start it because VPQ exists.

jobs := delayed.New(
	delayed.WithConfigSource("jobs", "config/jobs.json"),
	delayed.WithJobHandler("email.send", sendEmail),
)

WithConfigSource Init used to deadlock (mutex locked twice). That is fixed; the regression is TestWithConfigSourceInitializeDoesNotDeadlock.

worker_enabled and retry_jitter are pointers in the file: omit keeps the construct default; explicit false / 0 is how you turn the worker off or disable jitter. The poll loop reads worker_enabled every tick.

Default visibility is 1 minute. Set WithVisibility (per enqueue) well above the handler’s runtime or a slow job is reaped as hung and retried. Do not Info-log job.Payload if it can hold PII.

Dead letters sit in a ZSET until retention expires. Operators call ListDead, Replay (same id, attempts reset, due now), PurgeDead, or PurgeDeadAll. There is no HTTP admin; an app job or CLI is enough. Ack/release use the valkey peer’s live Client() (not the claim-time snapshot).

Depth gauges: valkey_jobs_ready, valkey_jobs_inflight, valkey_jobs_dead.

Wiring

Two wiring shapes. Prefer the app-owned shape.

Golden path (app-owned consumer, demoapp pattern)

main declares valkey (and postgres/http as needed) plus the app class. The app constructs the interest (or orders) queue in New and exposes it via Subcomponents() so the framework registers it. The app does not list "vpq" as a chassis peer it Gets unless some other component consumes the same queue instance.

fw := cf.New(&cf.FrameworkOptions{
	Logs:          &cf.LogsSettings{Format: "json", Level: "info", ConfigSource: "logs"},
	Observability: &cf.ObservabilitySettings{Address: ":9090", ConfigSource: "observability"},
	Components: []cf.CaerusComponent{
		cf_valkey.New(
			cf_valkey.WithConfigSource("valkey", "config/valkey.json"),
			cf_valkey.WithKeyPrefix("demo:"),
		),
		app.New(),
	},
})
func New() *App {
	a := &App{}
	a.interest = vpq.New(
		vpq.WithName("interest"),
		vpq.WithQueueName("interest"),
		vpq.WithHandler(a.InterestHandler),
	)
	return a
}

func (a *App) Subcomponents() []cf.CaerusComponent {
	return []cf.CaerusComponent{a.interest}
}

A process with more than one valkey uses vpq.WithValkeyName("valkey-cache"). GetDependencies reports that component Name(), not a config source nickname.

Simple path

Bare fw.AddComponent(valkey) + fw.AddComponent(queue) for a one-off binary:

fw := cf.New()
fw.AddComponent(cf_logs.New(cf_logs.WithWriter(os.Stdout)))
fw.AddComponent(cf_valkey.New(cf_valkey.WithAddress("127.0.0.1:6379")))
queue := vpq.New(
	vpq.WithQueueName("orders"),
	vpq.WithHandler(func(ctx context.Context, item *vpq.BGetObject) error {
		return processOrder(ctx, item.ObjectID, item.ObjectValue)
	}),
)
fw.AddComponent(queue) // GetDependencies: valkey, logs

The queue is a cf.Runnable: with a handler, Run consumes until cancel. Default recover of abandoned in-flight items is 30s (WithRecoverInterval(0) to disable).

Usage

queue := cf.MustGet[*vpq.PriorityQueue](fw)
added, err := queue.Add(ctx, "order-1", `{"amount": 42}`)
// added false → id already queued; weight +1; payload kept
item, err := queue.BlockingBGet(ctx)
if item != nil {
	_ = queue.Ack(ctx, item.ObjectID)
}

Options

Option Description
WithConfig(PQConfig) static snapshot; non-zero fields override option defaults
WithConfigSource(name, path, …) bind a configuration source (ConfigSourceRegistrar)
WithQueueName(name) required; key segment and identity (frozen after Init)
WithValkeyName(name) valkey component Name() (default "valkey")
WithBlockDuration(d) blocking pop wait (default 1s)
WithPublishWatermarkDelay(d) min interval between pub/sub on Add (default 0 = off)
WithCacheTimeout(d) max queue residence (default 0 = unlimited)
WithPollInterval(d) consumer poll (default 1s)
WithHandler(Handler) auto-consumer; default 30s recover + Health thresholds
WithWorkers(n) concurrent consumers (default 1). Reload of workers logs restart-required; the running pool size does not change
WithRecoverInterval(d) / WithRecoverMaxAge(d) deadlock recover tick / min age
WithMaxDepth(n) / WithMaxInFlight(n) Health ceilings
WithName(name) component Name() for multiple queues (default "vpq")
WithLogger(*slog.Logger) explicit logger; else framework logs via OnReconfigureFor

Reload updates tunables only. Queue name is frozen after Init. Valkey reconnect is the valkey owner’s job.

Health / metrics

Health pings valkey and checks depth / in-flight. A nil Client() (before Init, after Shutdown, or degraded peer) is not ready. Init may succeed when the peer is degraded (Client() nil); /readyz stays red until the fridge answers.

Metrics (vpq_info, vpq_depth, vpq_in_flight, vpq_recoveries_total) use copied label maps (queue, component).

Tests

Unit tests need no Valkey. Integration tests skip unless VALKEY_ADDR is set.

docker run -d --rm -p 6379:6379 --name v valkey/valkey:8
VALKEY_ADDR=127.0.0.1:6379 go test -race ./...

License

Apache License 2.0 — see LICENSE.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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