Honeydew Documentation
repository·master·Indexed 20 days ago
https://github.com/koudelka/honeydewA 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.
What's inside Honeydew
- 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.
Understand the Honeydew Job Lifecycle
masterA job in Honeydew moves through several stages involving the user process, the queue, a JobMonitor, and a Worker.
- Enqueuing: The user calls
async/3, which packages the task into aJoband sends it to a queue group. - 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.
- Monitoring: Upon dispatch, the queue marks the job as 'in-progress' and spawns a
JobMonitor. TheJobMonitoruses a timer to ensure the Worker claims the job. Once claimed, theJobMonitorwatches the Worker for crashes. - Execution: The Worker spawns a
JobRunnerusing the job and the user's state frominit/1. - Completion/Failure:
- Success: The
JobRunnerreports success. Ifreply: truewas used, the Worker sends the result to the user. TheJobMonitorexecutes theSuccessMode, notifies the queue to remove the job, and the Worker signals it is ready for new work. - Failure: If the job crashes, the
JobRunnerreports it. If the crash is unrecoverable, theJobMonitorexecutes the configuredFailureMode. The Worker then performs a controlled restart to recover state (e.g., re-establishing database connections).
- Success: The
- Enqueuing: The user calls
Configure failure modes for Honeydew queues
masterWhen 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.
Ensure job idempotency for at-least-once execution
masterHoneydew 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.Avoid using Honeydew as a simple resource pool
masterHoneydew 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 likesbrokerinstead.How to implement a global queue in a distributed cluster
masterIn 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
- Start the Queue: On a dedicated queue node, start the queue using
Honeydew.start_queue/2with a{:global, name}identifier. - Start the Workers: On worker nodes, start the workers using
Honeydew.start_workers/4, referencing the same{:global, name}. - Enqueue Jobs: From any node in the cluster, use
Honeydew.async/2with 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})- Start the Queue: On a dedicated queue node, start the queue using
Choose a Honeydew queue implementation
masterHoneydew 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
:queueandMapmodules. 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).
- Mnesia Queue: Highly configurable. Supports replication across multiple nodes, disk persistence via
Implement stateful workers using the init/1 callback
masterHoneydew workers can be stateless or initialized with state by implementing the
init/1callback.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/1function must return{:ok, state}. - Error Handling: If
init/1returns anything else or raises an error, Honeydew will trigger thefailed_init/0callback (if implemented). Iffailed_init/0is 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 endUse success modes for job monitoring
masterWhen a job completes successfully, Honeydew executes the
handle_success/2function 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.Quickstart: Create a basic worker and queue
masterHoneydew 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!"Start a queue and workers with arguments
masterYou can integrate Honeydew into your application's supervision tree by calling
Honeydew.start_queue/1andHoneydew.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'sinit/1callback 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 endImplement a custom Honeydew queue
masterTo 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?