AnnouncementsIntroducing Arkoren

    Menu

    Consuming Jobs

    Register typed handlers, choose worker queues, and run consumers with a controlled shutdown.

    What's on this page
    1. Register handlers that receive fresh payloads and propagate cancellation.
    2. Select queues and understand concurrency within and across workers.
    3. Own worker shutdown and test handlers through the real consumer policy.
    On this page

    A producer can request a report without knowing how to build it. The worker supplies that implementation by registering a handler on a *queue.Consumer. The consumer finds the handler by job name, decodes the payload, executes it, and asks the driver to settle the delivery according to the result.

    Start with a configured driver and the shared BuildReportJob and BuildReport payload from Producing Jobs. The consumer owns execution policy, not the driver’s connection lifecycle.

    Registering Handlers

    Register handlers before starting the worker. With context imported, queue from arkoren.dev/core/service/queue, and your application’s reports.Build(ctx, reportID) error operation, connect the job to its implementation:

    consumer := queue.NewConsumer(driver)
    err := consumer.Handle(BuildReportJob, func(ctx context.Context, payload BuildReport) error {
    	return reports.Build(ctx, payload.ReportID)
    })

    Each attempt gets a fresh decoded payload. The context carries consumer cancellation, the job’s execution deadline when configured, and propagated trace context when telemetry is enabled. Pass it to database, storage, and HTTP calls. A timeout cancels the context; it does not stop application code by force.

    Handle err before running the worker. Invalid definitions return queue.ErrInvalidName wrapping the definition error. A second registration for the same job name returns queue.ErrDuplicateName, even if its payload type differs. A nil handler is rejected without registration.

    Run discovers queues once when it starts. A later handler on an already consumed queue can handle subsequent deliveries, but adding a new queue does not start another consume call until a later run. Register everything first to avoid timing-dependent coverage.

    Selecting Queues

    By default, a consumer reads every queue that has a registered handler. If report generation needs dedicated workers, restrict an alternative consumer construction:

    consumer := queue.NewConsumer(driver, func(worker *queue.ConsumerBuilder) {
    	worker.Queues("reports")
    })

    Register handlers on that consumer as above. Other handlers may remain registered, but their queues are skipped. This restriction neither changes driver routing nor provisions broker resources. A selected queue without a handler is not consumed; if none remain, Run returns queue.ErrNoQueues.

    Each Queues call replaces the previous restriction; Queues() restores all handled queues. Inputs and the published restriction are copied. Construction callbacks run once in order, and a retained draft cannot reconfigure the published consumer. Telemetry and event buses remain shared services.

    Worker Lifetime

    Handle does not begin consumption. Register Run as a supervised runner in an existing application composition callback, with application imported from arkoren.dev/core/application:

    compose := func(composer *application.Composer) {
    	composer.Run(consumer.Run)
    }

    Pass the callback to your existing app.Run(ctx, compose). Include the driver’s startup, readiness, and cleanup hooks from Drivers when required. RabbitMQ and NATS need those hooks; memory needs cleanup but no readiness probe; SQS and Cloudflare have no driver hooks. Construction alone does not connect RabbitMQ or NATS.

    Run starts one driver Consume call per selected, handled queue. Shipped drivers execute handlers sequentially within each call, including fetched batches. Different queues run concurrently. For more concurrency on one queue, run additional workers sharing the same physical work queue or JetStream durable; they compete for deliveries rather than receiving a broadcast copy each. Prefetch and batch size reserve work ahead, not parallel handler slots.

    If any consume call returns an error, Run cancels its sibling queue consumers, waits for them, and returns the first error. As an application runner, that error stops the service; let your deployment supervisor restart a worker after broker failure rather than silently leaving it idle. Ordinary handler errors whose retry or failure settlement succeeds do not themselves end Run.

    Stopping Workers

    Cancel the consumer and wait for it to return before stopping its driver or closing a borrowed connection. The active handler must cooperate with cancellation; the consumer cannot kill it. A handler that completes successfully still needs its acknowledgement, so allow shutdown time for execution and settlement.

    Settlement has a fresh 30-second budget after execution and is detached from consumer cancellation. It can still fail if transport cleanup happens first or the backend is unavailable. If the application’s shutdown budget expires, cleanup can release a driver while a handler is still running. See shutdown settlement for the retry exception and its attempt-count implications.

    Memory consumer cancellation preserves work not yet admitted; it does not drain the backlog or revoke the active delivery. An active handler can acknowledge or retry with the detached settlement context. memoryqueue.Stop is different: it discards ready jobs and delayed retries, prevents new admissions, and waits for active work only until its context ends. Retrying into that stopped driver fails. Use a broker-backed driver if pending work must outlive the process.

    Handler Results

    Return nil only when the work is complete; swallowing an error requests acknowledgement of unfinished work. Return an ordinary error for a transient failure. The consumer requests retry using the registered job’s attempt limit and backoff, or terminal failure if attempts are exhausted.

    Use queue.Permanent(err) when another attempt cannot help. queue.RetryAfter requests a specific delay while still respecting the attempt cap. queue.Reject delegates unwanted-message handling to backend rejection policy; it is not a portable delete or dead-letter operation. Choose among these using Delivery and Retries.

    Make reports.Build repeat-safe with the stable report ID. A successful report write followed by a lost acknowledgement can run again, and a lost lease can overlap another attempt. For database-only effects, record completion and the effect in one transaction. For remote effects, use provider idempotency and reconcile uncertain results; a local completion flag cannot make a remote call atomic. Required ordering also belongs in application state.

    Job timeout and broker reservation are separate. NATS and SQS try to renew reservations, but renewal failure can permit duplicates. Cloudflare cannot renew its lease: all earlier handlers in a fetched batch consume the same lease time, so keep batches small and finish execution plus settlement before expiry.

    Testing Handlers

    Test the report-building operation directly for its application behavior. To exercise JSON decoding and consumer settlement too, use arkoren.dev/core/service/queue/queuetest. In a test with t *testing.T, construct both facades around the same fake:

    fake := queuetest.New()
    dispatcher := queue.NewDispatcher(fake)
    consumer := queue.NewConsumer(fake)

    Register your handler using Handle and check its registration error. Dispatch a report payload, then drain due work through that consumer:

    if err := dispatcher.Dispatch(t.Context(), BuildReportJob, BuildReport{ReportID: "rpt-204"}); err != nil {
    	t.Fatal(err)
    }
    if err := fake.Drain(t.Context(), consumer); err != nil {
    	t.Fatal(err)
    }

    Assert the report’s observable stored result using your application’s test fixtures. Successful draining proves pending due work was processed without a settlement error, not that every handler succeeded: a terminally failed job also drains. Inspect the fake’s Failed() or Rejected() records when testing those outcomes.

    Delayed retries remain pending by default. queuetest.WithScheduled() includes them by skipping waits, not advancing time; use it when testing attempt policy, not elapsed-time behavior. A per-message delivery limit catches endless retry loops. See running dispatched jobs for assertions and fake options.

    See the queue API and queuetest API.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive