cover/Elixir.WalletIntegrations.JobQueue.html

1 defmodule WalletIntegrations.JobQueue do
2 @moduledoc """
3 In-process ETS-backed job queue for wallet_integrations workers.
4
5 Provides synchronous `drain_queue/1` for CI/test operation and
6 is compatible with Oban-style `perform/1` workers.
7
8 ETS table: `:wallet_integrations_jobs`
9 """
10
11 use GenServer
12
13 alias WalletIntegrations.QueueConfig
14
15 @table :wallet_integrations_jobs
16
17
:-(
defstruct [
18 :job_id,
19 :queue,
20 :worker,
21 :args,
22 :status,
23 :attempts,
24 :max_attempts,
25 :error,
26 :enqueued_at,
27 :completed_at
28 ]
29
30
:-(
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
31
32 @spec enqueue(queue :: atom(), worker :: module(), args :: map()) ::
33 {:ok, String.t()} | {:error, term()}
34
:-(
def enqueue(queue, worker, args \\ %{}) do
35 30 GenServer.call(__MODULE__, {:enqueue, queue, worker, args})
36 end
37
38 @spec drain_queue(queue :: atom()) :: %{completed: non_neg_integer(), failed: non_neg_integer(), discarded: non_neg_integer()}
39 22 def drain_queue(queue), do: GenServer.call(__MODULE__, {:drain_queue, queue}, 60_000)
40
41 @spec reset() :: :ok
42 91 def reset, do: GenServer.call(__MODULE__, :reset)
43
44 # GenServer callbacks
45
46 @impl true
47 def init(_opts) do
48
:-(
:ets.new(@table, [:set, :protected, :named_table])
49 {:ok, %{}}
50 end
51
52 @impl true
53 def handle_call({:enqueue, queue, worker, args}, _from, state) do
54 30 job_id = "job_" <> WalletSharedKernel.Correlation.new_request_id()
55 30 max_att = QueueConfig.max_attempts(queue)
56
57 30 job = %__MODULE__{
58 job_id: job_id,
59 queue: queue,
60 worker: worker,
61 args: args,
62 status: :available,
63 attempts: 0,
64 max_attempts: max_att,
65 error: nil,
66 enqueued_at: DateTime.utc_now(),
67 completed_at: nil
68 }
69
70 30 :ets.insert(@table, {job_id, job})
71 30 {:reply, {:ok, job_id}, state}
72 end
73
74 @impl true
75 def handle_call({:drain_queue, queue}, _from, state) do
76 22 jobs =
77 :ets.tab2list(@table)
78 22 |> Enum.map(fn {_, j} -> j end)
79 22 |> Enum.filter(fn j -> j.queue == queue and j.status == :available end)
80
81 22 results = Enum.reduce(jobs, %{completed: 0, failed: 0, discarded: 0}, fn job, acc ->
82 18 updated = %{job | status: :executing, attempts: job.attempts + 1}
83 18 :ets.insert(@table, {job.job_id, updated})
84
85 18 case apply(job.worker, :perform, [job.args]) do
86 :ok ->
87 16 done = %{updated | status: :completed, completed_at: DateTime.utc_now()}
88 16 :ets.insert(@table, {job.job_id, done})
89 16 Map.update!(acc, :completed, &(&1 + 1))
90
91 {:ok, _} ->
92
:-(
done = %{updated | status: :completed, completed_at: DateTime.utc_now()}
93
:-(
:ets.insert(@table, {job.job_id, done})
94
:-(
Map.update!(acc, :completed, &(&1 + 1))
95
96 {:error, reason} when updated.attempts >= updated.max_attempts ->
97
:-(
dead = %{updated | status: :discarded, error: inspect(reason), completed_at: DateTime.utc_now()}
98
:-(
:ets.insert(@table, {job.job_id, dead})
99
:-(
Map.update!(acc, :discarded, &(&1 + 1))
100
101 {:error, reason} ->
102 2 retry = %{updated | status: :available, error: inspect(reason)}
103 2 :ets.insert(@table, {job.job_id, retry})
104 2 Map.update!(acc, :failed, &(&1 + 1))
105 end
106 end)
107
108 22 {:reply, results, state}
109 end
110
111 @impl true
112 def handle_call(:reset, _from, state) do
113 91 :ets.delete_all_objects(@table)
114 91 {:reply, :ok, state}
115 end
116 end
Line Hits Source