jonnifyechomq

E2.02.1 · Jobs, queues & workers · dive 1

The job model

A Job is a thin Elixir struct over a Redis hash — the struct is a view, the hash is the truth.

EchoMQ.Job.new(queue_name, name, data, opts \\ []) (arity 4) builds a %EchoMQ.Job{}. The struct is a runtime materialization; the job's canonical state lives in Redis as a hash (E2.01). The opts map is the router — :delay, :priority, :parent, :job_id — and the struct carries pure predicates over its own fields. Pick the options on the right to read where the job lands.

Job.new(opts) · pick the options

Job.new waitaddStandardJob-9 delayedaddDelayedJob-6 prioritizedaddPrioritizedJob-9

The struct

new/4 sets the loud fields and folds the rest of opts into the opts map. The id is opts[:job_id] if given, else auto-generated; the three positional values become name, data, and queue_name.

# EchoMQ.Job.new/4 — the loud fields (lib/echomq/job.ex, verified)
def new(queue_name, name, data, opts \\ []) do
  opts = opts_to_map(opts)
  %EchoMQ.Job{
    id:               Map.get(opts, :job_id) || generate_id(),  # custom or auto
    name:             name,
    data:             data,
    queue_name:       queue_name,
    opts:             opts,
    prefix:           Map.get(opts, :prefix, "bull"),
    timestamp:        Map.get(opts, :timestamp, ...),
    delay:            Map.get(opts, :delay, 0),
    priority:         Map.get(opts, :priority, 0),    # 0 = highest
    parent:           Map.get(opts, :parent),
    parent_key:       build_parent_key(Map.get(opts, :parent)),
    deduplication_id: get_in(opts, [:deduplication, :id])
  }
end

The full verified defstruct carries id, name, data, queue_name, token, connection, worker, parent_key, parent, processed_by, repeat_job_key, deduplication_id, deferred_failure, processed_on, finished_on, failed_reason, return_value, opts (default %{}), prefix ("bull"), timestamp, delay, priority, progress, stacktrace, plus attempts_made, attempts_started, and stalled_counter. The compressed hash field names that back them — atm (attemptsMade), ats, stc, deid, defa — belong to E1 · the protocol; this dive names the model, and the protocol decodes the hash field by field.

The hash schema, field by field, is E1 · the job hash — this dive names the struct and its options; the protocol owns the wire layout.

The options route the job

The opts map decides where a freshly added job lands, and the add script differs per target. The interactive above reports the target state and the add script for each option set; the table below is its data.

# the opts that route a job → target state → add* Lua script
  delay > 0      →  delayed      addDelayedJob-6
  priority > 0   →  prioritized  addPrioritizedJob-9   # 0 = highest priority
  neither        →  wait         addStandardJob-9

# other opts (the full job_opts map)
  :job_id        custom id            :prefix      default "bull"
  :timestamp     enqueue time         :parent      → parent_key (flows)
  :deduplication → deduplication_id   :lifo        LIFO ordering
  :attempts      max retries          :backoff     %{type: :fixed | :exponential, delay: ms}

The options decide the location, and the location decides the add script. A plain job lands in wait; a delay or a priority routes it elsewhere — and priority 0 is the highest.

State predicates — pure functions on the struct

The Job carries pure predicates over its own fields. Pick one to read what it tests and where it reads.

The pure predicates · pick one

completed?/1finished_on, no fail failed?/1failed_reason set active?/1processed_on, no fin delayed?/1delay > 0 has_parent?/1parent / parent_key + estimated_state/1should_retry?/1

The predicates are pure functions of the struct's fields — no Redis call. They read the materialized view; the canonical state is still the hash plus the state-key membership.

The hash is L1 — the runtimes only materialize it

A Job's canonical state is a Redis hash, and the hash is L1 — immutable and shared across the three runtimes. EchoMQ.Job.from_redis(job_id, queue_name, data, opts \\ []) reconstructs a Job from a Redis HGETALL of that hash; to_redis/1 serializes it back. The same hash, with the same field names, exists whether the queue is driven from Elixir, Go, or Node.js; only the object each runtime builds from it varies.

the protocol (immutable L1)

The job hash schema in Redis and its field names — emq:{q}:{id}, the same location and the same field set in every runtime.

its three runtimes (variable L3/L4)

Elixir materializes the hash into %EchoMQ.Job{} via Job.from_redis/4; Go materializes it into its own struct; Node.js into its own class. Each runtime builds a different object from the same hash; the wire hash does not move.

A Job is a thin view, not the source of truth. Restart the materializer and the hash is still in Redis — re-read it and you have the same Job again.

Next: the stateless queue — the add/4 path that admits this Job with a string name and a connection, and nothing held in memory.

References

The Redis hash behind the model, and the BullMQ job options it mirrors.

Sources

Related in this course