PgQueuer Documentation

repository·main·Indexed 23 days ago

https://github.com/janbjorge/pgqueuer

PgQueuer is a PostgreSQL-powered background job processor for Python 3.11+ and PostgreSQL 12+. It allows developers to use their existing database as a reliable, transactional job queue, eliminating the need for separate message brokers like Redis or RabbitMQ. Key features include ACID-compliant transactional enqueuing, a built-in scheduler via the @pgq.schedule decorator, real-time monitoring with an interactive dashboard, and an in-memory mode for testing.

Tokens
48.7K
Snippets
147
Records
264
Agent score
80%

What's inside pgqueuer

  1. Key Features of PgQueuer

    main

    PgQueuer provides several built-in capabilities for job processing:

    • Real-Time Delivery: Uses LISTEN/NOTIFY for instant job pushing to workers.
    • Concurrency Control: Use concurrency_limit on an entrypoint to protect downstream services. A limit of 1 ensures strict serial processing globally.
    • Built-In Scheduler: Supports cron-style recurring tasks via the @schedule decorator (supports 5-field or 6-field expressions).
    • Automatic Retries & Heartbeat: DatabaseRetryEntrypointExecutor handles exponential backoff, while heartbeat monitoring re-queues jobs from crashed workers.
    • Observability: Includes Prometheus metrics, Logfire/Sentry tracing, and a live CLI dashboard via pgq dashboard.
    • In-Memory Testing: Use PgQueuer.in_memory() for unit tests without requiring a real PostgreSQL instance.
    • Completion Tracking: CompletionWatcher allows callers to await job results via LISTEN/NOTIFY.
    • Deferred Execution: Use execute_after to schedule jobs for a specific future timestamp.
    • Error Handling: Set on_failure="hold" to park failed jobs for manual review. Use pgq failed to inspect and pgq requeue <id> to retry them.
    • MCP Server: An optional Model Context Protocol server provides read-only tools for queue size, throughput, failures, etc.
  2. What is a Job in PgQueuer?

    main

    A job is a unit of work represented as a row in the pgqueuer table. Jobs are created by producers using Queries.enqueue() and processed by workers via registered entrypoints.

    Key job attributes include:

    • id: Auto-incrementing primary key.
    • entrypoint: The name of the handler that should process the job.
    • payload: Arbitrary bytes passed to the handler.
    • priority: Integer where higher values are dequeued first.
    • status: The current lifecycle state (e.g., queued, picked, successful).
    • execute_after: Timestamp indicating the earliest time the job can be picked up.
    • attempts: Number of previous retry attempts (starts at 0).
    • dedupe_key: Optional unique key to prevent duplicate enqueuing.
  3. Understand PgQueuer operational expectations

    main

    To ensure stable operation, keep the following PgQueuer-specific behaviors in mind:

    • Error Handling: Health checks may raise exceptions from pgqueuer.errors (such as FailingListenerError). Ensure your service startup logic captures and handles these.
    • LISTEN/NOTIFY: PgQueuer relies on LISTEN/NOTIFY to keep consumers active. Ensure firewalls do not drop idle sockets and that the database user has pg_notify privileges. Monitor pg_notification_queue_usage() to check for starvation.
    • Payload Encoding: Payloads are stored as bytea. Producers and consumers must use a consistent encoding/serialization strategy.
  4. Compare Database-level Retry vs. Heartbeat Recovery

    main

    PgQueuer provides two distinct mechanisms for handling failures:

    1. Database-level Retry (RetryRequested or DatabaseRetryEntrypointExecutor): Used for transient failures. The job is re-queued in the database, allowing it to survive worker restarts and be picked up by any available worker.
    2. Heartbeat timeout: Used for worker-crash recovery. This handles stalled jobs where a worker has crashed entirely without having the chance to raise an exception or update its heartbeat.
  5. Understand the Job Status Lifecycle

    main

    Jobs transition through several states stored in the pgqueuer_status PostgreSQL enum. Once a job reaches a terminal state, it is moved from the pgqueuer table to the pgqueuer_log table as an audit record.

    Lifecycle States

    • queued: Waiting to be picked up by a worker.
    • picked: A worker has claimed this job and is processing it.
    • successful: Handler completed without raising an exception.
    • exception: Handler raised an unhandled exception (traceback is logged).
    • failed: Job held for manual review after terminal failure. Inspect with pgq failed and re-queue with pgq requeue <id>.
    • canceled: Job was canceled via mark_job_as_cancelled().
    • deleted: Job was removed before being processed.

    Transitions

    • Retries: A picked job may return to queued if the handler raises RetryRequested.
    • Re-queuing: A failed job can be manually re-queued via the CLI pgq requeue <id> or Queries.requeue_jobs().
  6. Use DatabaseRetryEntrypointExecutor for persistent retries

    main

    DatabaseRetryEntrypointExecutor converts unhandled exceptions into database-level retries using RetryRequested. Unlike in-memory retries, these retries are stored in the database, meaning they survive worker restarts and allow any available worker to pick up the job after the delay.

    When to use it

    • Downstream service outages where resolution takes minutes.
    • Jobs that must survive worker restarts.
    • When you want retries to be visible in the queue and log tables.

    Configuration Parameters

    ParameterDefaultDescription
    max_attempts5Maximum retries before the exception becomes terminal
    initial_delay1sDelay before the first retry
    max_delay5mCap on exponential backoff
    backoff_multiplier2.0Multiplier applied to delay after each attempt

    Note: If your handler raises RetryRequested directly, it passes through unchanged. The executor only converts non-retry exceptions into retries. For best results, combine this with on_failure="hold" to park jobs after max_attempts is exhausted instead of deleting them.

    from datetime import timedelta
    from pgqueuer import PgQueuer, Job
    from pgqueuer.executors import DatabaseRetryEntrypointExecutor
    
    pgq = PgQueuer(driver)
    
    @pgq.entrypoint(
        "sync_inventory",
        executor_factory=lambda params: DatabaseRetryEntrypointExecutor(
            parameters=params,
            max_attempts=5,
            initial_delay=timedelta(seconds=2),
            max_delay=timedelta(minutes=10),
            backoff_multiplier=3.0,
        ),
    )
    async def sync_inventory(job: Job) -> None:
        await inventory_api.sync(job.payload)
  7. How the QueueManager processing loop works

    main

    The QueueManager operates in a continuous loop to process jobs efficiently:

    1. Wait: It waits for a NOTIFY signal from PostgreSQL.
    2. Query: Once signaled, it queries the database for available jobs.
    3. Execute: If jobs are found, it executes the task associated with the job.
    4. Handle Result:
      • If the task succeeds, the job is marked as successful.
      • If the task encounters an error, it is marked as exception.
    5. Loop: The process repeats, waiting for the next notification if no jobs were found or after processing is complete.
                  ┌──────────────────┐
                  │ Wait for NOTIFY  │◀─────────────────────┐
                  └────────┬─────────┘                      │
                           │                                │
                           ▼                                │
                  ┌──────────────────┐   no jobs            │
                  │    Query jobs    │──────────────────────▶│
                  └────────┬─────────┘                      │
                           │                                │
                           │ found                          │
                           ▼                                │
                  ┌──────────────────┐                      │
                  │   Execute task   │                      │
                  └────────┬──────────┬───┘                      │
                      │          │                         │
               success│          │error                     │
                      ▼          ▼                         │
            ┌──────────┐  ┌─────────────┐                   │
            │successful│  │  exception  │                   │
            └─────┬────┘  └──────┬──────┘                   │
                  └──────────────┴──────────────────────────┘
  8. Use Shared Resources via `Context.resources`

    main

    PgQueuer allows you to provide a shared resource container that is injected into every job execution context. This is ideal for initializing heavyweight or shared components like database connection pools, HTTP clients, or ML models once at startup and reusing them across all jobs.

    Resources are passed as a mutable mapping during the construction of PgQueuer or QueueManager. Because the mapping is shared (not copied), mutations made to the resources in one job are visible to all other jobs in the same process.

    import asyncpg
    from contextlib import asynccontextmanager
    from pgqueuer import PgQueuer
    from pgqueuer.db import AsyncpgDriver
    
    @asynccontextmanager
    async def build_pgqueuer():
        conn = await asyncpg.connect()
        driver = AsyncpgDriver(conn)
    
        # Define shared resources here
        resources = {
            "http_client": build_http_client(),
            "vector_index": load_vector_index(),
            "feature_flags": {"beta_mode": True},
        }
    
        pgq = PgQueuer(driver, resources=resources)
    
        @pgq.entrypoint("process_user")
        async def process_user(job: Job, ctx: Context) -> None:
            # Access resources via ctx.resources
            http = ctx.resources["http_client"]
            flags = ctx.resources["feature_flags"]
            ...
    
        yield pgq
  9. How concurrency control works in PgQueuer

    main
    PgQueuer enforces concurrency limits globally at the database level via the dequeue SQL query. This means limits are not applied per-worker, but across your entire fleet of workers. If you set a concurrency_limit of 5, no more than 5 jobs of that type will be running simultaneously across all connected workers.
  10. Summary of failure behaviors

    main

    The behavior of a job depends on the on_failure setting and the type of exception raised:

    Scenarioon_failure="delete" (default)on_failure="hold"
    Handler raises exceptionJob deleted, logged as exceptionJob kept with status='failed'
    Handler raises RetryRequestedRe-queued with delay (both modes)Re-queued with delay (both modes)
    Handler completesJob deleted, logged as successfulJob deleted, logged as successful
    DatabaseRetryEntrypointExecutor exhaustedJob deletedJob held with status='failed'
  11. How job flow works in PgQueuer

    main

    PgQueuer uses a producer-consumer model powered by PostgreSQL LISTEN/NOTIFY to ensure low-latency job processing.

    1. Producer: Inserts a job into the database using Queries.enqueue().
    2. PostgreSQL: A database trigger emits a table_changed_event via a NOTIFY signal on a configured channel.
    3. EventRouter: Receives the notification and routes it to the QueueManager.
    4. QueueManager: Fetches ready jobs using FOR UPDATE SKIP LOCKED to prevent multiple workers from grabbing the same job, then dispatches them to the appropriate entrypoint.
    5. Consumer: Executes the task and updates the job status in PostgreSQL upon completion.

    Endpoint routing is managed by the EventRouter, which maps notification types to functions decorated with @pgq.entrypoint.

    Producer ──enqueue──▶ PostgreSQL ──NOTIFY──▶ EventRouter
                              ▲                       │
                              │                    signal
                              │                       ▼
                        update status            QueueManager
                              │                       │
                              └───────────────── Consumer
                              └───────────────── dispatch