Proletarian

repository·main·Indexed 42 days ago

https://github.com/msolli/proletarian

A durable job queuing and worker system for Clojure that leverages PostgreSQL 9.5+ or MySQL 8.0.1+. It supports the Outbox Pattern by allowing jobs to be enqueued within existing database transactions and provides an 'at least once' processing guarantee.

Tokens
5.6K
Snippets
17
Records
32
Agent score
53%

What's inside Proletarian

  1. Configure the Queue Worker's retry and failure behavior

    main

    The Queue Worker can be configured with several specialized functions to manage job lifecycles:

    • :proletarian/retry-strategy-fn: Defines how many times a job should be retried and the delay between attempts.
    • :proletarian/failed-job-fn: A callback triggered when a job has exhausted all retries and is marked as permanently failed.
    • :proletarian/on-polling-error: A handler for errors occurring during the queue polling process, allowing the worker to remain resilient.
    • :proletarian/install-jvm-shutdown-hook?: When enabled, ensures a graceful shutdown (e.g., on Ctrl-C) and prints a summary of job outcomes.
  2. How Queue Workers and Jobs work together

    main

    Queue Worker

    A process that works off a named queue. It uses worker threads to pick up jobs and execute them.

    • Default Queue: :proletarian/default.
    • Parallelism: Controlled by the number of worker threads in the worker and the number of worker processes running across different machines.
    • Polling: Workers poll the queue at a configurable interval when empty.

    Job Handler

    The logic executed by the worker. It receives the job-type and the payload. It is responsible for dispatching the work based on the type.

  3. Implement a Job Handler

    main

    The Job Handler is the function passed to create-queue-worker. It is invoked whenever a job is ready. It must be an arity-2 function that accepts job-type (a keyword) and payload (the data passed during enqueuing).

    A common pattern is to use a defmulti to dispatch different job types to specific implementation functions.

    (ns your-app.handlers)
    
    ;; Define the dispatcher
    (defmulti handle-job!
      (fn [job-type _payload] job-type))
    
    ;; Implement specific handlers
    (defmethod handle-job! ::confirmation-email
      [job-type {:keys [email]}]
      ;; Logic to send email
      )
  4. Understand Proletarian's 'At Least Once' processing guarantee

    main

    Proletarian guarantees that every job will be run at least once. It uses PostgreSQL/MySQL transactions to ensure that a job is only moved to the archive table once it has finished successfully.

    In rare failure scenarios (e.g., the database goes offline exactly when a job finishes but before the transaction to archive it completes), the job may be picked up and run again when the worker restarts.

    Requirement: Because of this guarantee, your job handlers must be idempotent or your business logic must tolerate the same job being executed more than once.

  5. How at-least-once processing and idempotence work

    main

    Proletarian guarantees at-least-once delivery. Jobs are only removed from the queue after successful completion and archival within the same database transaction.

    Warning: In rare failure scenarios (e.g., a database crash during a commit), a job may execute multiple times. Therefore, all job handler functions must be idempotent.

  6. Understand handler function modes

    main

    The shape of the data passed to your job handler depends on the :proletarian/handler-fn-mode configuration:

    • :default mode: The handler receives two arguments: (job-type, payload).
    • :advanced mode: The handler receives the full job map containing metadata such as :job-id, :attempts, :enqueued-at, etc.
  7. How job interruption and shutdown work in Proletarian

    main

    Proletarian's ability to shut down cleanly depends on how your job handlers interact with Java's thread interrupt mechanism:

    Interruptible Jobs (Blocking Operations)

    Jobs that use blocking operations—such as Thread/sleep, I/O reads, or core.async/<!!—respond to interrupts by throwing an InterruptedException. Proletarian catches this, stops the job, and emits the :proletarian.worker/job-interrupted event. Because the job did not complete, it remains in the queue and will be picked up again when the worker restarts.

    Non-Interruptible Jobs (CPU-bound Operations)

    Jobs that perform CPU-bound tasks (e.g., tight loops) that do not check the thread's interrupt flag cannot be stopped immediately. In this case, Proletarian will wait for the job to finish naturally before completing the shutdown. The worker will emit :proletarian.executor/shutting-down, wait for the job to emit :proletarian.worker/job-finished, and then emit :proletarian.executor/completed-shutdown.

    Best Practice: Since interrupted jobs are re-enqueued and re-run, ensure your job handlers are idempotent.

  8. Implement a job handler using multimethods

    main

    Proletarian uses Clojure multimethods to route jobs to their appropriate handlers based on the :job-type.

    To handle a specific job type, you must implement the proletarian.job/handle-job! multimethod.

    Pattern: Use a namespaced keyword for the :job-type. A common convention is to use the double-colon shorthand :: for the current namespace. For example, if your namespace is :example-a.enqueue-jobs, using ::echo as the job type will result in the keyword :example-a.enqueue-jobs/echo being used to dispatch the job.

  9. Install the Proletarian example database

    main

    To set up the database required for the examples, use the provided Makefile target. This creates a PostgreSQL user, database, and the necessary schema (including proletarian.job, proletarian.archived_job, and indexes).

    Local Installation

    Run the following command:

    make examples.db.install

    Docker Installation

    If you prefer using an ephemeral Docker instance, follow these steps:

    1. Start a Postgres container:
    docker run -p 55432:5432 -e POSTGRES_PASSWORD=proletarian postgres
    1. Install the database into the container:
    PGHOST=localhost PGPORT=55432 PGUSER=postgres PGPASSWORD=proletarian make examples.db.install
    1. Set the DATABASE_URL environment variable in your application:
    export DATABASE_URL="jdbc:postgresql://localhost:55432/proletarian?user=postgres&password=proletarian"

    Management

    • Uninstall: make examples.db.uninstall
    • Recreate: make examples.db.recreate
  10. Install and setup the example database

    main

    To run the provided examples or develop locally, you must install the example database which creates a proletarian database. Use the following make commands to manage the database state.

    # Install example database (creates 'proletarian' database)
    make examples.db.install
    
    # Uninstall example database
    make examples.db.uninstall
    
    # Recreate database (uninstall then install)
    make examples.db.recreate
  11. Enqueue a job in Proletarian

    main

    Jobs are added to a queue to be processed later by a Queue Worker. Each job consists of a unique ID, a type, and a payload.

    To enqueue jobs using the example command, run:

    clj -X:examples example-a.enqueue-jobs/run

    Job Structure

    When a job is enqueued, it contains the following fields:

    • :job-id: A unique UUID assigned to the job.
    • :job-type: A Clojure keyword that determines which handler function will process the job. It is common practice to use namespaced keywords (e.g., ::echo which expands to :example-a.enqueue-jobs/echo).
    • :payload: The data associated with the job. This is serialized (defaulting to Transit) and stored in a TEXT column in the database.

    Customizing Serialization

    You can change how payloads are serialized by implementing the proletarian.protocols/Serializer protocol. Pass an instance of your serializer under the :proletarian/serializer option to:

    1. proletarian.job/enqueue!
    2. proletarian.worker/queue-worker-controller
  12. Run tests and development commands

    main

    Use the following commands for testing and development:

    Testing

    # Run all tests via make
    make test
    
    # Run all tests directly with Clojure CLI
    clojure -M:test -m kaocha.runner --config-file test/tests.edn

    REPL Development

    # Start REPL with all necessary aliases
    make repl
    
    # Or with Clojure CLI
    clj -M:dev:test:examples

    Building

    make jar
    make deploy
    make mvn.install