stablemates-workhorse 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +318 -1
- data/README.md +24 -1
- data/lib/stablemates/workhorse/active_job.rb +141 -13
- data/lib/stablemates/workhorse/admin.rb +78 -3
- data/lib/stablemates/workhorse/batch_context.rb +6 -2
- data/lib/stablemates/workhorse/checkpoint_fingerprint.rb +118 -0
- data/lib/stablemates/workhorse/context.rb +77 -12
- data/lib/stablemates/workhorse/contracts.rb +14 -7
- data/lib/stablemates/workhorse/dashboard/backend.rb +20 -1
- data/lib/stablemates/workhorse/dashboard/bundle.json +1 -1
- data/lib/stablemates/workhorse/dashboard/read-surface-1.tar.gz +0 -0
- data/lib/stablemates/workhorse/dashboard/v1_generated.rb +149 -1
- data/lib/stablemates/workhorse/dashboard.rb +18 -1
- data/lib/stablemates/workhorse/errors.rb +77 -6
- data/lib/stablemates/workhorse/executor.rb +3 -0
- data/lib/stablemates/workhorse/queue.rb +169 -16
- data/lib/stablemates/workhorse/sql_catalogue_generated.rb +48 -6
- data/lib/stablemates/workhorse/task_group.rb +240 -0
- data/lib/stablemates/workhorse/telemetry.rb +14 -3
- data/lib/stablemates/workhorse/types.rb +5 -3
- data/lib/stablemates/workhorse/version.rb +1 -1
- data/lib/stablemates/workhorse/worker/heartbeat.rb +311 -51
- data/lib/stablemates/workhorse/worker.rb +72 -16
- data/lib/stablemates/workhorse.rb +3 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b030c71db8e2fa0f3b2e14e4ac34442270e1643ea1105efe904bc965ab6e4823
|
|
4
|
+
data.tar.gz: 90256bbd761dfa37265e9e0fce4f5f49534e378361a625bbc6bc27cf24b7634b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9d580a9360fe1772b51567673be3d787ea287ba7df8693d93d8ea4b153620f0a4c0b4c5ad492593830127ccd2c78a1063b5ac5f9174bbbea6aae3f1ca2880b70
|
|
7
|
+
data.tar.gz: d353d8e0ce3d2b03b869212dee916bcead4c41034de9403c83ab1f3e3a74eaaadfa36bf0c0c0c03f22728efdee770ef437c3de369f51d1f36765f5861948679f
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,323 @@
|
|
|
3
3
|
`stablemates-workhorse` gem versions and release notes live here. The gem carries the version the
|
|
4
4
|
other SDKs carry, because every tag names one release of all of them.
|
|
5
5
|
|
|
6
|
+
## 0.9.0 — 2026-10-20
|
|
7
|
+
|
|
8
|
+
The npm packages, Python distribution, Go module, Rust crate, Ruby gem, and JVM artifacts release from one source commit.
|
|
9
|
+
|
|
10
|
+
Requires **schema v73**, Ruby **3.3** or newer, and PostgreSQL **15** or newer.
|
|
11
|
+
|
|
12
|
+
### Upgrade steps
|
|
13
|
+
|
|
14
|
+
**Migrate the schema before starting updated processes.** Run `workhorse schema migrate` before any
|
|
15
|
+
process of this release starts, producers too. Migrations 0073 and 0074 move the schema to version
|
|
16
|
+
**73**. The SDK compatibility floor rises from 69 to **73**, because every SDK statement catalogue
|
|
17
|
+
now names `heartbeat_many_v2` and `heartbeat_v2`. An updated process refuses a schema below version
|
|
18
|
+
73 at startup. A 0.8 process keeps working on version 73, because a migration inside a major line
|
|
19
|
+
only adds.
|
|
20
|
+
|
|
21
|
+
### Heartbeats do not wait for a locked task row
|
|
22
|
+
|
|
23
|
+
Before this release, a heartbeat waited for a task row that another transaction locked. If a
|
|
24
|
+
settlement held the row, the heartbeat then kept the row locked after the settlement committed, and
|
|
25
|
+
a claim skipped the ready task (SM-1434). The worker now renews leases through `heartbeat_many_v2`
|
|
26
|
+
(SM-1443, ADR 0105). It skips a locked row and reports that lease as `busy`: no renewal and no
|
|
27
|
+
ownership verdict. The worker keeps a busy lease, retries only the busy leases after a short delay
|
|
28
|
+
with jitter, and does not move the regular heartbeat schedule. A busy answer never extends the local
|
|
29
|
+
lease watchdog, so a lease that stays busy until the watchdog ends is lost, as before. Old and new
|
|
30
|
+
workers can run together: an old worker keeps calling `heartbeat_many_v1`, which does not change.
|
|
31
|
+
|
|
32
|
+
**One-lease heartbeat without a row lock wait.** Migration 0074 adds `heartbeat_v2` (SM-1461). It
|
|
33
|
+
renews one lease by the rule of `heartbeat_many_v2`, so it reports a locked row as `busy`. This SDK
|
|
34
|
+
names it in its statement catalogue but does not call it. `heartbeat_v1` does not change.
|
|
35
|
+
|
|
36
|
+
### Heartbeat rounds keep a renewal margin and end on the server
|
|
37
|
+
|
|
38
|
+
Before this release, the dedicated heartbeat connection bounded each heartbeat statement by one full
|
|
39
|
+
interval. After a failed round, the heartbeat thread started a new schedule on its new connection.
|
|
40
|
+
With the defaults, one timed-out round thus left about 0 s before the lease watchdog expired
|
|
41
|
+
(SM-1444). Also, when the worker stopped waiting for a round, PostgreSQL continued to run the
|
|
42
|
+
heartbeat statement, and that statement kept its row locks (SM-1442). These changes apply to each
|
|
43
|
+
round on the dedicated heartbeat connection:
|
|
44
|
+
|
|
45
|
+
- **Rounds start on a fixed schedule.** A round falls due one interval after the previous round
|
|
46
|
+
started, not after it ended. The schedule uses a monotonic clock. If a round ends after its next
|
|
47
|
+
slot, the worker starts one overdue round at once and sends no round for the other missed slots.
|
|
48
|
+
- **The round bound is shorter.** A round ends by
|
|
49
|
+
`max(1 ms, interval × 3 / 4, min(50 ms, interval))`. For an interval of 200 ms or more, this is
|
|
50
|
+
three quarters of the interval. With the defaults (lease 30 s, interval 10 s), the bound is 7.5 s,
|
|
51
|
+
so one timed-out round leaves a margin of 2.5 s. An attempt keeps its lease after one timed-out
|
|
52
|
+
round if `2 × interval + round bound < lease`. The worker does not reject an interval that breaks
|
|
53
|
+
this rule. Before this release, each statement of a round had its own bound of one interval. Now
|
|
54
|
+
all statements of one round share one deadline, and a statement that would start after it waits
|
|
55
|
+
for the next round. Each round starts with the next statement in turn, so a statement that is
|
|
56
|
+
always slow cannot keep a later statement from its turn.
|
|
57
|
+
- **Each round sets server timeouts.** The round sends `BEGIN`, then one `set_config` statement for
|
|
58
|
+
`statement_timeout` and `idle_in_transaction_session_timeout`, then `heartbeat_many_v2`, then
|
|
59
|
+
`COMMIT`. Both settings are transaction-local. The heartbeat statement gets one half of the time
|
|
60
|
+
that remains, and each idle gap one eighth. If less than 8 ms remains at the settings statement,
|
|
61
|
+
the round fails before the heartbeat. The worker counts a renewal only after a `COMMIT` that ends
|
|
62
|
+
before the deadline. No setting bounds `COMMIT` on PostgreSQL 15 and later.
|
|
63
|
+
- **A failed round discards its connection only when a statement may still run** (SM-1492). A
|
|
64
|
+
round that expires before it sends the heartbeat statement rolls back and keeps its connection.
|
|
65
|
+
Under load, PostgreSQL can log
|
|
66
|
+
`canceling statement due to statement timeout` for a heartbeat statement. It can also end the
|
|
67
|
+
session with `terminating connection due to idle-in-transaction timeout`. Each message means that
|
|
68
|
+
one round failed. Every task continues, and the next round tries again. A deadlock (`40P01`) also
|
|
69
|
+
fails the round.
|
|
70
|
+
- **A busy retry with less than 20 ms left sends nothing** (SM-1492). A bounded busy retry runs
|
|
71
|
+
only if its deadline is at least 20 ms after its start: the 8 ms that a round needs at its
|
|
72
|
+
settings statement, plus a 12 ms margin. Otherwise the next regular round renews the busy leases.
|
|
73
|
+
- **Replacement connections.** The heartbeat thread keeps the start of the latest round when it
|
|
74
|
+
replaces a failed connection.
|
|
75
|
+
- **100 ms is the smallest supported heartbeat interval** (SM-1471). This applies to `heartbeat:` of
|
|
76
|
+
`Worker`, in seconds: the smallest supported value is 0.1. At 100 ms, the round bound is 75 ms.
|
|
77
|
+
The worker still accepts a shorter interval, and it does not warn. On a loaded host, a shorter
|
|
78
|
+
interval can make each round miss its deadline, and attempts can then lose their leases.
|
|
79
|
+
|
|
80
|
+
Rounds under `shared_heartbeats: true` have no client deadline and set no server timeouts, as
|
|
81
|
+
before. They follow the new schedule. The [heartbeat round
|
|
82
|
+
deadline](../docs/architecture/lifecycle.md#heartbeat-round-deadline) and the [heartbeat
|
|
83
|
+
schedule](../docs/architecture/lifecycle.md#heartbeat-schedule) state each rule.
|
|
84
|
+
|
|
85
|
+
### A payload contract compares the decimals of numbers
|
|
86
|
+
|
|
87
|
+
Before this release, `ContractSchema` compared an Integer with the binary value of a Float in the
|
|
88
|
+
bound keywords, `const`, `enum`, and `uniqueItems` (SM-1539). Thus `1152921504606846980` failed
|
|
89
|
+
`{"maximum": 1.152921504606847e18}`, although PostgreSQL stores the constant as
|
|
90
|
+
`1152921504606847000`. A contract now compares each Float through the decimal of `Float#to_s`
|
|
91
|
+
(ADR 0039). Ruby still reads a stored schema constant that a double cannot hold as a Float
|
|
92
|
+
(SM-1541).
|
|
93
|
+
|
|
94
|
+
### A bad worker interval raises ArgumentError
|
|
95
|
+
|
|
96
|
+
Before this release, a bad value for `heartbeat`, `poll_interval`, `maintenance_interval`,
|
|
97
|
+
`maintenance_routine_interval`, or `registry_interval` raised `NoMethodError`, because these options
|
|
98
|
+
have no upper limit (SM-1511). For example, `heartbeat: 0` did this. Now `Worker.new` raises
|
|
99
|
+
`ArgumentError`, and the message names the option and its lower limit.
|
|
100
|
+
|
|
101
|
+
### The `workhorse.tasks.retried` description matches `api/telemetry.txt`
|
|
102
|
+
|
|
103
|
+
The Ruby worker now describes `workhorse.tasks.retried` as "Failed or expired tasks returned to live
|
|
104
|
+
work", as the contract does (SM-1514). The counter already counted expired tasks. Only the
|
|
105
|
+
description changes.
|
|
106
|
+
|
|
107
|
+
### A wrapped durable replay conflict is terminal
|
|
108
|
+
|
|
109
|
+
Before this release, the Ruby worker checked only the outermost error for a durable replay conflict
|
|
110
|
+
(ADR 0098). If a handler wrapped a conflict in its own error, the worker recorded a retryable
|
|
111
|
+
failure, and the conflict repeated until `max_attempts` (SM-1512). The worker now follows the cause
|
|
112
|
+
chain of the error, as the Go and Rust workers do. It reads at most 16 errors, so a cyclic chain
|
|
113
|
+
cannot stop it. It reads `Exception#cause`, so an error raised in a `rescue` block also counts. A
|
|
114
|
+
wrapped conflict fails the task at once. The failure envelope records the name of the conflict. Its
|
|
115
|
+
fields `message` and `stack` stay those of the outer error.
|
|
116
|
+
|
|
117
|
+
### A heartbeat round sends at most 100 leases in each statement
|
|
118
|
+
|
|
119
|
+
`heartbeat_many_v2` accepts at most 100 leases. A fused completion claim registers its tasks before
|
|
120
|
+
the completing attempts unregister, so a round can hold more leases than the concurrency limit. That
|
|
121
|
+
round raised an error, and no lease in it renewed (SM-1495). The worker now splits a round into
|
|
122
|
+
statements of at most 100 leases. Each statement counts on its own. A failed statement renews none
|
|
123
|
+
of its leases, and the statements before it still count, as before for the statements of each
|
|
124
|
+
worker.
|
|
125
|
+
|
|
126
|
+
## 0.8.0 — 2026-10-13
|
|
127
|
+
|
|
128
|
+
The npm packages, Python distribution, Go module, Rust crate, and Ruby gem release from one source commit.
|
|
129
|
+
|
|
130
|
+
Requires **schema v69**, Ruby **3.3** or newer, and PostgreSQL **15** or newer.
|
|
131
|
+
|
|
132
|
+
### Upgrade steps
|
|
133
|
+
|
|
134
|
+
**Migrate the schema before starting updated processes.** The upgrade is a rolling deployment:
|
|
135
|
+
|
|
136
|
+
1. Run `workhorse schema migrate`. The final schema version is **71**.
|
|
137
|
+
2. Roll out the updated processes.
|
|
138
|
+
|
|
139
|
+
The SDK compatibility floor is schema version **69**, which migration 0070 reaches. The floor rises
|
|
140
|
+
from 54. Every SDK statement catalogue now names the task group functions that migration 0066 adds,
|
|
141
|
+
and `save_checkpoint_v2`, which migration 0069 adds. The dashboard of each SDK calls
|
|
142
|
+
`dashboard_task_groups_v1`, which migration 0067 adds, and `list_newest_task_group_events_v1`, which
|
|
143
|
+
migration 0068 adds. Migration 0070 fences a member add, so an updated process does not add tasks to
|
|
144
|
+
a group without that check. An updated process refuses a schema below version 69 at startup. A 0.7
|
|
145
|
+
process keeps working on version 71, because a migration inside a major line only adds.
|
|
146
|
+
|
|
147
|
+
**Give the eighth member when you build a `TaskCheckpoint`.** `TaskCheckpoint` is a `Data` class.
|
|
148
|
+
0.8.0 adds `input_fingerprint` as its eighth member (SM-1264). A call to `TaskCheckpoint.new` with
|
|
149
|
+
seven positional arguments raises `ArgumentError`. A call with keywords and no
|
|
150
|
+
`input_fingerprint:` also raises it. The gem has no compatible constructor. If your code builds a
|
|
151
|
+
`TaskCheckpoint`, for example in a test, give the eighth value. For a checkpoint without an input,
|
|
152
|
+
give `input_fingerprint: nil` or a positional `nil`.
|
|
153
|
+
|
|
154
|
+
Migration 0066 (`0066-add-task-groups-with-completion-callbacks.sql`) adds task groups (SM-1231,
|
|
155
|
+
[ADR 0095](https://github.com/stablemates/workhorse/blob/main/docs/decisions/0095-add-task-groups-with-completion-callbacks.md)).
|
|
156
|
+
A task group is a set of full-tier tasks that Workhorse tracks as one unit. When the group is
|
|
157
|
+
sealed and every member task is finished, Workhorse enqueues its `onFinish`, `onSuccess`, or
|
|
158
|
+
`onFailure` callback task. The migration adds the `task_group`, `task_group_member`,
|
|
159
|
+
`task_group_callback`, and `task_group_event` tables, and a trigger on `task_outcome` that counts
|
|
160
|
+
each member outcome in the transaction that writes it. A queue purge counts a purged member task as
|
|
161
|
+
`canceled`. The `terminal_storage` routine keeps member tasks while their group is not finished,
|
|
162
|
+
reports a new `task_groups` phase after `terminal_tasks`, and deletes a finished group after its
|
|
163
|
+
callback tasks are terminal. New SQLSTATEs: `P1008` (the task group does not accept tasks), `P1009`
|
|
164
|
+
(member limit of 100,000), and `P1010` (an enqueue idempotency hit returns a task that is not a
|
|
165
|
+
member of the group).
|
|
166
|
+
|
|
167
|
+
Migration 0067 (`0067-count-task-group-outcomes-in-counter-shards.sql`) counts task group outcomes
|
|
168
|
+
in counter shards (SM-1245,
|
|
169
|
+
[ADR 0096](https://github.com/stablemates/workhorse/blob/main/docs/decisions/0096-count-task-group-outcomes-in-counter-shards.md)).
|
|
170
|
+
Each group gets 32 rows in the new `task_group_shard` table, and a member outcome locks only its
|
|
171
|
+
shard row at commit. The outcome locks the group row only when the pending count of its shard
|
|
172
|
+
becomes 0. The migration moves the counts of existing groups into the shards. It locks `task_group`
|
|
173
|
+
in `EXCLUSIVE` mode while it runs, so task group changes wait for it. The counts on a `task_group`
|
|
174
|
+
row stay 0 until the group finishes, so read the counts through `get_task_group_v1`. The migration
|
|
175
|
+
also adds `dashboard_task_groups_v1`, which lists task groups one page at a time. The dashboard of
|
|
176
|
+
each SDK reads the task group list through it (SM-1237).
|
|
177
|
+
|
|
178
|
+
Migration 0068 (`0068-read-the-newest-task-group-events.sql`) adds
|
|
179
|
+
`list_newest_task_group_events_v1`, which returns the newest events of a task group first
|
|
180
|
+
(SM-1254). The task group detail of the dashboard reads its events through it. A group with more
|
|
181
|
+
than 1,000 events now shows its newest 100 events. `eventsTruncated` is true when the group has
|
|
182
|
+
older events that the detail does not show. `list_task_group_events_v1` does not change. The
|
|
183
|
+
migration changes no stored data.
|
|
184
|
+
|
|
185
|
+
Migration 0069 (`0069-check-the-input-of-a-checkpoint-on-replay.sql`) adds the nullable column
|
|
186
|
+
`task_checkpoint.input_fingerprint` and `save_checkpoint_v2` (SM-1211,
|
|
187
|
+
[ADR 0098](https://github.com/stablemates/workhorse/blob/main/docs/decisions/0098-check-the-input-of-a-checkpoint-on-replay.md)).
|
|
188
|
+
If a checkpoint name exists, `save_checkpoint_v2` returns `input_conflict` when the stored and the
|
|
189
|
+
given input fingerprints are both present and differ. `save_checkpoint_v1` now calls
|
|
190
|
+
`save_checkpoint_v2` with no fingerprint, so its results do not change. The checkpoint reads of each
|
|
191
|
+
statement catalogue return the new column. Existing rows keep a NULL fingerprint. The migration adds
|
|
192
|
+
the column without a table scan.
|
|
193
|
+
|
|
194
|
+
Migration 0070 (`0070-fence-a-task-group-member-add-and-index-the-group-list.sql`) fences a task
|
|
195
|
+
group member add (SM-1273). `add_task_group_members_v1` now locks the runtime row of the member task
|
|
196
|
+
first, before the shard rows and the group row, as a completion does. Then it refuses the add with
|
|
197
|
+
`P1008` and the reason `not_running_member` if the lease of the member task expired, its deadline or
|
|
198
|
+
attempt timeout passed, or a cancel was requested. Before, a worker whose lease expired could add
|
|
199
|
+
tasks after Workhorse gave the member task to a new worker. The signature does not change. The
|
|
200
|
+
migration also adds `task_group_state_created_idx`, so `dashboard_task_groups_v1` reads one page of
|
|
201
|
+
a state without a read of the groups of other states. The migration is a non-transactional step: it
|
|
202
|
+
builds the index with `CREATE INDEX CONCURRENTLY`, so writes to `task_group` continue. If it fails
|
|
203
|
+
part-way, drop the invalid index before the rerun, as `docs/schema-lifecycle.md` describes. It
|
|
204
|
+
changes no stored data.
|
|
205
|
+
|
|
206
|
+
Migration 0071 (`0071-lock-purged-runtime-rows-in-identity-order.sql`) makes a queue purge lock its
|
|
207
|
+
tasks in identity order (SM-1282). Before, `purge_queue_internal_v1` locked the runtime rows and the
|
|
208
|
+
task rows of the queue in scan order. An enqueue that named two of those tasks as prerequisites
|
|
209
|
+
locks them in identity order. Thus the purge and the enqueue could wait for each other, and
|
|
210
|
+
PostgreSQL aborted one of them with `40P01`. The purge now locks every runtime row, then every task
|
|
211
|
+
row, each in identity order. The signature and the deleted count do not change. The migration does
|
|
212
|
+
not raise the SDK compatibility floor, because no SDK statement changes and every process gets the
|
|
213
|
+
new lock order when the migration commits. It changes no stored data.
|
|
214
|
+
|
|
215
|
+
Migration 0072 (`0072-purge-a-queue-that-holds-a-prerequisite-of-a-live-dependent.sql`) lets a
|
|
216
|
+
queue purge succeed when it holds a prerequisite of a waiting dependent (SM-1284,
|
|
217
|
+
[ADR 0101](https://github.com/stablemates/workhorse/blob/main/docs/decisions/0101-cancel-a-purged-prerequisite-whose-dependent-stays.md)).
|
|
218
|
+
Before, the purge deleted that prerequisite, and PostgreSQL refused the delete with `23001`. The
|
|
219
|
+
whole purge then failed, also when the dependent was in the same queue. Now the purge deletes a
|
|
220
|
+
dependent in the same purge with its prerequisite. If the dependent stays, for example in another
|
|
221
|
+
queue, the purge cancels the prerequisite instead of a delete. The dependent's own
|
|
222
|
+
`on_cancellation` policy then decides its outcome, as after a cancel. The canceled task keeps its
|
|
223
|
+
history and its keys. The purge count, `deleted_count` and `deletedCount`, includes the canceled
|
|
224
|
+
tasks. The purge locks the dependents after its own tasks, as a cancel does, so it has no
|
|
225
|
+
deadlock that a cancel of the same tasks does not also have. Two purges run one after the
|
|
226
|
+
other, also for different queues. The migration does not raise the SDK compatibility
|
|
227
|
+
floor, because no SDK statement or result shape changes. It changes no stored data.
|
|
228
|
+
|
|
229
|
+
### Task groups
|
|
230
|
+
|
|
231
|
+
The Ruby SDK adds task groups (SM-1236). Use the block form to create a group, add its member
|
|
232
|
+
tasks, and seal it in one statement, so in one transaction:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
result = queue.task_group(on_finish: Stablemates::Workhorse::EnqueueRequest.new(
|
|
236
|
+
task_type: "import.report", payload: {"importId" => 42}
|
|
237
|
+
)) do |group|
|
|
238
|
+
rows.each { |row| group.enqueue("import.row", {"row" => row}) }
|
|
239
|
+
end
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
- `Queue#task_group` takes the `on_finish:`, `on_success:`, and `on_failure:` callbacks. Each
|
|
243
|
+
callback is an `EnqueueRequest` without idempotency, debounce, or throttle. The block receives a
|
|
244
|
+
`TaskGroupBuilder` with `add` and `enqueue`. If the block raises, nothing is enqueued.
|
|
245
|
+
- `Queue#create_task_group`, `Queue#add_to_task_group`, and `Queue#seal_task_group` build a group in
|
|
246
|
+
more than one transaction.
|
|
247
|
+
- `Queue#get_task_group`, `Queue#list_task_group_members`, and `Queue#find_task_group` read a
|
|
248
|
+
group. `Admin` has `get_task_group`, `list_task_group_members`, `list_task_group_events`,
|
|
249
|
+
`cancel_task_group`, `list_task_group_drift`, and `repair_task_group_drift`.
|
|
250
|
+
- `HandlerContext#task_group` and `BatchHandlerContext#task_group` return a `TaskGroupContext`.
|
|
251
|
+
Its `get` returns the `TaskGroupLink` of the task, with the group ID and the counts. Its `add`
|
|
252
|
+
adds tasks from a running member task, also after the seal. If a request names no queue, `add`
|
|
253
|
+
puts the task in the queue of the member task, not in the first queue of the worker (SM-1251,
|
|
254
|
+
[ADR 0097](https://github.com/stablemates/workhorse/blob/main/docs/decisions/0097-put-work-that-a-handler-creates-on-the-queue-of-its-task.md)).
|
|
255
|
+
- `Stablemates::Workhorse::ActiveJob.task_group` runs a block in which each job that the
|
|
256
|
+
Workhorse adapter enqueues becomes a member task. Its callbacks can be Active Job instances,
|
|
257
|
+
such as `ReportJob.new(import.id)`. A job reads its group through
|
|
258
|
+
`Stablemates::Workhorse::ActiveJob.current_task_group`.
|
|
259
|
+
- A job class with `enqueue_after_transaction_commit` set to true cannot join a task group. Rails
|
|
260
|
+
would enqueue that job after the transaction commits, so outside the group. If a task group block
|
|
261
|
+
enqueues such a job through the Workhorse adapter, the SDK raises `ArgumentError` before Rails
|
|
262
|
+
defers the job. Set `enqueue_after_transaction_commit` to false on that job class. The group
|
|
263
|
+
statement runs in the caller's transaction, so the member tasks still commit with it.
|
|
264
|
+
- `TaskGroupClosedError` (`P1008`), `TaskGroupLimitExceededError` (`P1009`), and
|
|
265
|
+
`TaskGroupMembershipConflictError` (`P1010`) report the task group refusals.
|
|
266
|
+
- `Queue#list_task_group_members` and `Admin#list_task_group_members` return a nil `next_after`
|
|
267
|
+
on the last page, also when the page is full (SM-1277). Before, a full last page gave a
|
|
268
|
+
`next_after` that read an empty page. A full page now costs one more read of one member.
|
|
269
|
+
`TaskGroup#callbacks` keeps the order `on_finish`, `on_success`, `on_failure`, and a test now
|
|
270
|
+
asserts it. The change adds no SQL function and no migration.
|
|
271
|
+
|
|
272
|
+
### Checkpoint input
|
|
273
|
+
|
|
274
|
+
A checkpoint finds its stored result by name only. If a deployment changes the input of a checkpoint
|
|
275
|
+
operation, a replay returns the result of the old input and gives no error. A Ruby handler can now
|
|
276
|
+
give the input of a checkpoint (SM-1264,
|
|
277
|
+
[ADR 0098](https://github.com/stablemates/workhorse/blob/main/docs/decisions/0098-check-the-input-of-a-checkpoint-on-replay.md)):
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
context.checkpoint("charge-card", input: {"amountCents" => amount}) { gateway.charge(amount) }
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
- `HandlerContext#checkpoint` and `BatchHandlerContext#checkpoint` take the `input:` keyword, a JSON
|
|
284
|
+
value. The checkpoint stores the fingerprint of the input with its value, through
|
|
285
|
+
`save_checkpoint_v2`. A call without `input:` keeps its current behavior. `input: nil` gives the
|
|
286
|
+
JSON input null.
|
|
287
|
+
- If the stored checkpoint, or an overlapping call with the same name, has the fingerprint of a
|
|
288
|
+
different input, the call raises `CheckpointInputConflictError` and the block does not run. If the
|
|
289
|
+
call or the stored checkpoint has no input, the call compares nothing.
|
|
290
|
+
- `CheckpointInputConflictError` is a subclass of `ConflictError`. The worker fails the task at once
|
|
291
|
+
and keeps the current attempt, as for every replay conflict. The task error names the class.
|
|
292
|
+
- `Stablemates::Workhorse.checkpoint_input_fingerprint(input)` returns the fingerprint: the SHA-256
|
|
293
|
+
digest of the RFC 8785 canonical JSON of the input, as 64 lowercase hexadecimal characters. It
|
|
294
|
+
refuses an Integer or a Float larger than 2**53 - 1 in magnitude, such as `1e21`, because RFC 8785
|
|
295
|
+
writes each number as an IEEE 754 double. Every SDK refuses the same inputs (SM-1274).
|
|
296
|
+
- `TaskCheckpoint#input_fingerprint` returns the stored fingerprint, or nil. `Admin#get_checkpoint`,
|
|
297
|
+
`Admin#list_checkpoints`, and `HandlerContext#get_checkpoint` return it.
|
|
298
|
+
|
|
299
|
+
### Behaviour changes
|
|
300
|
+
|
|
301
|
+
- If a child task request names no queue, `run_child`, `run_children`, and `run_children_all` put
|
|
302
|
+
the child in the queue of the parent task (SM-1260, [ADR 0097](https://github.com/stablemates/workhorse/blob/main/docs/decisions/0097-put-work-that-a-handler-creates-on-the-queue-of-its-task.md)).
|
|
303
|
+
Before, the child went to the first queue in the `queues:` list of the worker. To keep the old
|
|
304
|
+
queue, name that queue on the request, for example `queue: "default"`.
|
|
305
|
+
A parent task that created a child before the upgrade can replay that request after the upgrade.
|
|
306
|
+
If the replayed request names no queue, it keeps the queue of the stored child, so the parent
|
|
307
|
+
task joins its child.
|
|
308
|
+
- `Worker#run_once` does not change: a pass that released every task that it claimed returns
|
|
309
|
+
`false`, and the next call claims at once (SM-1259, [ADR 0099](https://github.com/stablemates/workhorse/blob/main/docs/decisions/0099-report-a-released-only-single-pass-as-no-progress.md)). TypeScript
|
|
310
|
+
`runOnce()` now does the same. If you call the single-pass call in a loop, wait after `false`.
|
|
311
|
+
- A continuous worker that released every task of a claim now waits for its next poll before it
|
|
312
|
+
claims again (SM-1247). A worker releases a task when it has no handler for the task type, for
|
|
313
|
+
example during a rolling deployment. Before, the release notification ended the empty wait of the
|
|
314
|
+
same worker, so it claimed the task again after 0 to 50 ms. One listening worker released one task
|
|
315
|
+
about 30 times each second. Now a notification does not end this release wait. Only its deadline,
|
|
316
|
+
a stop, or a pause ends it. The deadline is the poll interval while the listener is active, or the
|
|
317
|
+
poll backoff without a listener. Thus a worker claims a task that it cannot run at most once in
|
|
318
|
+
each poll interval.
|
|
319
|
+
- The Rack dashboard refuses a mutation whose `audit.requestId` is more than 512 UTF-8 bytes
|
|
320
|
+
with `BAD_REQUEST`, before any SQL runs (SM-1253). It measures the value as sent, before trim.
|
|
321
|
+
Before, the SQL audit function refused the value, and the client got an internal server error.
|
|
322
|
+
|
|
6
323
|
## 0.7.1 — 2026-10-09
|
|
7
324
|
|
|
8
325
|
The npm packages, Python distribution, Go module, Rust crate, and Ruby gem release from one source commit.
|
|
@@ -316,7 +633,7 @@ Requires **schema v43**, Ruby **3.3** or newer, and PostgreSQL **15** or newer.
|
|
|
316
633
|
renews the lease, `Worker#stop` still wakes the dispatcher, and a second signal still exits the
|
|
317
634
|
process. The first failure of each logger is written to standard error.
|
|
318
635
|
- Add the Active Job adapter, selected with `config.active_job.queue_adapter =
|
|
319
|
-
:stablemates_workhorse`. It requires Active Job 8.0 or later. A default job runs under the
|
|
636
|
+
:stablemates_workhorse`. It requires Active Job 8.0 or later. A default job runs under the
|
|
320
637
|
`active_job` task type. A typed job declares its task type with `workhorse_options` and carries
|
|
321
638
|
one JSON `Hash`, so another SDK can enqueue or run it. `Stablemates::Workhorse::ActiveJob.handle`
|
|
322
639
|
registers both formats on a `Worker`.
|
data/README.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
The Ruby queue client, operator client, and worker runtime for the Workhorse durable task queue for
|
|
4
4
|
PostgreSQL.
|
|
5
5
|
|
|
6
|
+
<picture>
|
|
7
|
+
<source media="(max-width: 600px) and (prefers-color-scheme: dark)" srcset="https://workhorse.run/diagrams/readme-overview-narrow-dark.png">
|
|
8
|
+
<source media="(max-width: 600px)" srcset="https://workhorse.run/diagrams/readme-overview-narrow-light.png">
|
|
9
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://workhorse.run/diagrams/readme-overview-dark.png">
|
|
10
|
+
<img src="https://workhorse.run/diagrams/readme-overview-light.png" width="960" alt="One protocol for your workers and your dashboard">
|
|
11
|
+
</picture>
|
|
12
|
+
|
|
6
13
|
> **Public beta:** Workhorse is usable for evaluation and early production adoption. A 0.x minor
|
|
7
14
|
> release may change behaviour, so read the
|
|
8
15
|
> [changelog](https://github.com/stablemates/workhorse/blob/main/ruby/CHANGELOG.md) before you
|
|
@@ -23,7 +30,7 @@ bundle add connection_pool
|
|
|
23
30
|
Install the schema once, as a deployment step. The application never installs or migrates it.
|
|
24
31
|
|
|
25
32
|
```bash
|
|
26
|
-
npx --package @stablemates/workhorse@0.
|
|
33
|
+
npx --package @stablemates/workhorse@0.9.0 workhorse schema install
|
|
27
34
|
```
|
|
28
35
|
|
|
29
36
|
The machine that runs that deployment step needs Node.js 22 or newer. The application itself needs
|
|
@@ -70,6 +77,22 @@ effects. `ruby/examples/` also holds a transactional enqueue, a dedicated worker
|
|
|
70
77
|
orchestration of child tasks, signals, and human decisions. An integration spec runs every one of
|
|
71
78
|
them against PostgreSQL.
|
|
72
79
|
|
|
80
|
+
## Task groups, checkpoint inputs, and single passes
|
|
81
|
+
|
|
82
|
+
- A [task group](https://workhorse.run/docs/task-groups) tracks many tasks as one unit. After the
|
|
83
|
+
group is sealed and every member task is finished, Workhorse enqueues the callback tasks of the
|
|
84
|
+
group. The `Queue#task_group` block creates a group, adds its tasks, and seals it in one
|
|
85
|
+
transaction.
|
|
86
|
+
- A handler can give the [input of a
|
|
87
|
+
checkpoint](https://workhorse.run/docs/durable-execution#check-the-input-of-a-checkpoint). Give it
|
|
88
|
+
as `context.checkpoint(name, input: input) { ... }`. If a later run gives a different input, the
|
|
89
|
+
call raises `CheckpointInputConflictError`, the task fails, and the operation does not run again.
|
|
90
|
+
If the stored checkpoint or the call has no input, Workhorse compares nothing.
|
|
91
|
+
- `Worker#run_once` returns true only when a handler ran. It returns false after a pass that only
|
|
92
|
+
[released the tasks that it
|
|
93
|
+
claimed](https://workhorse.run/docs/workers#return-a-task-that-the-worker-cannot-run). If you call
|
|
94
|
+
it in a loop, wait after false.
|
|
95
|
+
|
|
73
96
|
## Package boundary
|
|
74
97
|
|
|
75
98
|
The gem is `stablemates-workhorse`, and its namespace is `Stablemates::Workhorse`.
|
|
@@ -25,8 +25,9 @@ module Stablemates
|
|
|
25
25
|
# The keywords +workhorse_options+ accepts. A job class that passes any other key raises.
|
|
26
26
|
OPTION_KEYS = %i[task_type max_attempts tags concurrency_key].freeze
|
|
27
27
|
WORKER_KEY = :stablemates_workhorse_active_job_worker
|
|
28
|
+
CONTEXT_KEY = :stablemates_workhorse_active_job_context
|
|
28
29
|
private_constant :JOB_ID_TAG, :CLASS_TAG, :MAX_OPTION_TAGS, :MAX_TAG_LENGTH, :MAX_TASK_TYPE_BYTES,
|
|
29
|
-
:WORKER_KEY
|
|
30
|
+
:WORKER_KEY, :CONTEXT_KEY
|
|
30
31
|
|
|
31
32
|
# Declares Workhorse options on a job class. Include it and call +workhorse_options+.
|
|
32
33
|
module Options
|
|
@@ -77,13 +78,50 @@ module Stablemates
|
|
|
77
78
|
typed[task_type] = job_class
|
|
78
79
|
end
|
|
79
80
|
admin = Admin.new(worker.executor)
|
|
80
|
-
worker.handle(TASK_TYPE) { |payload, context| running(worker) { run_default(payload, context.task) } }
|
|
81
|
+
worker.handle(TASK_TYPE) { |payload, context| running(worker, context) { run_default(payload, context.task) } }
|
|
81
82
|
typed.each do |task_type, job_class|
|
|
82
|
-
worker.handle(task_type)
|
|
83
|
+
worker.handle(task_type) do |payload, context|
|
|
84
|
+
running(worker, context) { run_typed(job_class, payload, context.task, admin) }
|
|
85
|
+
end
|
|
83
86
|
end
|
|
84
87
|
worker
|
|
85
88
|
end
|
|
86
89
|
|
|
90
|
+
# Creates a task group (ADR 0095), runs the block, and seals the group, in one statement. Every
|
|
91
|
+
# job the block enqueues through the Workhorse adapter, with +perform_later+ or
|
|
92
|
+
# +perform_all_later+, becomes a member task. The jobs are enqueued when the block returns,
|
|
93
|
+
# and each job's +provider_job_id+ is set then. If the block raises, nothing is enqueued.
|
|
94
|
+
#
|
|
95
|
+
# A job class with +enqueue_after_transaction_commit+ set to true cannot join a group. Rails
|
|
96
|
+
# would enqueue it after the transaction commits, so after the block ends and outside the
|
|
97
|
+
# group. If the block enqueues such a job through the Workhorse adapter, the call raises
|
|
98
|
+
# ArgumentError before Rails defers the job. Set +enqueue_after_transaction_commit+ to false on
|
|
99
|
+
# that job class. The group statement runs in the caller's transaction, so the member tasks
|
|
100
|
+
# still commit with it.
|
|
101
|
+
#
|
|
102
|
+
# Each callback is an Active Job instance, such as +ReportJob.new(import.id)+, an
|
|
103
|
+
# EnqueueRequest, or nil. +on_finish+ runs when every member task is finished, +on_success+
|
|
104
|
+
# when every member task succeeded, and +on_failure+ when one or more did not succeed.
|
|
105
|
+
# +queue+ is the Queue that creates the group. It defaults to the queue of the Workhorse
|
|
106
|
+
# adapter that ActiveJob::Base uses. Returns a TaskGroupBlockResult.
|
|
107
|
+
def task_group(on_finish: nil, on_success: nil, on_failure: nil, queue: nil, &)
|
|
108
|
+
raise ArgumentError, "task_group needs a block" unless block_given?
|
|
109
|
+
raise ArgumentError, "queue must be a Queue or nil" unless queue.nil? || queue.is_a?(Queue)
|
|
110
|
+
|
|
111
|
+
queue ||= adapter_queue
|
|
112
|
+
queue.task_group(on_finish: callback(on_finish), on_success: callback(on_success),
|
|
113
|
+
on_failure: callback(on_failure), &)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The TaskGroupLink of the job that runs now, as a member task or as a callback task, or nil
|
|
117
|
+
# for a job with no group. Raises ArgumentError outside a job that a Workhorse worker runs.
|
|
118
|
+
def current_task_group
|
|
119
|
+
context = Thread.current[CONTEXT_KEY]
|
|
120
|
+
raise ArgumentError, "current_task_group needs a job that a Workhorse worker runs" if context.nil?
|
|
121
|
+
|
|
122
|
+
context.task_group.get
|
|
123
|
+
end
|
|
124
|
+
|
|
87
125
|
# The request that enqueues +job+, or ArgumentError before any statement runs. An invalid
|
|
88
126
|
# priority raises ActiveJob::EnqueueError, which Active Job records on the job.
|
|
89
127
|
def request(job, run_at) # :nodoc:
|
|
@@ -104,6 +142,22 @@ module Stablemates
|
|
|
104
142
|
# The worker running the current job, or nil outside a worker.
|
|
105
143
|
def current_worker = Thread.current[WORKER_KEY] # :nodoc:
|
|
106
144
|
|
|
145
|
+
# Raises ArgumentError when a task group block is open and one of +jobs+ goes to the Workhorse
|
|
146
|
+
# adapter with +enqueue_after_transaction_commit+ set. Rails would enqueue that job after the
|
|
147
|
+
# commit, outside the group.
|
|
148
|
+
def refuse_deferral(jobs) # :nodoc:
|
|
149
|
+
return if TaskGroupBuilder.current.nil?
|
|
150
|
+
|
|
151
|
+
jobs.each do |job|
|
|
152
|
+
next unless job.class.queue_adapter.is_a?(::ActiveJob::QueueAdapters::StablematesWorkhorseAdapter)
|
|
153
|
+
next unless DEFERRING.include?(job.class.enqueue_after_transaction_commit)
|
|
154
|
+
|
|
155
|
+
raise ArgumentError, "#{job.class.name} sets enqueue_after_transaction_commit, so Rails would enqueue " \
|
|
156
|
+
"it after the transaction commits and outside the task group; set " \
|
|
157
|
+
"enqueue_after_transaction_commit to false on #{job.class.name}"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
107
161
|
def validate_options(class_name, options) # :nodoc:
|
|
108
162
|
unknown = options.keys - OPTION_KEYS
|
|
109
163
|
raise ArgumentError, "workhorse_options does not accept #{unknown.map(&:inspect).join(", ")}" unless
|
|
@@ -165,13 +219,31 @@ module Stablemates
|
|
|
165
219
|
adapter_tags + (option || [])
|
|
166
220
|
end
|
|
167
221
|
|
|
168
|
-
def running(worker)
|
|
169
|
-
previous = Thread.current[WORKER_KEY]
|
|
222
|
+
def running(worker, context)
|
|
223
|
+
previous = [Thread.current[WORKER_KEY], Thread.current[CONTEXT_KEY]]
|
|
170
224
|
Thread.current[WORKER_KEY] = worker
|
|
225
|
+
Thread.current[CONTEXT_KEY] = context
|
|
171
226
|
yield
|
|
172
227
|
nil
|
|
173
228
|
ensure
|
|
174
|
-
Thread.current[WORKER_KEY] = previous
|
|
229
|
+
Thread.current[WORKER_KEY], Thread.current[CONTEXT_KEY] = previous
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def callback(job)
|
|
233
|
+
case job
|
|
234
|
+
when nil, EnqueueRequest then job
|
|
235
|
+
when ::ActiveJob::Base then request(job, job.scheduled_at && Time.at(job.scheduled_at.to_f))
|
|
236
|
+
else raise ArgumentError, "a task group callback must be an Active Job, an EnqueueRequest, or nil"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def adapter_queue
|
|
241
|
+
adapter = ::ActiveJob::Base.queue_adapter
|
|
242
|
+
unless adapter.is_a?(::ActiveJob::QueueAdapters::StablematesWorkhorseAdapter)
|
|
243
|
+
raise ArgumentError, "task_group needs a queue: ActiveJob::Base does not use the Workhorse adapter"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
adapter.workhorse_queue
|
|
175
247
|
end
|
|
176
248
|
|
|
177
249
|
def run_default(payload, task)
|
|
@@ -204,6 +276,42 @@ module Stablemates
|
|
|
204
276
|
concurrency_key: "must be a non-empty String or respond to call"
|
|
205
277
|
}.freeze
|
|
206
278
|
private_constant :OPTION_RULES
|
|
279
|
+
|
|
280
|
+
# The +enqueue_after_transaction_commit+ values that defer an enqueue. Active Job 8.0 also
|
|
281
|
+
# accepts the deprecated :always.
|
|
282
|
+
DEFERRING = [true, :always].freeze
|
|
283
|
+
private_constant :DEFERRING
|
|
284
|
+
|
|
285
|
+
# Checks one job before Rails can defer its enqueue. A prepend on ActiveJob::Base runs before
|
|
286
|
+
# the module that Rails includes for the deferral. Internal to the SDK.
|
|
287
|
+
module JobDeferralCheck # :nodoc:
|
|
288
|
+
private
|
|
289
|
+
|
|
290
|
+
def raw_enqueue(...)
|
|
291
|
+
::Stablemates::Workhorse::ActiveJob.refuse_deferral([self])
|
|
292
|
+
super
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Checks the jobs of +perform_all_later+ before Rails can defer them. Internal to the SDK.
|
|
297
|
+
module BatchDeferralCheck # :nodoc:
|
|
298
|
+
def perform_all_later(*jobs)
|
|
299
|
+
::Stablemates::Workhorse::ActiveJob.refuse_deferral(jobs.flatten)
|
|
300
|
+
super
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
private_constant :JobDeferralCheck, :BatchDeferralCheck
|
|
304
|
+
|
|
305
|
+
# The checks wait for ActiveJob::Base. Loading it here would run its load hooks, and Rails
|
|
306
|
+
# would then look up this adapter before this file defines it.
|
|
307
|
+
::ActiveSupport.on_load(:active_job) { prepend(JobDeferralCheck) }
|
|
308
|
+
::ActiveJob.singleton_class.prepend(BatchDeferralCheck)
|
|
309
|
+
# Active Job 8.1 splits +perform_all_later+ in a module that it prepends when Rails includes
|
|
310
|
+
# the deferral, which can be after this file loads. The check therefore also goes into that
|
|
311
|
+
# module, so it runs before the split.
|
|
312
|
+
if ::ActiveJob::EnqueueAfterTransactionCommit.const_defined?(:ActiveJobMethods, false)
|
|
313
|
+
::ActiveJob::EnqueueAfterTransactionCommit::ActiveJobMethods.prepend(BatchDeferralCheck)
|
|
314
|
+
end
|
|
207
315
|
end
|
|
208
316
|
end
|
|
209
317
|
end
|
|
@@ -226,8 +334,9 @@ module ActiveJob
|
|
|
226
334
|
|
|
227
335
|
# Enqueues +jobs+ in atomic chunks of the shared batch limit and returns how many it enqueued.
|
|
228
336
|
# A job with an invalid priority records the error and is skipped. Each job reports only the
|
|
229
|
-
# error of this call.
|
|
337
|
+
# error of this call. Inside the block form of a task group, the jobs join the group instead.
|
|
230
338
|
def enqueue_all(jobs)
|
|
339
|
+
builder = ::Stablemates::Workhorse::TaskGroupBuilder.current
|
|
231
340
|
pending = jobs.filter_map do |job|
|
|
232
341
|
job.enqueue_error = nil
|
|
233
342
|
[job, workhorse.request(job, job.scheduled_at && Time.at(job.scheduled_at.to_f))]
|
|
@@ -236,6 +345,8 @@ module ActiveJob
|
|
|
236
345
|
job.successfully_enqueued = false
|
|
237
346
|
nil
|
|
238
347
|
end
|
|
348
|
+
return grouped(builder, pending) if builder
|
|
349
|
+
|
|
239
350
|
pending.each_slice(::Stablemates::Workhorse::SqlCatalogue::MAX_ENQUEUE_BATCH_SIZE) do |chunk|
|
|
240
351
|
results = queue.enqueue_many(chunk.map(&:last))
|
|
241
352
|
chunk.zip(results) do |(job, _request), result|
|
|
@@ -252,20 +363,37 @@ module ActiveJob
|
|
|
252
363
|
!worker.nil? && worker.stopping?
|
|
253
364
|
end
|
|
254
365
|
|
|
366
|
+
# The Queue this adapter enqueues through. Internal to the SDK.
|
|
367
|
+
def workhorse_queue # :nodoc:
|
|
368
|
+
@queue || @lock.synchronize do
|
|
369
|
+
@queue ||= ::Stablemates::Workhorse::Queue.new(
|
|
370
|
+
@executor || ::Stablemates::Workhorse::ActiveRecordExecutor.new(::ActiveRecord::Base)
|
|
371
|
+
)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
255
375
|
private
|
|
256
376
|
|
|
257
377
|
def workhorse = ::Stablemates::Workhorse::ActiveJob
|
|
258
378
|
|
|
379
|
+
def queue = workhorse_queue
|
|
380
|
+
|
|
381
|
+
# Inside the block form of a task group, a job joins the group. The group enqueues it when the
|
|
382
|
+
# block returns.
|
|
259
383
|
def enqueue_one(job, run_at)
|
|
260
|
-
|
|
384
|
+
request = workhorse.request(job, run_at)
|
|
385
|
+
builder = ::Stablemates::Workhorse::TaskGroupBuilder.current
|
|
386
|
+
return job.provider_job_id = queue.enqueue_many([request]).first.task_id if builder.nil?
|
|
387
|
+
|
|
388
|
+
builder.add_request(request) { |result| job.provider_job_id = result.task_id }
|
|
261
389
|
end
|
|
262
390
|
|
|
263
|
-
def
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
)
|
|
391
|
+
def grouped(builder, pending)
|
|
392
|
+
pending.each do |job, request|
|
|
393
|
+
builder.add_request(request) { |result| job.provider_job_id = result.task_id }
|
|
394
|
+
job.successfully_enqueued = true
|
|
268
395
|
end
|
|
396
|
+
pending.length
|
|
269
397
|
end
|
|
270
398
|
end
|
|
271
399
|
end
|