wurk 0.0.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 +7 -0
- data/CHANGELOG.md +43 -0
- data/CONTRIBUTING.md +73 -0
- data/LICENSE +21 -0
- data/README.md +137 -0
- data/SECURITY.md +39 -0
- data/app/controllers/wurk/api/pagination.rb +67 -0
- data/app/controllers/wurk/api/serializers.rb +131 -0
- data/app/controllers/wurk/api_controller.rb +248 -0
- data/app/controllers/wurk/application_controller.rb +7 -0
- data/app/controllers/wurk/dashboard_controller.rb +48 -0
- data/config/locales/en.yml +15 -0
- data/config/routes.rb +34 -0
- data/exe/wurk +22 -0
- data/lib/active_job/queue_adapters/wurk_adapter.rb +96 -0
- data/lib/generators/wurk/install/install_generator.rb +22 -0
- data/lib/generators/wurk/install/templates/wurk.rb +16 -0
- data/lib/wurk/active_job/wrapper.rb +32 -0
- data/lib/wurk/api/fast.rb +78 -0
- data/lib/wurk/batch/buffer.rb +26 -0
- data/lib/wurk/batch/callback_job.rb +37 -0
- data/lib/wurk/batch/callbacks.rb +176 -0
- data/lib/wurk/batch/client_middleware.rb +27 -0
- data/lib/wurk/batch/death_handler.rb +39 -0
- data/lib/wurk/batch/empty.rb +21 -0
- data/lib/wurk/batch/server_middleware.rb +62 -0
- data/lib/wurk/batch/status.rb +140 -0
- data/lib/wurk/batch.rb +351 -0
- data/lib/wurk/batch_set.rb +67 -0
- data/lib/wurk/capsule.rb +176 -0
- data/lib/wurk/cli.rb +349 -0
- data/lib/wurk/client/buffered.rb +372 -0
- data/lib/wurk/client.rb +330 -0
- data/lib/wurk/compat.rb +136 -0
- data/lib/wurk/component.rb +136 -0
- data/lib/wurk/configuration.rb +373 -0
- data/lib/wurk/context.rb +35 -0
- data/lib/wurk/cron.rb +636 -0
- data/lib/wurk/dashboard_manifest.rb +39 -0
- data/lib/wurk/dead_set.rb +78 -0
- data/lib/wurk/deploy.rb +91 -0
- data/lib/wurk/embedded.rb +94 -0
- data/lib/wurk/encryption.rb +276 -0
- data/lib/wurk/engine.rb +81 -0
- data/lib/wurk/fetcher/reaper.rb +264 -0
- data/lib/wurk/fetcher/reliable.rb +138 -0
- data/lib/wurk/fetcher.rb +11 -0
- data/lib/wurk/health.rb +193 -0
- data/lib/wurk/heartbeat.rb +211 -0
- data/lib/wurk/iterable_job.rb +292 -0
- data/lib/wurk/job/options.rb +70 -0
- data/lib/wurk/job.rb +33 -0
- data/lib/wurk/job_logger.rb +68 -0
- data/lib/wurk/job_record.rb +156 -0
- data/lib/wurk/job_retry.rb +320 -0
- data/lib/wurk/job_set.rb +212 -0
- data/lib/wurk/job_util.rb +162 -0
- data/lib/wurk/keys.rb +52 -0
- data/lib/wurk/launcher.rb +289 -0
- data/lib/wurk/leader.rb +221 -0
- data/lib/wurk/limiter/base.rb +138 -0
- data/lib/wurk/limiter/bucket.rb +80 -0
- data/lib/wurk/limiter/concurrent.rb +132 -0
- data/lib/wurk/limiter/leaky.rb +91 -0
- data/lib/wurk/limiter/points.rb +89 -0
- data/lib/wurk/limiter/server_middleware.rb +77 -0
- data/lib/wurk/limiter/unlimited.rb +48 -0
- data/lib/wurk/limiter/window.rb +80 -0
- data/lib/wurk/limiter.rb +255 -0
- data/lib/wurk/logger.rb +81 -0
- data/lib/wurk/lua/loader.rb +53 -0
- data/lib/wurk/lua.rb +187 -0
- data/lib/wurk/manager.rb +132 -0
- data/lib/wurk/metrics/history.rb +151 -0
- data/lib/wurk/metrics/query.rb +173 -0
- data/lib/wurk/metrics/rollup.rb +169 -0
- data/lib/wurk/metrics/statsd.rb +197 -0
- data/lib/wurk/metrics.rb +7 -0
- data/lib/wurk/middleware/chain.rb +128 -0
- data/lib/wurk/middleware/current_attributes.rb +87 -0
- data/lib/wurk/middleware/expiry.rb +50 -0
- data/lib/wurk/middleware/i18n.rb +63 -0
- data/lib/wurk/middleware/interrupt_handler.rb +45 -0
- data/lib/wurk/middleware/poison_pill.rb +149 -0
- data/lib/wurk/middleware.rb +34 -0
- data/lib/wurk/process_set.rb +243 -0
- data/lib/wurk/processor.rb +247 -0
- data/lib/wurk/queue.rb +108 -0
- data/lib/wurk/queues.rb +80 -0
- data/lib/wurk/rails.rb +9 -0
- data/lib/wurk/railtie.rb +28 -0
- data/lib/wurk/redis_pool.rb +79 -0
- data/lib/wurk/retry_set.rb +17 -0
- data/lib/wurk/scheduled.rb +189 -0
- data/lib/wurk/scheduled_set.rb +18 -0
- data/lib/wurk/sorted_entry.rb +95 -0
- data/lib/wurk/stats.rb +190 -0
- data/lib/wurk/swarm/child_boot.rb +105 -0
- data/lib/wurk/swarm.rb +260 -0
- data/lib/wurk/testing.rb +102 -0
- data/lib/wurk/topology.rb +74 -0
- data/lib/wurk/unique.rb +240 -0
- data/lib/wurk/version.rb +5 -0
- data/lib/wurk/web/config.rb +180 -0
- data/lib/wurk/web/enterprise.rb +138 -0
- data/lib/wurk/web/search.rb +139 -0
- data/lib/wurk/web.rb +25 -0
- data/lib/wurk/work_set.rb +116 -0
- data/lib/wurk/worker/setter.rb +93 -0
- data/lib/wurk/worker.rb +216 -0
- data/lib/wurk.rb +238 -0
- data/vendor/assets/dashboard/assets/index-8P3N_m1X.js +152 -0
- data/vendor/assets/dashboard/assets/index-Bqz4_SOQ.css +1 -0
- data/vendor/assets/dashboard/index.html +13 -0
- data/vendor/assets/dashboard/wurk-manifest.json +4 -0
- metadata +232 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../worker'
|
|
4
|
+
|
|
5
|
+
module Wurk
|
|
6
|
+
class Batch
|
|
7
|
+
# Worker that runs a single batch callback. Target may be a Class name
|
|
8
|
+
# ("MyCallback" → `MyCallback.new.on_<event>(status, options)`) or a
|
|
9
|
+
# "Klass#method" spec ("Foo#bar" → `Foo.new.bar(status, options)`).
|
|
10
|
+
# Failures retry like any ordinary job — callbacks MUST be idempotent.
|
|
11
|
+
#
|
|
12
|
+
# Spec: docs/target/sidekiq-pro.md §2.4.
|
|
13
|
+
class CallbackJob
|
|
14
|
+
include Wurk::Job
|
|
15
|
+
|
|
16
|
+
sidekiq_options retry: true
|
|
17
|
+
|
|
18
|
+
def perform(bid, target_spec, event, options)
|
|
19
|
+
klass, method = resolve(target_spec, event)
|
|
20
|
+
instance = klass.new
|
|
21
|
+
status = Wurk::Batch::Status.new(bid)
|
|
22
|
+
instance.public_send(method, status, options || {})
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def resolve(spec, event)
|
|
28
|
+
if spec.include?('#')
|
|
29
|
+
klass_name, method_name = spec.split('#', 2)
|
|
30
|
+
[Object.const_get(klass_name), method_name.to_sym]
|
|
31
|
+
else
|
|
32
|
+
[Object.const_get(spec), :"on_#{event}"]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Wurk
|
|
6
|
+
class Batch
|
|
7
|
+
# Fires batch callbacks (`:success`, `:complete`, `:death`) by enqueuing
|
|
8
|
+
# them as ordinary jobs on the batch's `callback_queue`. Dedup is via
|
|
9
|
+
# b-<bid>-notify so the same callback can't be enqueued twice even
|
|
10
|
+
# if multiple workers race to ack the final job.
|
|
11
|
+
#
|
|
12
|
+
# Callback wrapper job: Wurk::Batch::CallbackJob — given a target spec
|
|
13
|
+
# ("Klass" or "Klass#method") and options hash, it instantiates and
|
|
14
|
+
# invokes on_<event> (or the named method) with the Status snapshot.
|
|
15
|
+
module Callbacks
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Called from the server middleware after BATCH_ACK_SUCCESS. Fires
|
|
19
|
+
# `:complete` when live jids hit 0; fires `:success` when pending
|
|
20
|
+
# also hits 0 and there have been no deaths.
|
|
21
|
+
def maybe_fire(bid, pending:, live:)
|
|
22
|
+
return unless live.zero?
|
|
23
|
+
|
|
24
|
+
fire_complete(bid)
|
|
25
|
+
fire_success(bid) if pending.zero? && !death_fired?(bid)
|
|
26
|
+
propagate_to_parent(bid)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Fired from Wurk::Batch::DeathHandler on the FIRST permanent death
|
|
30
|
+
# in the batch only. Subsequent deaths bump the counter but do not
|
|
31
|
+
# re-enqueue the callback.
|
|
32
|
+
def fire_death(bid)
|
|
33
|
+
return unless dedup_set(bid, 'death')
|
|
34
|
+
|
|
35
|
+
record_event(bid, 'death_at')
|
|
36
|
+
Wurk.redis { |conn| conn.call('ZADD', 'dead-batches', Time.now.to_f.to_s, bid) }
|
|
37
|
+
enqueue_callbacks(bid, 'death')
|
|
38
|
+
cascade_death(bid)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A child's death means the parent — and every ancestor — can never
|
|
42
|
+
# fully succeed, so `:death` propagates up the parent chain. The
|
|
43
|
+
# recursion bottoms out at the root (empty parent_bid); fire_death's own
|
|
44
|
+
# dedup_set guard makes each ancestor's `:death` fire exactly once even
|
|
45
|
+
# under racing children.
|
|
46
|
+
def cascade_death(bid)
|
|
47
|
+
parent_bid = parent_bid_for(bid)
|
|
48
|
+
return if parent_bid.nil? || parent_bid.empty?
|
|
49
|
+
|
|
50
|
+
fire_death(parent_bid)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def fire_complete(bid)
|
|
54
|
+
return unless dedup_set(bid, 'complete')
|
|
55
|
+
|
|
56
|
+
record_event(bid, 'complete_at')
|
|
57
|
+
enqueue_callbacks(bid, 'complete')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def fire_success(bid)
|
|
61
|
+
return unless dedup_set(bid, 'success')
|
|
62
|
+
|
|
63
|
+
record_event(bid, 'success_at')
|
|
64
|
+
enqueue_callbacks(bid, 'success')
|
|
65
|
+
apply_linger(bid)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Post-success retention: a succeeded batch no longer coordinates any
|
|
69
|
+
# jobs, so its keys expire after the per-batch `linger` override (else
|
|
70
|
+
# 24h) instead of the 30d pending TTL. Mirrors Sidekiq Pro §2.8.
|
|
71
|
+
def apply_linger(bid)
|
|
72
|
+
raw = Wurk.redis { |conn| conn.call('HGET', "b-#{bid}", 'linger') }
|
|
73
|
+
seconds = raw.nil? || raw.to_s.empty? ? Batch::POST_SUCCESS_EXPIRY_SECONDS : raw.to_i
|
|
74
|
+
Wurk.redis do |conn|
|
|
75
|
+
Batch.keys_for(bid).each { |key| conn.call('EXPIRE', key, seconds) }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Atomically marks `bid` as having fired `event`. Returns true the
|
|
80
|
+
# first time, false thereafter — caller skips the enqueue when false.
|
|
81
|
+
# SET NX makes this safe under racing acks.
|
|
82
|
+
def dedup_set(bid, event)
|
|
83
|
+
Wurk.redis do |conn|
|
|
84
|
+
ok = conn.call('SET', "b-#{bid}-#{event}", '1', 'NX', 'EX', Batch::CALLBACK_NOTIFY_TTL)
|
|
85
|
+
ok == 'OK'
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def record_event(bid, field)
|
|
90
|
+
now = ::Process.clock_gettime(::Process::CLOCK_REALTIME)
|
|
91
|
+
Wurk.redis do |conn|
|
|
92
|
+
conn.call('HSET', "b-#{bid}", field, now.to_s)
|
|
93
|
+
conn.call('HSET', "b-#{bid}", field.to_s.sub('_at', ''), '1')
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# True once `:death` has fired for this batch — from one of its own
|
|
98
|
+
# jobs dying or from a descendant's death cascading up. Suppresses
|
|
99
|
+
# `:success`, which must never fire after any death in the subtree.
|
|
100
|
+
#
|
|
101
|
+
# Reads the durable `death` field on `b-<bid>` (written by `record_event`),
|
|
102
|
+
# not the `b-<bid>-death` dedup key — the dedup key has its own 30d TTL
|
|
103
|
+
# and can expire while an ancestor batch is still open, after which a
|
|
104
|
+
# late `maybe_fire` would wrongly emit `:success`.
|
|
105
|
+
def death_fired?(bid)
|
|
106
|
+
Wurk.redis { |conn| conn.call('HGET', "b-#{bid}", 'death') } == '1'
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Per-callback rescue: one bad spec or a transient enqueue failure must
|
|
110
|
+
# not strand the batch with the remaining callbacks for this event
|
|
111
|
+
# un-enqueued. Log and move on so every other callback still fires.
|
|
112
|
+
def enqueue_callbacks(bid, event)
|
|
113
|
+
callbacks, queue = callback_specs_for(bid)
|
|
114
|
+
|
|
115
|
+
callbacks.each do |(cb_event, target, options)|
|
|
116
|
+
next unless cb_event == event
|
|
117
|
+
|
|
118
|
+
enqueue_callback_job(bid, target, event, options, queue)
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
Wurk.logger.warn("batch #{bid}: #{event} callback #{target.inspect} enqueue failed: #{e.class}: #{e.message}")
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def callback_specs_for(bid)
|
|
125
|
+
raw = Wurk.redis { |conn| conn.call('HMGET', "b-#{bid}", 'callbacks', 'callback_queue') }
|
|
126
|
+
callbacks_json, queue = raw
|
|
127
|
+
queue = 'default' if queue.nil? || queue.empty?
|
|
128
|
+
parsed = parse_callbacks(callbacks_json)
|
|
129
|
+
[parsed, queue]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def parse_callbacks(raw)
|
|
133
|
+
return [] if raw.nil? || raw.empty?
|
|
134
|
+
|
|
135
|
+
JSON.parse(raw)
|
|
136
|
+
rescue JSON::ParserError
|
|
137
|
+
[]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def enqueue_callback_job(bid, target, event, options, queue)
|
|
141
|
+
Wurk::Client.push(
|
|
142
|
+
'class' => 'Wurk::Batch::CallbackJob',
|
|
143
|
+
'args' => [bid, target, event, options],
|
|
144
|
+
'queue' => queue,
|
|
145
|
+
'retry' => true
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# When a child batch's `:success` fires, decrement the parent's pkids
|
|
150
|
+
# set so the parent's own `:success` waits on the full subtree. When
|
|
151
|
+
# parent's pkids hits 0 *and* its own pending is 0, parent's success
|
|
152
|
+
# fires too.
|
|
153
|
+
def propagate_to_parent(bid)
|
|
154
|
+
parent_bid = parent_bid_for(bid)
|
|
155
|
+
return if parent_bid.nil? || parent_bid.empty?
|
|
156
|
+
return unless pkids_drained?(parent_bid, bid)
|
|
157
|
+
|
|
158
|
+
maybe_fire(parent_bid, pending: pending_for(parent_bid), live: live_for(parent_bid))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def parent_bid_for(bid)
|
|
162
|
+
Wurk.redis { |conn| conn.call('HGET', "b-#{bid}", 'parent_bid') }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def pkids_drained?(parent_bid, child_bid)
|
|
166
|
+
Wurk.redis do |conn|
|
|
167
|
+
conn.call('SREM', "b-#{parent_bid}-pkids", child_bid)
|
|
168
|
+
conn.call('SCARD', "b-#{parent_bid}-pkids").to_i.zero?
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def pending_for(bid) = Wurk.redis { |conn| conn.call('HGET', "b-#{bid}", 'pending') }.to_i
|
|
173
|
+
def live_for(bid) = Wurk.redis { |conn| conn.call('SCARD', "b-#{bid}-jids") }.to_i
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../middleware'
|
|
4
|
+
|
|
5
|
+
module Wurk
|
|
6
|
+
class Batch
|
|
7
|
+
# Client middleware. When a `Job.perform_async` runs inside a
|
|
8
|
+
# `batch.jobs { ... }` block, Thread.current[Batch::THREAD_KEY] holds
|
|
9
|
+
# the active batch — we stamp `bid` onto the payload so the worker
|
|
10
|
+
# can re-open the batch and Client#raw_push can route through
|
|
11
|
+
# BATCH_PUSH for atomic increment+LPUSH.
|
|
12
|
+
#
|
|
13
|
+
# Auto-registered at the head of the client chain when this file is
|
|
14
|
+
# required.
|
|
15
|
+
class ClientMiddleware
|
|
16
|
+
include Wurk::Middleware::ClientMiddleware
|
|
17
|
+
|
|
18
|
+
def call(_worker, job, _queue, _redis_pool)
|
|
19
|
+
batch = Thread.current[Batch::THREAD_KEY]
|
|
20
|
+
job['bid'] = batch.bid if batch
|
|
21
|
+
yield
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Wurk.configuration.client_middleware.prepend(Wurk::Batch::ClientMiddleware)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../lua'
|
|
4
|
+
require_relative 'callbacks'
|
|
5
|
+
|
|
6
|
+
module Wurk
|
|
7
|
+
class Batch
|
|
8
|
+
# Registered as a config death_handler. Fires for every job that
|
|
9
|
+
# exhausts retries or carries `dead: false` and discards. If the job
|
|
10
|
+
# carries a `bid`, we BATCH_ACK_COMPLETE → record the death → fire
|
|
11
|
+
# `:death` callback exactly once per batch (first death only).
|
|
12
|
+
#
|
|
13
|
+
# Spec: docs/target/sidekiq-pro.md §2.4 (`:death`).
|
|
14
|
+
class DeathHandler
|
|
15
|
+
def self.call(job, _exception)
|
|
16
|
+
bid = job['bid']
|
|
17
|
+
return unless bid
|
|
18
|
+
|
|
19
|
+
result = Wurk.redis do |conn|
|
|
20
|
+
Wurk::Lua::Loader.eval_cached(
|
|
21
|
+
conn,
|
|
22
|
+
:batch_ack_complete,
|
|
23
|
+
keys: ["b-#{bid}", "b-#{bid}-jids", "b-#{bid}-died", "b-#{bid}-failed"],
|
|
24
|
+
argv: [job['jid']]
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
live, _died, first_death = Array(result).map(&:to_i)
|
|
28
|
+
|
|
29
|
+
Wurk::Batch::Callbacks.fire_death(bid) if first_death == 1
|
|
30
|
+
return unless live.zero?
|
|
31
|
+
|
|
32
|
+
Wurk::Batch::Callbacks.fire_complete(bid)
|
|
33
|
+
Wurk::Batch::Callbacks.propagate_to_parent(bid)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Wurk.configuration.death_handlers << Wurk::Batch::DeathHandler
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../worker'
|
|
4
|
+
|
|
5
|
+
module Wurk
|
|
6
|
+
class Batch
|
|
7
|
+
# No-op marker job inserted into an empty `batch.jobs { }` block so
|
|
8
|
+
# `:complete` and `:success` callbacks still fire. Pro 7.1+ behaviour:
|
|
9
|
+
# without this marker, total=0 means no batch_push ever ran and the
|
|
10
|
+
# callback path can't tell "empty batch" from "never flushed batch".
|
|
11
|
+
#
|
|
12
|
+
# Spec: docs/target/sidekiq-pro.md §2.3 / §12.
|
|
13
|
+
class Empty
|
|
14
|
+
include Wurk::Job
|
|
15
|
+
|
|
16
|
+
sidekiq_options retry: false, queue: 'default'
|
|
17
|
+
|
|
18
|
+
def perform; end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative '../middleware'
|
|
5
|
+
require_relative '../lua'
|
|
6
|
+
|
|
7
|
+
module Wurk
|
|
8
|
+
class Batch
|
|
9
|
+
# Server middleware. Runs around `perform` for any job carrying a `bid`.
|
|
10
|
+
# On success → BATCH_ACK_SUCCESS → if pending hit zero and no deaths,
|
|
11
|
+
# enqueue `:success` callback jobs; if live jids hit zero, enqueue
|
|
12
|
+
# `:complete` callback jobs.
|
|
13
|
+
#
|
|
14
|
+
# Invalidated batches short-circuit: the job is skipped without
|
|
15
|
+
# raising — counts as a "success" for batch purposes per spec §12.
|
|
16
|
+
#
|
|
17
|
+
# Death handling lives in Wurk::Batch::DeathHandler (registered as a
|
|
18
|
+
# config death_handler) because death is signalled from the retry layer,
|
|
19
|
+
# not from this middleware's rescue path.
|
|
20
|
+
class ServerMiddleware
|
|
21
|
+
include Wurk::Middleware::ServerMiddleware
|
|
22
|
+
|
|
23
|
+
def call(_worker, job, _queue)
|
|
24
|
+
bid = job['bid']
|
|
25
|
+
return yield unless bid
|
|
26
|
+
|
|
27
|
+
if invalidated?(bid)
|
|
28
|
+
ack_success(bid, job['jid'])
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
yield
|
|
33
|
+
ack_success(bid, job['jid'])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def invalidated?(bid)
|
|
39
|
+
redis_pool.with { |conn| conn.call('HGET', "b-#{bid}", 'invalidated') } == '1'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ack_success(bid, jid)
|
|
43
|
+
result = redis_pool.with do |conn|
|
|
44
|
+
Wurk::Lua::Loader.eval_cached(
|
|
45
|
+
conn,
|
|
46
|
+
:batch_ack_success,
|
|
47
|
+
keys: ["b-#{bid}", "b-#{bid}-jids"],
|
|
48
|
+
argv: [jid]
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
pending, live = Array(result).map(&:to_i)
|
|
52
|
+
return if pending.negative?
|
|
53
|
+
|
|
54
|
+
Wurk::Batch::Callbacks.maybe_fire(bid, pending: pending, live: live)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
require_relative 'callbacks'
|
|
61
|
+
|
|
62
|
+
Wurk.configuration.server_middleware.add(Wurk::Batch::ServerMiddleware)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Wurk
|
|
6
|
+
class Batch
|
|
7
|
+
# Snapshot of a batch's state — read-only view backed by HGETALL of
|
|
8
|
+
# `b-<bid>` plus the supporting JIDs/failed/died sets. Mirrors the
|
|
9
|
+
# Sidekiq::Batch::Status surface from docs/target/sidekiq-pro.md §2.5.
|
|
10
|
+
#
|
|
11
|
+
# `#data` returns the JSON-friendly hash served by the polling endpoint.
|
|
12
|
+
# `#join` blocks the current thread until `complete?` — test/util only.
|
|
13
|
+
# `#delete` UNLINKs every key associated with the batch.
|
|
14
|
+
class Status
|
|
15
|
+
JOIN_POLL_INTERVAL = 0.5
|
|
16
|
+
|
|
17
|
+
attr_reader :bid
|
|
18
|
+
|
|
19
|
+
def initialize(bid)
|
|
20
|
+
raise ArgumentError, 'bid required' if bid.nil? || bid.to_s.empty?
|
|
21
|
+
|
|
22
|
+
@bid = bid.to_s
|
|
23
|
+
reload!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# False when no `b-<bid>` hash exists — a well-formed bid that was never
|
|
27
|
+
# created (or has expired). Lets callers 404 instead of serving an
|
|
28
|
+
# all-zero phantom batch.
|
|
29
|
+
def exists? = !@data.empty?
|
|
30
|
+
|
|
31
|
+
def total = @data['total'].to_i
|
|
32
|
+
def pending = @data['pending'].to_i
|
|
33
|
+
def failures = @data['failures'].to_i
|
|
34
|
+
def created_at = numeric_or_nil(@data['created_at'])
|
|
35
|
+
def complete_at = numeric_or_nil(@data['complete_at'])
|
|
36
|
+
def success_at = numeric_or_nil(@data['success_at'])
|
|
37
|
+
def death_at = numeric_or_nil(@data['death_at'])
|
|
38
|
+
def description = @data['description']
|
|
39
|
+
def parent_bid = @data['parent_bid']
|
|
40
|
+
def callback_queue = @data['callback_queue']
|
|
41
|
+
def invalidated? = @data['invalidated'] == '1'
|
|
42
|
+
|
|
43
|
+
# `:complete` fires when the live jids set is empty (every job has
|
|
44
|
+
# either succeeded or died). Hash field is set by the server callback;
|
|
45
|
+
# falling back to a recompute keeps Status accurate even if the
|
|
46
|
+
# callback dispatch is in flight.
|
|
47
|
+
def complete?
|
|
48
|
+
return true if @data['complete'] == '1'
|
|
49
|
+
|
|
50
|
+
total.positive? && live_jids_count.zero?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def failed_jids
|
|
54
|
+
Wurk.redis { |conn| conn.call('SMEMBERS', "b-#{@bid}-failed") }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def dead_jids
|
|
58
|
+
Wurk.redis { |conn| conn.call('SMEMBERS', "b-#{@bid}-died") }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def child_count
|
|
62
|
+
Wurk.redis { |conn| conn.call('SCARD', "b-#{@bid}-kids") }.to_i
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def tags
|
|
66
|
+
raw = @data['tags']
|
|
67
|
+
return [] if raw.nil? || raw.empty?
|
|
68
|
+
|
|
69
|
+
JSON.parse(raw)
|
|
70
|
+
rescue JSON::ParserError
|
|
71
|
+
[]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# JSON-serializable snapshot used by the polling middleware / web UI.
|
|
75
|
+
# Field names are wire-compat with Sidekiq Pro's BatchStatus.
|
|
76
|
+
def data
|
|
77
|
+
{
|
|
78
|
+
'bid' => @bid,
|
|
79
|
+
'total' => total,
|
|
80
|
+
'pending' => pending,
|
|
81
|
+
'failures' => failures,
|
|
82
|
+
'created_at' => created_at,
|
|
83
|
+
'complete_at' => complete_at,
|
|
84
|
+
'success_at' => success_at,
|
|
85
|
+
'death_at' => death_at,
|
|
86
|
+
'complete' => complete?,
|
|
87
|
+
'invalidated' => invalidated?,
|
|
88
|
+
'description' => description,
|
|
89
|
+
'parent_bid' => parent_bid,
|
|
90
|
+
'tags' => tags,
|
|
91
|
+
'failed_jids' => failed_jids,
|
|
92
|
+
'dead_jids' => dead_jids,
|
|
93
|
+
'child_count' => child_count
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Blocks the current thread until `complete?` is true. Test/util only —
|
|
98
|
+
# polling Redis from a worker thread would defeat the whole point of
|
|
99
|
+
# asynchronous batches.
|
|
100
|
+
def join
|
|
101
|
+
loop do
|
|
102
|
+
reload!
|
|
103
|
+
break if complete?
|
|
104
|
+
|
|
105
|
+
sleep JOIN_POLL_INTERVAL
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Nukes every key for this batch. Dangerous if jobs are still in flight
|
|
110
|
+
# — they'll succeed/fail without a batch to ack against, callbacks won't
|
|
111
|
+
# fire, and counts get permanently inconsistent. Caller's problem.
|
|
112
|
+
def delete
|
|
113
|
+
Wurk.redis do |conn|
|
|
114
|
+
conn.call('UNLINK', *Batch.keys_for(@bid))
|
|
115
|
+
conn.call('ZREM', 'batches', @bid)
|
|
116
|
+
conn.call('ZREM', 'dead-batches', @bid)
|
|
117
|
+
end
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def reload!
|
|
122
|
+
raw = Wurk.redis { |conn| conn.call('HGETALL', "b-#{@bid}") }
|
|
123
|
+
@data = raw.is_a?(Hash) ? raw : raw.each_slice(2).to_h
|
|
124
|
+
self
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def live_jids_count
|
|
130
|
+
Wurk.redis { |conn| conn.call('SCARD', "b-#{@bid}-jids") }.to_i
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def numeric_or_nil(val)
|
|
134
|
+
return nil if val.nil? || val.to_s.empty?
|
|
135
|
+
|
|
136
|
+
val.to_f
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|