wurk 1.7.6 → 1.8.1
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/README.md +26 -5
- data/app/controllers/concerns/wurk/sse_streaming.rb +2 -1
- data/app/controllers/concerns/wurk/stream_concurrency_guard.rb +33 -9
- data/app/controllers/wurk/api/pagination.rb +25 -7
- data/app/controllers/wurk/api_controller.rb +49 -25
- data/app/controllers/wurk/extensions_controller.rb +8 -2
- data/app/controllers/wurk/profiles_controller.rb +45 -9
- data/config/routes.rb +31 -10
- data/lib/generators/wurk/install/templates/wurk.rb +1 -1
- data/lib/sidekiq/capsule.rb +4 -0
- data/lib/sidekiq/component.rb +4 -0
- data/lib/sidekiq/config.rb +4 -0
- data/lib/sidekiq/deploy.rb +4 -0
- data/lib/sidekiq/embedded.rb +4 -0
- data/lib/sidekiq/fetch.rb +4 -0
- data/lib/sidekiq/iterable_job.rb +4 -0
- data/lib/sidekiq/job/interrupt_handler.rb +4 -0
- data/lib/sidekiq/job/iterable/active_record_enumerator.rb +4 -0
- data/lib/sidekiq/job/iterable/csv_enumerator.rb +4 -0
- data/lib/sidekiq/job/iterable.rb +4 -0
- data/lib/sidekiq/job_logger.rb +4 -0
- data/lib/sidekiq/job_util.rb +4 -0
- data/lib/sidekiq/loader.rb +4 -0
- data/lib/sidekiq/logger.rb +4 -0
- data/lib/sidekiq/metrics/query.rb +4 -0
- data/lib/sidekiq/middleware/current_attributes.rb +7 -0
- data/lib/sidekiq/middleware/i18n.rb +6 -0
- data/lib/sidekiq/middleware/modules.rb +4 -0
- data/lib/sidekiq/pro/web.rb +5 -0
- data/lib/sidekiq/profiler.rb +4 -0
- data/lib/sidekiq/redis_client_adapter.rb +4 -0
- data/lib/sidekiq/test_api.rb +5 -0
- data/lib/sidekiq/testing/inline.rb +8 -0
- data/lib/sidekiq/transaction_aware_client.rb +4 -0
- data/lib/sidekiq/worker_compatibility_alias.rb +4 -0
- data/lib/sidekiq-ent/periodic/testing.rb +5 -0
- data/lib/sidekiq-ent/web.rb +5 -0
- data/lib/sidekiq-ent.rb +7 -0
- data/lib/sidekiq-pro.rb +7 -0
- data/lib/wurk/api/fast.rb +8 -33
- data/lib/wurk/api/idempotency.rb +28 -5
- data/lib/wurk/api/swarm.rb +1 -1
- data/lib/wurk/batch/callbacks.rb +143 -92
- data/lib/wurk/batch/death_handler.rb +19 -8
- data/lib/wurk/batch/server_middleware.rb +37 -24
- data/lib/wurk/batch/status.rb +49 -15
- data/lib/wurk/batch.rb +164 -65
- data/lib/wurk/capsule.rb +0 -2
- data/lib/wurk/client/buffered.rb +227 -106
- data/lib/wurk/client.rb +33 -13
- data/lib/wurk/compat.rb +13 -0
- data/lib/wurk/configuration.rb +155 -24
- data/lib/wurk/cpu_count.rb +97 -0
- data/lib/wurk/cron.rb +97 -34
- data/lib/wurk/dead_set.rb +6 -7
- data/lib/wurk/encryption.rb +46 -13
- data/lib/wurk/fetcher/many_queues.rb +65 -0
- data/lib/wurk/fetcher/orphan_grace.rb +67 -0
- data/lib/wurk/fetcher/private_list_key.rb +76 -0
- data/lib/wurk/fetcher/reaper.rb +55 -74
- data/lib/wurk/fetcher/reliable.rb +23 -17
- data/lib/wurk/fetcher/unit_of_work.rb +19 -1
- data/lib/wurk/fetcher/unparseable_keys.rb +56 -0
- data/lib/wurk/flow/completion.rb +28 -3
- data/lib/wurk/flow/creation.rb +24 -4
- data/lib/wurk/flow.rb +24 -2
- data/lib/wurk/health.rb +182 -40
- data/lib/wurk/heartbeat.rb +37 -14
- data/lib/wurk/history.rb +7 -1
- data/lib/wurk/import/sidekiq_cron.rb +248 -0
- data/lib/wurk/job/options.rb +22 -3
- data/lib/wurk/job_logger.rb +13 -3
- data/lib/wurk/job_record.rb +68 -19
- data/lib/wurk/job_retry.rb +12 -6
- data/lib/wurk/job_set.rb +99 -51
- data/lib/wurk/job_util.rb +27 -8
- data/lib/wurk/launcher.rb +84 -40
- data/lib/wurk/leader.rb +53 -28
- data/lib/wurk/limiter/base.rb +11 -30
- data/lib/wurk/limiter/bucket.rb +14 -5
- data/lib/wurk/limiter/server_middleware.rb +2 -1
- data/lib/wurk/limiter.rb +1 -1
- data/lib/wurk/loader.rb +48 -0
- data/lib/wurk/lua/batch_ack_complete.lua +28 -0
- data/lib/wurk/lua/batch_ack_failed.lua +18 -0
- data/lib/wurk/lua/batch_ack_success.lua +35 -0
- data/lib/wurk/lua/batch_append_callback.lua +66 -0
- data/lib/wurk/lua/batch_delete.lua +31 -0
- data/lib/wurk/lua/batch_hold.lua +17 -0
- data/lib/wurk/lua/batch_invalidate.lua +14 -0
- data/lib/wurk/lua/batch_kid_done.lua +14 -0
- data/lib/wurk/lua/batch_push.lua +47 -0
- data/lib/wurk/lua/batch_remove_jobs.lua +29 -0
- data/lib/wurk/lua/batch_schedule.lua +27 -0
- data/lib/wurk/lua/fetch_first.lua +27 -0
- data/lib/wurk/lua/flow_abandon.lua +16 -21
- data/lib/wurk/lua/flow_advance.lua +41 -26
- data/lib/wurk/lua/flow_create.lua +24 -9
- data/lib/wurk/lua/flow_fail.lua +3 -3
- data/lib/wurk/lua/leader_campaign.lua +19 -0
- data/lib/wurk/lua/limiter_bucket_acquire.lua +3 -2
- data/lib/wurk/lua/limiter_list_sweep.lua +8 -9
- data/lib/wurk/lua/loader.rb +36 -23
- data/lib/wurk/lua/throttle_slot.lua +13 -26
- data/lib/wurk/lua.rb +31 -244
- data/lib/wurk/manager.rb +25 -18
- data/lib/wurk/metrics/accumulator.rb +31 -10
- data/lib/wurk/metrics/dashboard_series.rb +163 -0
- data/lib/wurk/metrics/histogram.rb +46 -0
- data/lib/wurk/metrics/history.rb +49 -57
- data/lib/wurk/metrics/prometheus.rb +232 -0
- data/lib/wurk/metrics/query.rb +127 -155
- data/lib/wurk/metrics/rollup.rb +2 -2
- data/lib/wurk/metrics/statsd.rb +1 -1
- data/lib/wurk/middleware/interrupt_handler.rb +6 -2
- data/lib/wurk/middleware/poison_pill.rb +5 -4
- data/lib/wurk/middleware/timeout.rb +8 -8
- data/lib/wurk/process_set.rb +5 -5
- data/lib/wurk/processor.rb +69 -31
- data/lib/wurk/profile_set.rb +19 -24
- data/lib/wurk/profiler.rb +48 -50
- data/lib/wurk/queue.rb +36 -19
- data/lib/wurk/rails_boot.rb +43 -1
- data/lib/wurk/railtie.rb +2 -0
- data/lib/wurk/rake_tasks.rb +18 -0
- data/lib/wurk/redact.rb +39 -0
- data/lib/wurk/redis_connection.rb +6 -3
- data/lib/wurk/redis_options.rb +21 -4
- data/lib/wurk/redis_pool.rb +119 -21
- data/lib/wurk/scheduled.rb +44 -11
- data/lib/wurk/sentry/error_handler.rb +53 -7
- data/lib/wurk/sentry/middleware.rb +7 -8
- data/lib/wurk/shutdown_gate.rb +24 -1
- data/lib/wurk/sorted_entry.rb +16 -5
- data/lib/wurk/stats.rb +9 -4
- data/lib/wurk/status.rb +2 -2
- data/lib/wurk/swarm/child_boot.rb +48 -38
- data/lib/wurk/swarm/liveness.rb +153 -0
- data/lib/wurk/swarm/restart.rb +24 -3
- data/lib/wurk/swarm.rb +115 -24
- data/lib/wurk/telemetry/server_middleware.rb +2 -5
- data/lib/wurk/testing.rb +15 -5
- data/lib/wurk/throttle.rb +16 -1
- data/lib/wurk/topology.rb +1 -5
- data/lib/wurk/unique.rb +1 -1
- data/lib/wurk/version.rb +1 -1
- data/lib/wurk/watchdog.rb +2 -2
- data/lib/wurk/web/config.rb +56 -20
- data/lib/wurk/web/enterprise.rb +21 -18
- data/lib/wurk/web/extension.rb +98 -22
- data/lib/wurk/web/rack_app.rb +12 -11
- data/lib/wurk/web/search.rb +18 -8
- data/lib/wurk/worker/setter.rb +17 -14
- data/lib/wurk/worker.rb +6 -67
- data/lib/wurk.rb +2 -3
- data/vendor/assets/dashboard/assets/ArgsValue-_MYkd_vA.js +1 -0
- data/vendor/assets/dashboard/assets/BatchDetail-DPmg-hle.js +1 -0
- data/vendor/assets/dashboard/assets/Batches-NTgqSlvD.js +1 -0
- data/vendor/assets/dashboard/assets/Busy-2rxcLJMq.js +1 -0
- data/vendor/assets/dashboard/assets/Cron-BFpJgKYb.js +1 -0
- data/vendor/assets/dashboard/assets/Dashboard-C8VOE2S7.js +1 -0
- data/vendor/assets/dashboard/assets/Dead-Dse2YEbZ.js +1 -0
- data/vendor/assets/dashboard/assets/Extension-BIc2c1zG.js +1 -0
- data/vendor/assets/dashboard/assets/FilterBox-BfZm8ZzH.js +1 -0
- data/vendor/assets/dashboard/assets/FlowDetail-BWddOtgf.js +1 -0
- data/vendor/assets/dashboard/assets/FlowState-6lGsojxE.js +1 -0
- data/vendor/assets/dashboard/assets/Flows-BgKTlYwI.js +1 -0
- data/vendor/assets/dashboard/assets/JobDetailModal-BrjmO98c.js +2 -0
- data/vendor/assets/dashboard/assets/Limiters-DeUkfbcg.js +1 -0
- data/vendor/assets/dashboard/assets/Metrics-DzmAgBFG.js +1 -0
- data/vendor/assets/dashboard/assets/NotFound-CQLLXIfa.js +1 -0
- data/vendor/assets/dashboard/assets/PageHeader-CeSaqysY.js +1 -0
- data/vendor/assets/dashboard/assets/Profiles-CI2i2tl8.js +1 -0
- data/vendor/assets/dashboard/assets/Queues-DUCQ_3St.js +1 -0
- data/vendor/assets/dashboard/assets/Retries-Dc2RFuTf.js +1 -0
- data/vendor/assets/dashboard/assets/Scheduled-B8zOKCLC.js +1 -0
- data/vendor/assets/dashboard/assets/Search-x0eS-V-U.js +1 -0
- data/vendor/assets/dashboard/assets/charts-VggSMFGc.js +1 -0
- data/vendor/assets/dashboard/assets/i18n-B6AbnmJn.js +1 -0
- data/vendor/assets/dashboard/assets/index-CTWV8VL4.js +141 -0
- data/vendor/assets/dashboard/assets/index-WpZjU076.css +1 -0
- data/vendor/assets/dashboard/assets/useResetPageOnEmpty-BsYWeStI.js +1 -0
- data/vendor/assets/dashboard/assets/useSort-BWWfpnZH.js +1 -0
- data/vendor/assets/dashboard/assets/utils-DnzJXljj.js +1 -0
- data/vendor/assets/dashboard/index.html +3 -3
- data/vendor/assets/dashboard/wurk-manifest.json +2 -2
- metadata +90 -34
- data/config/locales/en.yml +0 -15
- data/vendor/assets/dashboard/assets/ArgsValue-D-x_ifLY.js +0 -1
- data/vendor/assets/dashboard/assets/BatchDetail-C39NJuew.js +0 -1
- data/vendor/assets/dashboard/assets/Batches-CSwo7Asa.js +0 -1
- data/vendor/assets/dashboard/assets/Busy-BOFMu-sq.js +0 -1
- data/vendor/assets/dashboard/assets/Cron-Dy8RQzDI.js +0 -1
- data/vendor/assets/dashboard/assets/Dashboard-BuTHI-O1.js +0 -1
- data/vendor/assets/dashboard/assets/Dead-B9KRvQ0N.js +0 -1
- data/vendor/assets/dashboard/assets/Extension-BnBVHfux.js +0 -1
- data/vendor/assets/dashboard/assets/FilterBox-DC24zite.js +0 -1
- data/vendor/assets/dashboard/assets/FlowDetail-DyLuzUvt.js +0 -1
- data/vendor/assets/dashboard/assets/FlowState-DAPKUahm.js +0 -1
- data/vendor/assets/dashboard/assets/Flows-Fr3rjZM_.js +0 -1
- data/vendor/assets/dashboard/assets/JobDetailModal-N6kiJXq3.js +0 -2
- data/vendor/assets/dashboard/assets/Limiters-kbFA7uS1.js +0 -1
- data/vendor/assets/dashboard/assets/Metrics-Dj2uoZ3o.js +0 -1
- data/vendor/assets/dashboard/assets/PageHeader-B_F94azl.js +0 -1
- data/vendor/assets/dashboard/assets/Profiles-D_DjEezN.js +0 -1
- data/vendor/assets/dashboard/assets/Queues-CO4V9hAz.js +0 -1
- data/vendor/assets/dashboard/assets/Retries-DCWnzeLa.js +0 -1
- data/vendor/assets/dashboard/assets/Scheduled-BebDUjLU.js +0 -1
- data/vendor/assets/dashboard/assets/Search-Cvr5fy4Y.js +0 -1
- data/vendor/assets/dashboard/assets/Skeleton-Bu3Ke6rV.js +0 -1
- data/vendor/assets/dashboard/assets/charts-BCs9bQKz.js +0 -1
- data/vendor/assets/dashboard/assets/index-BIwyOC5Q.js +0 -141
- data/vendor/assets/dashboard/assets/index-DBQN6Jk8.css +0 -1
- data/vendor/assets/dashboard/assets/useResetPageOnEmpty-Bzh-BJyL.js +0 -1
- data/vendor/assets/dashboard/assets/useSort-COA3fVJ5.js +0 -1
- data/vendor/assets/dashboard/assets/utils-BIrvZ1hi.js +0 -1
data/lib/wurk/batch.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'json'
|
|
|
4
4
|
require 'securerandom'
|
|
5
5
|
require_relative 'lua'
|
|
6
6
|
require_relative 'batch/buffer'
|
|
7
|
+
require_relative 'batch/callbacks'
|
|
7
8
|
|
|
8
9
|
module Wurk
|
|
9
10
|
# Sidekiq Pro Batches. Group jobs, attach success/complete/death callbacks,
|
|
@@ -26,12 +27,14 @@ module Wurk
|
|
|
26
27
|
# or callback only). `mutable?` is false because reopening implies the
|
|
27
28
|
# first flush already happened.
|
|
28
29
|
# 3. `#jobs { ... }` collects `Job.perform_async` calls via the client
|
|
29
|
-
# middleware (Thread.current[:wurk_current_batch] is the signal)
|
|
30
|
-
#
|
|
30
|
+
# middleware (Thread.current[:wurk_current_batch] is the signal) and
|
|
31
|
+
# pushes them at block exit, each registered via BATCH_PUSH. A hold
|
|
32
|
+
# keeps the batch from draining while the block is open.
|
|
31
33
|
# 4. Workers ack on success → BATCH_ACK_SUCCESS → pending--.
|
|
32
34
|
# Death handler acks on permanent failure → BATCH_ACK_COMPLETE.
|
|
33
|
-
# 5. When live jids
|
|
34
|
-
# hits zero with zero deaths → fire
|
|
35
|
+
# 5. When live jids and pending child batches both hit zero → fire
|
|
36
|
+
# `:complete`. When pending also hits zero with zero deaths → fire
|
|
37
|
+
# `:success`.
|
|
35
38
|
#
|
|
36
39
|
# Nested batches: a job opening its OWN batch (`batch.jobs { ... }`)
|
|
37
40
|
# increments live counters on the existing batch. A callback opening its
|
|
@@ -82,11 +85,22 @@ module Wurk
|
|
|
82
85
|
|
|
83
86
|
THREAD_KEY = :wurk_current_batch
|
|
84
87
|
|
|
88
|
+
EMPTY_JOB = 'Sidekiq::Batch::Empty'
|
|
89
|
+
|
|
85
90
|
# Set on the current thread (to a Buffer) only inside an autoflush
|
|
86
91
|
# `#jobs` block. Client#raw_push reads it: when present, batched pushes
|
|
87
92
|
# accumulate here instead of round-tripping per job.
|
|
88
93
|
BUFFER_KEY = :wurk_batch_buffer
|
|
89
94
|
|
|
95
|
+
# Bids whose `#jobs` hold this thread already owns, so a block nested in
|
|
96
|
+
# another block of the same batch neither takes a second hold nor drops
|
|
97
|
+
# the outer one when it exits.
|
|
98
|
+
HOLDS_KEY = :wurk_batch_holds
|
|
99
|
+
|
|
100
|
+
# Prefix of the hold sentinel's member in `b-<bid>-jids`. Real jids are
|
|
101
|
+
# hex, so a sentinel can never collide with one.
|
|
102
|
+
HOLD_PREFIX = 'hold:'
|
|
103
|
+
|
|
90
104
|
attr_reader :bid, :parent_bid, :linger, :callback_class
|
|
91
105
|
attr_accessor :description, :callback_queue, :autoflush
|
|
92
106
|
|
|
@@ -95,6 +109,29 @@ module Wurk
|
|
|
95
109
|
[base, *KEY_SUFFIXES.map { |s| "#{base}-#{s}" }]
|
|
96
110
|
end
|
|
97
111
|
|
|
112
|
+
# Runs the block with `batch` as the thread's active batch (the client
|
|
113
|
+
# middleware stamps its bid) and `buffer` collecting batched pushes —
|
|
114
|
+
# either may be nil — restoring whatever an enclosing block had set.
|
|
115
|
+
def self.with_thread_batch(batch, buffer)
|
|
116
|
+
previous = Thread.current[THREAD_KEY]
|
|
117
|
+
prev_buffer = Thread.current[BUFFER_KEY]
|
|
118
|
+
Thread.current[THREAD_KEY] = batch
|
|
119
|
+
Thread.current[BUFFER_KEY] = buffer
|
|
120
|
+
yield
|
|
121
|
+
ensure
|
|
122
|
+
Thread.current[THREAD_KEY] = previous
|
|
123
|
+
Thread.current[BUFFER_KEY] = prev_buffer
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# BATCH_ACK_SUCCESS for `jid`, as Integers: [removed, pending, live, kids].
|
|
127
|
+
# The last three are the fire-gate inputs `Callbacks.maybe_fire` takes.
|
|
128
|
+
def self.ack_success(conn, bid, jid)
|
|
129
|
+
Wurk::Lua::Loader.eval_cached(
|
|
130
|
+
conn, :batch_ack_success,
|
|
131
|
+
keys: ["b-#{bid}", "b-#{bid}-jids", "b-#{bid}-failed", "b-#{bid}-pkids"], argv: [jid]
|
|
132
|
+
).map(&:to_i)
|
|
133
|
+
end
|
|
134
|
+
|
|
98
135
|
# Two-axis trim of a batch index ZSET (`batches`, `dead-batches`), in the
|
|
99
136
|
# shape of the morgue trim (`DeadSet#trim`): `ZREMRANGEBYSCORE` evicts
|
|
100
137
|
# entries older than `timeout`, `ZREMRANGEBYRANK 0 -max` caps the member
|
|
@@ -199,22 +236,25 @@ module Wurk
|
|
|
199
236
|
end
|
|
200
237
|
|
|
201
238
|
# Remove jobs from the batch. Decrements pending/total by exactly the
|
|
202
|
-
# count of jids actually removed (idempotent for repeated calls)
|
|
239
|
+
# count of jids actually removed (idempotent for repeated calls), in one
|
|
240
|
+
# atomic script. Removing the last live jid drains the batch like an ack
|
|
241
|
+
# would, so the callbacks fire here rather than never.
|
|
203
242
|
def remove_jobs(*jids)
|
|
204
243
|
return 0 if jids.empty?
|
|
205
244
|
|
|
206
|
-
Wurk.redis do |conn|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
245
|
+
removed, pending, live, kids = Wurk.redis do |conn|
|
|
246
|
+
Wurk::Lua::Loader.eval_cached(
|
|
247
|
+
conn, :batch_remove_jobs,
|
|
248
|
+
keys: ["b-#{@bid}", "b-#{@bid}-jids", "b-#{@bid}-failed", "b-#{@bid}-pkids"], argv: jids
|
|
249
|
+
)
|
|
250
|
+
end.map(&:to_i)
|
|
251
|
+
Callbacks.maybe_fire(@bid, pending: pending, live: live, kids: kids) if removed.positive?
|
|
252
|
+
removed
|
|
214
253
|
end
|
|
215
254
|
|
|
216
255
|
# Mark batch invalid. Pending jobs still exist in their queues; the
|
|
217
|
-
# server middleware short-circuits them when it observes the flag
|
|
256
|
+
# server middleware short-circuits them when it observes the flag and acks
|
|
257
|
+
# them as successes (spec §12), so the batch still drains and fires.
|
|
218
258
|
# Cascades to descendant batches via b-<bid>-kids.
|
|
219
259
|
def invalidate_all
|
|
220
260
|
cascade_invalidate(@bid)
|
|
@@ -253,23 +293,35 @@ module Wurk
|
|
|
253
293
|
self
|
|
254
294
|
end
|
|
255
295
|
|
|
256
|
-
# Atomic enqueue block. Inside the block, `Job.perform_async`
|
|
257
|
-
# this batch via Thread.current[THREAD_KEY] and stamps `bid` onto the
|
|
258
|
-
# payload
|
|
259
|
-
#
|
|
260
|
-
#
|
|
296
|
+
# Atomic enqueue block (spec §2.3). Inside the block, `Job.perform_async`
|
|
297
|
+
# finds this batch via Thread.current[THREAD_KEY] and stamps `bid` onto the
|
|
298
|
+
# payload; the pushes are collected and flushed at block exit, each
|
|
299
|
+
# through BATCH_PUSH. A block that raises pushes nothing it collected.
|
|
300
|
+
# `autoflush = N` flushes every N jobs instead, trading that atomicity for
|
|
301
|
+
# bounded memory. A block that creates the batch and adds nothing to it —
|
|
302
|
+
# no job, no child batch — synthesises a Batch::Empty no-op (spec §2.3);
|
|
303
|
+
# a block re-entering an existing batch needs none, the batch already has
|
|
304
|
+
# members (or drains when the hold is released).
|
|
305
|
+
#
|
|
306
|
+
# For the whole block the batch carries a hold (BATCH_HOLD), so a job
|
|
307
|
+
# pushed early — a flushed autoflush slice, a scheduled job, a nested
|
|
308
|
+
# child batch — cannot ack the batch empty and fire its callbacks before
|
|
309
|
+
# the rest is in. The hold is released at block exit through the same ack
|
|
310
|
+
# path as a job. When the block that *created* the batch raises, the hold
|
|
311
|
+
# stays: nothing the block collected was pushed, and a batch that fired
|
|
312
|
+
# for whatever slipped out early would be the partial batch §2.3 rules
|
|
313
|
+
# out. A block re-entering an existing batch releases it either way, or
|
|
314
|
+
# the batch's own jobs could never fire it.
|
|
261
315
|
def jobs(&block)
|
|
262
316
|
raise ArgumentError, 'jobs requires a block' unless block
|
|
263
317
|
|
|
318
|
+
threshold = autoflush_threshold
|
|
319
|
+
created = !@flushed_once
|
|
264
320
|
ensure_first_flush!
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
# Scheduled (`perform_in`) jobs count here too: BATCH_SCHEDULE moves
|
|
270
|
-
# `total` at creation, so a scheduled-only block does not misfire the
|
|
271
|
-
# marker as if it were empty.
|
|
272
|
-
enqueue_empty_marker if job_count == pre_count
|
|
321
|
+
with_hold(release_on_error: !created) do
|
|
322
|
+
collect_jobs(threshold, &block)
|
|
323
|
+
enqueue_empty_marker if created && untouched?
|
|
324
|
+
end
|
|
273
325
|
@mutable = false
|
|
274
326
|
self
|
|
275
327
|
end
|
|
@@ -277,41 +329,70 @@ module Wurk
|
|
|
277
329
|
private
|
|
278
330
|
|
|
279
331
|
# Runs the block with this batch active so the client middleware stamps
|
|
280
|
-
# the bid.
|
|
281
|
-
#
|
|
282
|
-
def collect_jobs
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
332
|
+
# the bid. Batched pushes accumulate in a Buffer and flush once at exit
|
|
333
|
+
# (only on a normal exit); the per-N flushing happens in Client#raw_push.
|
|
334
|
+
def collect_jobs(threshold)
|
|
335
|
+
buffer = Buffer.new([], threshold)
|
|
336
|
+
Batch.with_thread_batch(self, buffer) do
|
|
337
|
+
yield
|
|
338
|
+
flush_buffer(buffer)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Unset (or `true`/`false`) → buffer the whole block (nil threshold,
|
|
343
|
+
# drained at exit); positive Integer → flush every N. Any other value is
|
|
344
|
+
# a config typo (`0`, `-1`, `"5"`) — fail fast instead of silently
|
|
345
|
+
# degrading to "flush at block exit". Checked before anything touches
|
|
346
|
+
# Redis, so a typo never leaves a batch created and held.
|
|
347
|
+
def autoflush_threshold
|
|
348
|
+
return nil if @autoflush.nil? || @autoflush == true || @autoflush == false
|
|
349
|
+
return @autoflush if @autoflush.is_a?(Integer) && @autoflush.positive?
|
|
350
|
+
|
|
351
|
+
raise ArgumentError, "autoflush must be true or a positive Integer, got #{@autoflush.inspect}"
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def with_hold(release_on_error:)
|
|
355
|
+
holds = (Thread.current[HOLDS_KEY] ||= {})
|
|
356
|
+
outermost = !holds.key?(@bid)
|
|
357
|
+
sentinel = take_hold if outermost
|
|
358
|
+
holds[@bid] = true if outermost
|
|
359
|
+
completed = false
|
|
288
360
|
begin
|
|
289
361
|
yield
|
|
290
|
-
|
|
362
|
+
completed = true
|
|
291
363
|
ensure
|
|
292
|
-
|
|
293
|
-
|
|
364
|
+
holds.delete(@bid) if outermost
|
|
365
|
+
release_hold(sentinel) if sentinel && (completed || release_on_error)
|
|
294
366
|
end
|
|
295
367
|
end
|
|
296
368
|
|
|
297
|
-
#
|
|
298
|
-
#
|
|
299
|
-
#
|
|
300
|
-
def
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
369
|
+
# Named after the running job when there is one, so a job reclaimed after
|
|
370
|
+
# a SIGKILL mid-block re-takes the *same* hold (BATCH_HOLD's SADD guard
|
|
371
|
+
# makes that a no-op) and its release clears the one the dead run left.
|
|
372
|
+
def take_hold
|
|
373
|
+
sentinel = "#{HOLD_PREFIX}#{Wurk::Context.current[:jid] || SecureRandom.hex(12)}"
|
|
374
|
+
Wurk.redis do |conn|
|
|
375
|
+
Wurk::Lua::Loader.eval_cached(conn, :batch_hold,
|
|
376
|
+
keys: ["b-#{@bid}", "b-#{@bid}-jids"], argv: [sentinel, @expires_in])
|
|
377
|
+
end
|
|
378
|
+
sentinel
|
|
304
379
|
end
|
|
305
380
|
|
|
306
|
-
#
|
|
307
|
-
#
|
|
308
|
-
#
|
|
309
|
-
#
|
|
310
|
-
def
|
|
311
|
-
|
|
312
|
-
|
|
381
|
+
# The ack is retried and raises if Redis stays down — a hold left behind
|
|
382
|
+
# means the batch never fires, which the caller has to hear about. The
|
|
383
|
+
# fire after it is reported, not raised: every job is already pushed, and
|
|
384
|
+
# a caller retrying the block would push them all twice.
|
|
385
|
+
def release_hold(sentinel)
|
|
386
|
+
_removed, pending, live, kids = Callbacks.retrying do
|
|
387
|
+
Wurk.redis { |conn| Batch.ack_success(conn, @bid, sentinel) }
|
|
388
|
+
end
|
|
389
|
+
fire_after_release(pending, live, kids)
|
|
390
|
+
end
|
|
313
391
|
|
|
314
|
-
|
|
392
|
+
def fire_after_release(pending, live, kids)
|
|
393
|
+
Callbacks.retrying { Callbacks.maybe_fire(@bid, pending: pending, live: live, kids: kids) }
|
|
394
|
+
rescue StandardError => e
|
|
395
|
+
Wurk.configuration.handle_exception(e, { context: "batch #{@bid}: firing callbacks at #jobs exit", bid: @bid })
|
|
315
396
|
end
|
|
316
397
|
|
|
317
398
|
def flush_buffer(buffer)
|
|
@@ -384,16 +465,25 @@ module Wurk
|
|
|
384
465
|
# Only `b-#{@bid}` is stamped here — none of the sub-keys exist yet at first
|
|
385
466
|
# flush (BATCH_PUSH/BATCH_SCHEDULE create `-jids`, the acks create
|
|
386
467
|
# `-failed`/`-died`), and EXPIRE on a missing key is a no-op. Each key is
|
|
387
|
-
# stamped `NX` where it is created instead; see
|
|
468
|
+
# stamped `NX` where it is created instead; see lua/batch_push.lua.
|
|
388
469
|
def pipelined_first_flush(pipe, now)
|
|
389
470
|
pipe.call('HSET', "b-#{@bid}", *first_flush_hash(now).flatten)
|
|
390
471
|
pipe.call('EXPIRE', "b-#{@bid}", @expires_in)
|
|
391
472
|
pipe.call('ZADD', 'batches', now.to_s, @bid)
|
|
392
473
|
Batch.trim_index(pipe, 'batches')
|
|
393
|
-
@tags.each { |t| pipe
|
|
474
|
+
@tags.each { |t| index_tag(pipe, "tags:#{t}") }
|
|
394
475
|
link_to_parent(pipe) if current_parent_bid
|
|
395
476
|
end
|
|
396
477
|
|
|
478
|
+
# The reverse index is shared by every batch carrying the tag, so it must
|
|
479
|
+
# outlive the longest-lived of them: NX gives a fresh (or legacy TTL-less)
|
|
480
|
+
# set a clock, GT only ever extends it. `Status#delete` SREMs the bid.
|
|
481
|
+
def index_tag(pipe, key)
|
|
482
|
+
pipe.call('SADD', key, @bid)
|
|
483
|
+
pipe.call('EXPIRE', key, @expires_in, 'NX')
|
|
484
|
+
pipe.call('EXPIRE', key, @expires_in, 'GT')
|
|
485
|
+
end
|
|
486
|
+
|
|
397
487
|
def first_flush_hash(now)
|
|
398
488
|
{
|
|
399
489
|
'created_at' => now.to_s,
|
|
@@ -432,24 +522,33 @@ module Wurk
|
|
|
432
522
|
pipe.call('EXPIRE', "#{parent_key}-pkids", DEFAULT_EXPIRY_SECONDS, 'NX')
|
|
433
523
|
end
|
|
434
524
|
|
|
435
|
-
|
|
436
|
-
|
|
525
|
+
# Read after the buffer flushed, so `total` reflects everything the block
|
|
526
|
+
# pushed. Scheduled (`perform_in`) jobs count too: BATCH_SCHEDULE moves
|
|
527
|
+
# `total` at creation, so a scheduled-only block is not mistaken for an
|
|
528
|
+
# empty one.
|
|
529
|
+
def untouched?
|
|
530
|
+
total, kids = Wurk.redis do |conn|
|
|
531
|
+
conn.pipelined do |pipe|
|
|
532
|
+
pipe.call('HGET', "b-#{@bid}", 'total')
|
|
533
|
+
pipe.call('SCARD', "b-#{@bid}-kids")
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
total.to_i.zero? && kids.to_i.zero?
|
|
437
537
|
end
|
|
438
538
|
|
|
539
|
+
# Pushed straight through, never into an enclosing batch's buffer: it has
|
|
540
|
+
# to be live before this block's hold is released. The payload names the
|
|
541
|
+
# class `Sidekiq::Batch::Empty`, the spec's wire name (§2.3) — the alias
|
|
542
|
+
# resolves it here, and Sidekiq Pro can still run it after a swap back.
|
|
439
543
|
def enqueue_empty_marker
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
Thread.current[THREAD_KEY] = self
|
|
443
|
-
begin
|
|
444
|
-
Wurk::Batch::Empty.perform_async
|
|
445
|
-
ensure
|
|
446
|
-
Thread.current[THREAD_KEY] = previous
|
|
544
|
+
Batch.with_thread_batch(self, nil) do
|
|
545
|
+
Wurk::Client.push('class' => EMPTY_JOB, 'args' => [], 'queue' => 'default', 'retry' => false)
|
|
447
546
|
end
|
|
448
547
|
end
|
|
449
548
|
|
|
450
549
|
def cascade_invalidate(bid)
|
|
451
550
|
Wurk.redis do |conn|
|
|
452
|
-
Wurk::Lua::Loader.eval_cached(conn, :batch_invalidate, keys: ["b-#{bid}"
|
|
551
|
+
Wurk::Lua::Loader.eval_cached(conn, :batch_invalidate, keys: ["b-#{bid}"], argv: [])
|
|
453
552
|
kids = conn.call('SMEMBERS', "b-#{bid}-kids") || []
|
|
454
553
|
kids.each { |child| cascade_invalidate(child) }
|
|
455
554
|
end
|
data/lib/wurk/capsule.rb
CHANGED