Honeydew Documentation

repository·master·Indexed 20 days ago

https://github.com/koudelka/honeydew

A pluggable job queue and worker pool for Elixir designed for at-least-once execution. Honeydew supports various backends including Mnesia and Ecto, and is built to be clusterable across BEAM nodes. It features an Ecto Poll Queue that turns Ecto schemas into queues to ensure state unification between the database and work queue, as well as support for global queues in distributed clusters with node healing.

Tokens
13.5K
Snippets
54
Records
78
Agent score
69%

What's inside Honeydew

  1. Use the Ecto Queue

    master
    The Ecto Queue allows you to turn an Ecto schema into a work queue using your existing database as the backing store. This eliminates synchronization issues between your database and the queue, as they share the same storage and consistency semantics. This is ideal for tasks like sending welcome emails immediately after a user record is inserted.
  2. Understand the Honeydew Job Lifecycle

    master

    A job in Honeydew moves through several stages involving the user process, the queue, a JobMonitor, and a Worker.

    1. Enqueuing: The user calls async/3, which packages the task into a Job and sends it to a queue group.
    2. Dispatching: The queue receives the job. If a Worker is available, it is dispatched immediately via the selected dispatch strategy; otherwise, it waits in the queue.
    3. Monitoring: Upon dispatch, the queue marks the job as 'in-progress' and spawns a JobMonitor. The JobMonitor uses a timer to ensure the Worker claims the job. Once claimed, the JobMonitor watches the Worker for crashes.
    4. Execution: The Worker spawns a JobRunner using the job and the user's state from init/1.
    5. Completion/Failure:
      • Success: The JobRunner reports success. If reply: true was used, the Worker sends the result to the user. The JobMonitor executes the SuccessMode, notifies the queue to remove the job, and the Worker signals it is ready for new work.
      • Failure: If the job crashes, the JobRunner reports it. If the crash is unrecoverable, the JobMonitor executes the configured FailureMode. The Worker then performs a controlled restart to recover state (e.g., re-establishing database connections).
  3. Configure failure modes for Honeydew queues

    master

    When a worker crashes, Honeydew uses a failure mode to determine what happens to the job. You can select a failure mode when calling Honeydew.start_queue/3.

    Available failure modes:

    • Honeydew.FailureMode.Abandon: The job is simply forgotten.
    • Honeydew.FailureMode.Move: The job is removed from the original queue and placed on a different queue.
    • Honeydew.FailureMode.Retry: The job is re-attempted on its original queue a specified number of times. After the final failure, it triggers another failure mode.
  4. Ensure job idempotency for at-least-once execution

    master
    Honeydew provides "at least once" job execution guarantees. This means that under certain circumstances, a job might execute successfully, but Honeydew may fail to report that success back to the queue. To prevent side effects from duplicate executions, you must write your jobs to be idempotent.
  5. Avoid using Honeydew as a simple resource pool

    master
    Honeydew is not designed to function as a simple resource pool. A key distinction is that the user's code is not executed in the requesting process. If your primary requirement is process-based resource pooling, consider using alternatives like sbroker instead.
  6. How to implement a global queue in a distributed cluster

    master

    In a distributed Erlang/Elixir cluster, you can distribute Honeydew's queue and worker processes across different nodes. This allows you to offload heavy tasks from client-facing nodes to a dedicated farm of background job processing nodes.

    To implement a global queue, use the {:global, name} tuple when starting Honeydew components. Honeydew automatically detects node availability and reconnects workers if nodes go up or down.

    Workflow

    1. Start the Queue: On a dedicated queue node, start the queue using Honeydew.start_queue/2 with a {:global, name} identifier.
    2. Start the Workers: On worker nodes, start the workers using Honeydew.start_workers/4, referencing the same {:global, name}.
    3. Enqueue Jobs: From any node in the cluster, use Honeydew.async/2 with the {:global, name} identifier to dispatch tasks.
    # Start queue on a specific node
    Honeydew.start_queue({:global, :my_queue}, queue: {Honeydew.Queue.Mnesia, [disc_copies: nodes]})
    
    # Start workers on worker nodes
    Honeydew.start_workers({:global, :my_queue}, MyWorker, num: 10, nodes: [:node1, :node2])
    
    # Enqueue from any node
    {:task, [args]} |> Honeydew.async({:global, :my_queue})
  7. Choose a Honeydew queue implementation

    master

    Honeydew provides several queue implementations depending on your requirements for persistence, replication, and performance. If no queue is explicitly specified, Honeydew defaults to an in-memory Mnesia store.

    Available Queue Types

    • Mnesia Queue: Highly configurable. Supports replication across multiple nodes, disk persistence via dets, and various safety modes ("access contexts").
    • ErlangQueue: A fast FIFO queue implemented using the :queue and Map modules. It is in-memory and does not survive node crashes.
    • Ecto Poll Queue: An Ecto-backed queue that supports auto-enqueuing jobs when a new row is inserted into a database table. It is suitable for environments using replicated databases (like CockroachDB or Postgres).
  8. Implement stateful workers using the init/1 callback

    master

    Honeydew workers can be stateless or initialized with state by implementing the init/1 callback.

    Important constraints:

    • Immutability: Worker state is immutable. To change the state, you must cause the worker to crash so that Honeydew restarts it.
    • Return Value: The init/1 function must return {:ok, state}.
    • Error Handling: If init/1 returns anything else or raises an error, Honeydew will trigger the failed_init/0 callback (if implemented). If failed_init/0 is not implemented, Honeydew will attempt to re-initialize the worker after a five-second delay.
    defmodule MyWorker do
      # Implement init/1 to provide state
      def init(args) do
        {:ok, %{my_state: "some_value"}}
      end
    end
  9. Use success modes for job monitoring

    master

    When a job completes successfully, Honeydew executes the handle_success/2 function from the selected success mode module on the queue's node. This is typically used for monitoring and telemetry.

    To calculate performance metrics, you can use the following fields available on the job:

    • :enqueued_at
    • :started_at
    • :completed_at

    Select a success mode using Honeydew.start_queue/3.

  10. Quickstart: Create a basic worker and queue

    master

    Honeydew is a pluggable job queue and worker pool for Elixir focused on at-least-once execution. To get started, define a worker module with a function to execute, start a queue, and start workers assigned to that queue. You can then enqueue jobs using Honeydew.async/3.

    defmodule MyWorker do
      def do_a_thing do
        IO.puts "doing a thing!"
      end
    end
    
    :ok = Honeydew.start_queue(:my_queue)
    :ok = Honeydew.start_workers(:my_queue, MyWorker)
    
    :do_a_thing |> Honeydew.async(:my_queue)
    
    # => "doing a thing!"
  11. Start a queue and workers with arguments

    master

    You can integrate Honeydew into your application's supervision tree by calling Honeydew.start_queue/1 and Honeydew.start_workers/2.

    When calling Honeydew.start_workers/2, the second argument is a tuple containing your worker module and a list of arguments. These arguments are passed directly to the worker's init/1 callback to establish state.

    defmodule App do
      def start do
        Honeydew.start_queue(:my_queue)
        Honeydew.start_workers(:my_queue, {Worker, ['127.0.0.1', 8087]})
      end
    end
  12. Implement a custom Honeydew queue

    master

    To implement a custom queue, use the existing queue modules as a reference. When designing your implementation, consider the following architectural questions:

    • Where exactly does the queue state live?
    • Is the queue process(es) the location where jobs reside?
    • Is the queue a stateless connector for an external broker?
    • Or is it a hybrid approach?