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/dead_set.rb
CHANGED
|
@@ -23,22 +23,21 @@ module Wurk
|
|
|
23
23
|
|
|
24
24
|
# Two-axis trim: `ZREMRANGEBYSCORE` evicts entries older than
|
|
25
25
|
# `dead_timeout_in_seconds`, `ZREMRANGEBYRANK 0 -dead_max_jobs` keeps
|
|
26
|
-
# the count bounded.
|
|
27
|
-
# axis applied (acceptable; trim is non-critical, runs again next kill).
|
|
26
|
+
# the count bounded. One MULTI, as upstream — both axes apply together.
|
|
28
27
|
#
|
|
29
28
|
# `max_jobs:` / `timeout:` override the global config for this call.
|
|
30
29
|
# Lets parallel tests run trim with isolated limits without mutating
|
|
31
30
|
# `Wurk.configuration` (which is process-global and races across threads).
|
|
32
|
-
def trim(max_jobs: nil, timeout: nil)
|
|
31
|
+
def trim(max_jobs: nil, timeout: nil)
|
|
33
32
|
config = Wurk.configuration
|
|
34
33
|
max_jobs ||= config[:dead_max_jobs] || 10_000
|
|
35
34
|
timeout ||= config[:dead_timeout_in_seconds] || (180 * 24 * 60 * 60)
|
|
36
35
|
cutoff = ::Process.clock_gettime(::Process::CLOCK_REALTIME) - timeout
|
|
37
36
|
|
|
38
37
|
Wurk.redis do |conn|
|
|
39
|
-
conn.
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
conn.multi do |tx|
|
|
39
|
+
tx.call('ZREMRANGEBYSCORE', @name, '-inf', "(#{cutoff}")
|
|
40
|
+
tx.call('ZREMRANGEBYRANK', @name, 0, -max_jobs)
|
|
42
41
|
end
|
|
43
42
|
end
|
|
44
43
|
true
|
|
@@ -65,7 +64,7 @@ module Wurk
|
|
|
65
64
|
# synthesized RuntimeError when callers don't have one) — death handlers
|
|
66
65
|
# receive `(job, ex)`. `max_jobs:` / `timeout:` propagate to the auto-trim;
|
|
67
66
|
# see `#trim` for the rationale.
|
|
68
|
-
def kill(message, opts = {})
|
|
67
|
+
def kill(message, opts = {})
|
|
69
68
|
notify = opts.fetch(:notify_failure, true)
|
|
70
69
|
do_trim = opts.fetch(:trim, true)
|
|
71
70
|
ex = opts[:ex] || RuntimeError.new(API_KILL_MESSAGE).tap { |e| e.set_backtrace(caller) }
|
data/lib/wurk/encryption.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Wurk
|
|
|
34
34
|
# end
|
|
35
35
|
#
|
|
36
36
|
# Wire format (per docs/target/sidekiq-ent.md §4.4): the last arg becomes
|
|
37
|
-
# a plain JSON Hash `{"v"=>N, "iv"
|
|
37
|
+
# a plain JSON Hash `{"__wurk_enc__"=>true, "v"=>N, "iv"=>…, "ct"=>…, "tag"=>…}`
|
|
38
38
|
# — *not* a base64 blob of a binary envelope — so the args array stays
|
|
39
39
|
# valid JSON for inspectors that don't know about encryption.
|
|
40
40
|
#
|
|
@@ -89,7 +89,7 @@ module Wurk
|
|
|
89
89
|
#
|
|
90
90
|
# Idempotent: re-calling rebinds the resolver and rebuilds the cache.
|
|
91
91
|
# Middleware is installed at most once per chain.
|
|
92
|
-
def enable(active_version:, &resolver)
|
|
92
|
+
def enable(active_version:, &resolver)
|
|
93
93
|
raise ArgumentError, 'active_version is required' unless active_version
|
|
94
94
|
raise ArgumentError, 'block returning the key bytes is required' unless resolver
|
|
95
95
|
|
|
@@ -117,7 +117,7 @@ module Wurk
|
|
|
117
117
|
raise Error, 'Wurk::Encryption not enabled' unless enabled?
|
|
118
118
|
|
|
119
119
|
@key_cache ||= {}
|
|
120
|
-
@key_cache[version] ||= validate_key!(version,
|
|
120
|
+
@key_cache[version] ||= validate_key!(version, resolve(version))
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
# Encrypt `value` (any JSON-serializable Ruby value) under
|
|
@@ -167,8 +167,27 @@ module Wurk
|
|
|
167
167
|
value.key?('v') && value.key?('iv') && value.key?('ct') && value.key?('tag')
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
+
# `args` with its last element enveloped, unless it already is one. The
|
|
171
|
+
# single sealing rule shared by the client middleware and every server
|
|
172
|
+
# path that writes a payload back to Redis (InterruptHandler, limiter
|
|
173
|
+
# dead-routing): a job hash seen mid-chain may hold decrypted args.
|
|
174
|
+
def seal_args(args)
|
|
175
|
+
return args unless args.is_a?(::Array) && !args.empty? && !envelope?(args.last)
|
|
176
|
+
|
|
177
|
+
args[0..-2] + [encrypt(args.last)]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# `job` safe to write back to Redis: a copy with the last arg re-sealed
|
|
181
|
+
# when crypto is on and the job opted in, otherwise `job` itself.
|
|
182
|
+
def seal(job)
|
|
183
|
+
return job unless enabled? && job['encrypt']
|
|
184
|
+
|
|
185
|
+
sealed = seal_args(job['args'])
|
|
186
|
+
sealed.equal?(job['args']) ? job : job.merge('args' => sealed)
|
|
187
|
+
end
|
|
188
|
+
|
|
170
189
|
# Web UI display helper (§4.7). Given a job hash, returns the args
|
|
171
|
-
# array with the last element replaced by the literal `"
|
|
190
|
+
# array with the last element replaced by the literal `"[encrypted data]"`
|
|
172
191
|
# when the job opted in. Cleartext preceding args are untouched so
|
|
173
192
|
# operators can still triage on user_id / object_id / etc.
|
|
174
193
|
#
|
|
@@ -181,7 +200,7 @@ module Wurk
|
|
|
181
200
|
return args if args.empty?
|
|
182
201
|
return args unless job['encrypt'] || job[:encrypt] || envelope?(args.last)
|
|
183
202
|
|
|
184
|
-
args[0..-2] + ['
|
|
203
|
+
args[0..-2] + ['[encrypted data]']
|
|
185
204
|
end
|
|
186
205
|
|
|
187
206
|
# A decryption failure means the key is gone (rotated away) or the
|
|
@@ -212,6 +231,16 @@ module Wurk
|
|
|
212
231
|
cipher
|
|
213
232
|
end
|
|
214
233
|
|
|
234
|
+
# A resolver built on `keys.fetch(v)` / `ENV.fetch(...)` raises KeyError
|
|
235
|
+
# (an IndexError) for a rotated-away version where a Hash#[] resolver
|
|
236
|
+
# returns nil. Both mean "no key for this version", so both must take
|
|
237
|
+
# the same dead-set path; a bare KeyError would bubble into 25 retries.
|
|
238
|
+
def resolve(version)
|
|
239
|
+
@resolver.call(version)
|
|
240
|
+
rescue ::IndexError => e
|
|
241
|
+
raise KeyMissingError, "key resolver has no key for version #{version} (#{e.class}: #{e.message})"
|
|
242
|
+
end
|
|
243
|
+
|
|
215
244
|
def validate_key!(version, bytes)
|
|
216
245
|
raise KeyMissingError, "key resolver returned nil for version #{version}" if bytes.nil?
|
|
217
246
|
|
|
@@ -240,17 +269,16 @@ module Wurk
|
|
|
240
269
|
include Wurk::Middleware::ClientMiddleware
|
|
241
270
|
|
|
242
271
|
def call(_worker, job, _queue, _redis_pool)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
args = job['args']
|
|
246
|
-
if args.is_a?(::Array) && !args.empty? && !Wurk::Encryption.envelope?(args.last)
|
|
247
|
-
job['args'] = args[0..-2] + [Wurk::Encryption.encrypt(args.last)]
|
|
248
|
-
end
|
|
272
|
+
job['args'] = Wurk::Encryption.seal_args(job['args']) if Wurk::Encryption.enabled? && job['encrypt']
|
|
249
273
|
yield
|
|
250
274
|
end
|
|
251
275
|
end
|
|
252
276
|
|
|
253
|
-
# Server middleware — peels the envelope before perform runs
|
|
277
|
+
# Server middleware — peels the envelope before perform runs and puts it
|
|
278
|
+
# back once the chain unwinds, success or failure. The job hash is shared
|
|
279
|
+
# with every outer middleware and the processor; leaving plaintext in it
|
|
280
|
+
# let any of them write the secret back to Redis (InterruptHandler's
|
|
281
|
+
# re-push, limiter dead-routing) or hand it to an error reporter. A decrypt
|
|
254
282
|
# failure (missing/rotated key, bad tag) is terminal and non-retryable,
|
|
255
283
|
# so rather than let it bubble into the 25× retry pipeline we route the
|
|
256
284
|
# job straight to the dead set tagged `encryption_error` and ACK it via
|
|
@@ -262,8 +290,13 @@ module Wurk
|
|
|
262
290
|
def call(_worker, job, _queue)
|
|
263
291
|
return yield unless Wurk::Encryption.enabled? && job['encrypt']
|
|
264
292
|
|
|
293
|
+
sealed = job['args']
|
|
265
294
|
decrypt_last_arg!(job)
|
|
266
|
-
|
|
295
|
+
begin
|
|
296
|
+
yield
|
|
297
|
+
ensure
|
|
298
|
+
job['args'] = sealed
|
|
299
|
+
end
|
|
267
300
|
end
|
|
268
301
|
|
|
269
302
|
private
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../lua'
|
|
4
|
+
require_relative '../pid_cache'
|
|
5
|
+
|
|
6
|
+
module Wurk
|
|
7
|
+
class Fetcher
|
|
8
|
+
# The rest of {Fetcher::Reliable#walk}'s non-blocking pass once the first
|
|
9
|
+
# queue came back empty: every remaining queue in one round trip
|
|
10
|
+
# (lib/wurk/lua/fetch_first.lua) instead of one LMOVE per queue. A sparse
|
|
11
|
+
# 100-queue capsule used to pay 100 round trips per pass to reach a job on
|
|
12
|
+
# its last queue; it now pays two. The script walks the order it is given,
|
|
13
|
+
# so strict order, the weighted shuffle and the paused filter all stay
|
|
14
|
+
# Reliable#queues_cmd's.
|
|
15
|
+
#
|
|
16
|
+
# Why not the first queue too, for one round trip: a script pays Redis per
|
|
17
|
+
# key it is handed, before it moves anything — measured on Redis 7.0 at
|
|
18
|
+
# ~13us server time for 10 queues and ~83us for 100, against ~0.2us for a
|
|
19
|
+
# bare LMOVE. A capsule whose first queue is busy is the common shape, and
|
|
20
|
+
# folding that queue into the script made its every fetch 1.4x slower
|
|
21
|
+
# end to end on 10 queues. Leading with the LMOVE keeps that path exactly
|
|
22
|
+
# where it was and spends the script only on a pass that already missed.
|
|
23
|
+
module ManyQueues
|
|
24
|
+
NO_ARGV = [].freeze
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Queues 2..n. Two queues is one more LMOVE — a one-key script costs more
|
|
29
|
+
# than the move it would wrap.
|
|
30
|
+
#
|
|
31
|
+
# Apply-safe for the reason #lmove is: a claim whose reply was lost left
|
|
32
|
+
# the job in this process's private list, un-ACKed, which the next boot's
|
|
33
|
+
# Reaper reclaims, and the replay claims a different job.
|
|
34
|
+
def claim_rest(queues)
|
|
35
|
+
return lmove(queues[1]) if queues.size == 2
|
|
36
|
+
|
|
37
|
+
keys = claim_keys(queues)
|
|
38
|
+
index, job = config.redis(idempotent: true) do |conn|
|
|
39
|
+
Wurk::Lua::Loader.eval_cached(conn, :fetch_first, keys: keys, argv: NO_ARGV)
|
|
40
|
+
end
|
|
41
|
+
return nil unless job
|
|
42
|
+
|
|
43
|
+
public_q = queues[index]
|
|
44
|
+
priv, name = queue_keys(public_q)
|
|
45
|
+
unit_of_work(public_q, priv, name, job)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# `[public, private, public, private, ...]` for queues 2..n, fetch_first's
|
|
49
|
+
# KEYS. Strict mode with nothing paused hands #walk the same frozen array
|
|
50
|
+
# every pass, so the steady state reuses one build; a shuffled or filtered
|
|
51
|
+
# list is a fresh array per pass and rebuilds. Keyed on the pid too, for
|
|
52
|
+
# the reason #queue_keys is. Copy-on-write: processor threads share this
|
|
53
|
+
# fetcher.
|
|
54
|
+
def claim_keys(queues)
|
|
55
|
+
pid = PidCache.pid
|
|
56
|
+
cached = @claim_keys
|
|
57
|
+
return cached[2] if cached && cached[0].equal?(queues) && cached[1] == pid
|
|
58
|
+
|
|
59
|
+
keys = queues.drop(1).flat_map { |public_q| [public_q, queue_keys(public_q).first] }.freeze
|
|
60
|
+
@claim_keys = [queues, pid, keys].freeze
|
|
61
|
+
keys
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wurk
|
|
4
|
+
class Fetcher
|
|
5
|
+
# The Reaper's boot-grace check: has this heartbeat-judged orphan been
|
|
6
|
+
# left alone for at least `grace` seconds? A booting worker can claim a job
|
|
7
|
+
# a moment before its first heartbeat lands, and draining its list in that
|
|
8
|
+
# window runs the job twice, concurrently.
|
|
9
|
+
#
|
|
10
|
+
# The answer comes from OBJECT IDLETIME, which a live owner resets on every
|
|
11
|
+
# claim and ACK. Some servers refuse it — Redis under an LFU maxmemory
|
|
12
|
+
# policy, Dragonfly with no OBJECT at all — and the grace must not quietly
|
|
13
|
+
# vanish with it. Without IDLETIME the clock is this reaper's own: a list
|
|
14
|
+
# is drained only once it has been judged orphaned for `grace` seconds
|
|
15
|
+
# across sweeps, by which time a booting owner has beaten and the
|
|
16
|
+
# heartbeat re-check spares it. The switch is logged once.
|
|
17
|
+
class OrphanGrace
|
|
18
|
+
NO_IDLETIME_WARNING = 'reaper: OBJECT IDLETIME is unavailable on this Redis (%s); the boot grace ' \
|
|
19
|
+
'before reclaiming a private list now runs on this process\'s own clock, so an ' \
|
|
20
|
+
'orphan is drained one sweep later than it would be. Expected under an LFU ' \
|
|
21
|
+
'maxmemory policy or on servers without OBJECT (e.g. Dragonfly).'
|
|
22
|
+
|
|
23
|
+
# Lists remembered for the fallback clock. A list leaves on the pass
|
|
24
|
+
# that clears it; one judged orphaned and then found alive can linger,
|
|
25
|
+
# so the memory is bounded and simply restarts when full — every clock
|
|
26
|
+
# restarting is the conservative direction.
|
|
27
|
+
MEMORY = 10_000
|
|
28
|
+
|
|
29
|
+
def initialize(config, grace)
|
|
30
|
+
@config = config
|
|
31
|
+
@grace = grace
|
|
32
|
+
@idletime = true
|
|
33
|
+
@first_seen = {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# true while the list must be left alone.
|
|
37
|
+
def recent?(key)
|
|
38
|
+
return false unless @grace.positive?
|
|
39
|
+
return local_recent?(key) unless @idletime
|
|
40
|
+
|
|
41
|
+
idle = @config.redis(idempotent: true) { |c| c.call('OBJECT', 'IDLETIME', key) }
|
|
42
|
+
# nil: the list is already gone, nothing to drain.
|
|
43
|
+
idle.nil? || idle < @grace
|
|
44
|
+
rescue RedisClient::CommandError => e
|
|
45
|
+
idletime_unavailable!(e)
|
|
46
|
+
local_recent?(key)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def local_recent?(key)
|
|
52
|
+
now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
53
|
+
@first_seen.clear if @first_seen.size >= MEMORY && !@first_seen.key?(key)
|
|
54
|
+
seen = (@first_seen[key] ||= now)
|
|
55
|
+
return true if now - seen < @grace
|
|
56
|
+
|
|
57
|
+
@first_seen.delete(key)
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def idletime_unavailable!(error)
|
|
62
|
+
@idletime = false
|
|
63
|
+
@config.logger.warn { format(NO_IDLETIME_WARNING, error.message.lines.first&.strip) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../keys'
|
|
4
|
+
|
|
5
|
+
module Wurk
|
|
6
|
+
class Fetcher
|
|
7
|
+
# Reads the owner back out of a reliable-fetch private list key
|
|
8
|
+
# (`queue:<public>|<host>|<pid>|<nonce>|<idx>`, see
|
|
9
|
+
# Reliable.private_queue_name) for the Reaper's liveness check. Pure string
|
|
10
|
+
# work, no Redis.
|
|
11
|
+
module PrivateListKey
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# `queue:<public>|<host>|<pid>|<nonce>|<idx>` → [public_q, host, pid,
|
|
15
|
+
# nonce], parsed from the right so a `|` inside the queue name is
|
|
16
|
+
# tolerated. nil when the key isn't a well-formed private list.
|
|
17
|
+
#
|
|
18
|
+
# With no known prefix to split on, both owner shapes are tried in
|
|
19
|
+
# preference order and the first one leaving a real public queue behind
|
|
20
|
+
# wins. The narrow reading is what saves a pre-nonce key from an all-digit
|
|
21
|
+
# host (`queue:q|123456789012|<pid>|<idx>` — a bare Docker hostname is 12
|
|
22
|
+
# hex chars): read wide, its host segment eats the whole queue name.
|
|
23
|
+
def parse_full(key)
|
|
24
|
+
parts = key.split('|')
|
|
25
|
+
owner_tails(parts).each do |host, pid, nonce, width|
|
|
26
|
+
public_q = parts[0...-width].join('|')
|
|
27
|
+
next unless public_q.start_with?(Keys::QUEUE_PREFIX) && public_q != Keys::QUEUE_PREFIX
|
|
28
|
+
|
|
29
|
+
return [public_q, host, pid, nonce]
|
|
30
|
+
end
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# `<public_q>|<host>|<pid>|<nonce>|<idx>` → [host, pid, nonce] (pid as
|
|
35
|
+
# Integer), or all-nil when the suffix isn't a well-formed owner tail.
|
|
36
|
+
# Splitting the suffix off the known public-queue prefix tolerates a `|`
|
|
37
|
+
# inside the queue name itself, and leaves the tail unambiguous: exactly
|
|
38
|
+
# 4 segments for the current shape, exactly 3 for the pre-nonce one.
|
|
39
|
+
def parse_owner(public_q, key)
|
|
40
|
+
suffix = key.delete_prefix("#{public_q}|")
|
|
41
|
+
return [nil, nil, nil] if suffix == key
|
|
42
|
+
|
|
43
|
+
host, pid, nonce = owner_tails(suffix.split('|')).first
|
|
44
|
+
[host, pid, nonce]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Owner segments of a private-list key, taken from the right, as
|
|
48
|
+
# [host, pid, nonce, segment_count] readings in preference order (empty
|
|
49
|
+
# when nothing parses). The wide shape is preferred: an all-digit nonce is
|
|
50
|
+
# rare but reachable, and reading such a key narrow would take the pid for
|
|
51
|
+
# the host and the nonce for the pid — draining a live owner's list out
|
|
52
|
+
# from under it.
|
|
53
|
+
def owner_tails(parts)
|
|
54
|
+
return [] unless parts.size >= 3 && integer?(parts[-1])
|
|
55
|
+
|
|
56
|
+
[wide_tail(parts), narrow_tail(parts)].compact
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# `<host>|<pid>|<nonce>|<idx>` — the shape every current process writes.
|
|
60
|
+
def wide_tail(parts)
|
|
61
|
+
[parts[-4], parts[-3].to_i, parts[-2], 4] if parts.size >= 4 && integer?(parts[-3])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# `<host>|<pid>|<idx>` — written before the nonce existed. Such a list can
|
|
65
|
+
# still hold a pre-upgrade process's in-flight jobs across a rolling
|
|
66
|
+
# upgrade, so it stays reclaimable even though nothing writes it anymore.
|
|
67
|
+
def narrow_tail(parts)
|
|
68
|
+
[parts[-3], parts[-2].to_i, nil, 3] if integer?(parts[-2])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def integer?(str)
|
|
72
|
+
str.is_a?(String) && str.match?(/\A\d+\z/)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/wurk/fetcher/reaper.rb
CHANGED
|
@@ -4,6 +4,9 @@ require_relative '../component'
|
|
|
4
4
|
require_relative '../keys'
|
|
5
5
|
require_relative '../middleware/poison_pill'
|
|
6
6
|
require_relative '../timer_loop'
|
|
7
|
+
require_relative 'private_list_key'
|
|
8
|
+
require_relative 'unparseable_keys'
|
|
9
|
+
require_relative 'orphan_grace'
|
|
7
10
|
|
|
8
11
|
module Wurk
|
|
9
12
|
class Fetcher
|
|
@@ -30,6 +33,15 @@ module Wurk
|
|
|
30
33
|
# `processes` member (one whose `info` hash still exists). Such reclaim
|
|
31
34
|
# therefore waits out the 60s heartbeat TTL, exactly as the spec says.
|
|
32
35
|
#
|
|
36
|
+
# The heartbeat verdict is a snapshot taken before a SCAN that can run for
|
|
37
|
+
# a long time on a big keyspace, and a booting process can claim jobs a
|
|
38
|
+
# moment before its first beat lands. Draining on that snapshot alone
|
|
39
|
+
# reclaims a live worker's in-flight job — a double run. So a heartbeat-
|
|
40
|
+
# judged orphan is re-checked right before its drain: its list must have
|
|
41
|
+
# sat untouched for `grace` seconds (OBJECT IDLETIME; a live owner touches
|
|
42
|
+
# it on every claim and ACK), and its identity must still have no `info`
|
|
43
|
+
# hash. Our own incarnation skips both: kill(0) is not a snapshot.
|
|
44
|
+
#
|
|
33
45
|
# Re-pushed jobs run through Wurk::Middleware::PoisonPill, which caps a
|
|
34
46
|
# job at RECOVERY_THRESHOLD recoveries within 72h: past the cap the job
|
|
35
47
|
# is killed into the dead set instead of re-queued, so a job that crashes
|
|
@@ -59,17 +71,31 @@ module Wurk
|
|
|
59
71
|
# the fleet, since a global SCAN is far costlier than the scoped pass.
|
|
60
72
|
FULL_INTERVAL = 3600
|
|
61
73
|
|
|
74
|
+
# Seconds a heartbeat-judged private list must have sat idle before it is
|
|
75
|
+
# drained. Far below the heartbeat TTL a dead owner's list already waits
|
|
76
|
+
# out, so it never delays a real reclaim; well above the gap between a
|
|
77
|
+
# booting process's first claim and its first beat.
|
|
78
|
+
DEFAULT_GRACE = 30
|
|
79
|
+
|
|
62
80
|
LOCK_KEY = 'super_fetch:reaper'
|
|
63
81
|
FULL_LOCK_KEY = 'super_fetch:reaper:full'
|
|
64
82
|
SCAN_COUNT = 100
|
|
83
|
+
|
|
84
|
+
# The hourly sweep walks the whole keyspace, where almost nothing
|
|
85
|
+
# matches: COUNT is the work per SCAN call, so a bigger batch is the
|
|
86
|
+
# same server work in a tenth of the round trips — a million keys is
|
|
87
|
+
# 1,000 calls instead of 10,000. Still small enough that one call never
|
|
88
|
+
# holds Redis for more than a fraction of a millisecond.
|
|
89
|
+
FULL_SCAN_COUNT = 1000
|
|
65
90
|
THREAD_NAME = 'wurk-reaper'
|
|
66
91
|
|
|
67
92
|
attr_reader :interval
|
|
68
93
|
|
|
69
94
|
def initialize(config, interval: DEFAULT_INTERVAL, lock_key: LOCK_KEY,
|
|
70
|
-
full_interval: FULL_INTERVAL, full_lock_key: FULL_LOCK_KEY)
|
|
95
|
+
full_interval: FULL_INTERVAL, full_lock_key: FULL_LOCK_KEY, grace: DEFAULT_GRACE)
|
|
71
96
|
@config = config
|
|
72
97
|
@interval = interval
|
|
98
|
+
@grace = OrphanGrace.new(config, grace)
|
|
73
99
|
@lock_key = lock_key
|
|
74
100
|
@full_interval = full_interval
|
|
75
101
|
@full_lock_key = full_lock_key
|
|
@@ -77,6 +103,7 @@ module Wurk
|
|
|
77
103
|
@done = false
|
|
78
104
|
@mutex = ::Mutex.new
|
|
79
105
|
@sleeper = ::ConditionVariable.new
|
|
106
|
+
@unparseable = UnparseableKeys.new(config)
|
|
80
107
|
end
|
|
81
108
|
|
|
82
109
|
# Spawns the sweep loop. Idempotent. The loop waits one interval before
|
|
@@ -139,7 +166,7 @@ module Wurk
|
|
|
139
166
|
owners = live_owners
|
|
140
167
|
reclaimed = 0
|
|
141
168
|
each_full_private_list do |key, public_q, host, pid, nonce|
|
|
142
|
-
next
|
|
169
|
+
next unless orphaned?(key, host, pid, nonce, owners)
|
|
143
170
|
|
|
144
171
|
reclaimed += drain(key, public_q)
|
|
145
172
|
end
|
|
@@ -162,7 +189,7 @@ module Wurk
|
|
|
162
189
|
def reclaim_queue(public_q, owners)
|
|
163
190
|
reclaimed = 0
|
|
164
191
|
each_private_list(public_q) do |key, host, pid, nonce|
|
|
165
|
-
next
|
|
192
|
+
next unless orphaned?(key, host, pid, nonce, owners)
|
|
166
193
|
|
|
167
194
|
reclaimed += drain(key, public_q)
|
|
168
195
|
end
|
|
@@ -179,8 +206,8 @@ module Wurk
|
|
|
179
206
|
c.call('SCAN', cursor, 'MATCH', "#{public_q}|*", 'COUNT', SCAN_COUNT)
|
|
180
207
|
end
|
|
181
208
|
keys.each do |key|
|
|
182
|
-
host, pid, nonce = parse_owner(public_q, key)
|
|
183
|
-
yield
|
|
209
|
+
host, pid, nonce = PrivateListKey.parse_owner(public_q, key)
|
|
210
|
+
pid ? yield(key, host, pid, nonce) : @unparseable.report(key)
|
|
184
211
|
end
|
|
185
212
|
break if cursor == '0'
|
|
186
213
|
end
|
|
@@ -188,83 +215,22 @@ module Wurk
|
|
|
188
215
|
|
|
189
216
|
# Yields [private_list_key, public_q, host, pid, nonce] for every list in
|
|
190
217
|
# the keyspace. MATCH `queue:*|*` matches only private lists (public queue
|
|
191
|
-
# keys carry no `|`);
|
|
218
|
+
# keys carry no `|`); PrivateListKey.parse_full drops anything that isn't a
|
|
192
219
|
# well-formed `queue:<public>|<host>|<pid>|<nonce>|<idx>`.
|
|
193
220
|
def each_full_private_list
|
|
194
221
|
cursor = '0'
|
|
195
222
|
loop do
|
|
196
223
|
cursor, keys = redis(idempotent: true) do |c|
|
|
197
|
-
c.call('SCAN', cursor, 'MATCH', "#{Keys::QUEUE_PREFIX}*|*", 'COUNT',
|
|
224
|
+
c.call('SCAN', cursor, 'MATCH', "#{Keys::QUEUE_PREFIX}*|*", 'COUNT', FULL_SCAN_COUNT)
|
|
198
225
|
end
|
|
199
226
|
keys.each do |key|
|
|
200
|
-
parsed =
|
|
201
|
-
yield
|
|
227
|
+
parsed = PrivateListKey.parse_full(key)
|
|
228
|
+
parsed ? yield(key, *parsed) : @unparseable.report(key)
|
|
202
229
|
end
|
|
203
230
|
break if cursor == '0'
|
|
204
231
|
end
|
|
205
232
|
end
|
|
206
233
|
|
|
207
|
-
# `queue:<public>|<host>|<pid>|<nonce>|<idx>` → [public_q, host, pid,
|
|
208
|
-
# nonce], parsed from the right so a `|` inside the queue name is
|
|
209
|
-
# tolerated. nil when the key isn't a well-formed private list.
|
|
210
|
-
#
|
|
211
|
-
# With no known prefix to split on, both owner shapes are tried in
|
|
212
|
-
# preference order and the first one leaving a real public queue behind
|
|
213
|
-
# wins. The narrow reading is what saves a pre-nonce key from an all-digit
|
|
214
|
-
# host (`queue:q|123456789012|<pid>|<idx>` — a bare Docker hostname is 12
|
|
215
|
-
# hex chars): read wide, its host segment eats the whole queue name.
|
|
216
|
-
def parse_full_key(key)
|
|
217
|
-
parts = key.split('|')
|
|
218
|
-
owner_tails(parts).each do |host, pid, nonce, width|
|
|
219
|
-
public_q = parts[0...-width].join('|')
|
|
220
|
-
next unless public_q.start_with?(Keys::QUEUE_PREFIX) && public_q != Keys::QUEUE_PREFIX
|
|
221
|
-
|
|
222
|
-
return [public_q, host, pid, nonce]
|
|
223
|
-
end
|
|
224
|
-
nil
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# `<public_q>|<host>|<pid>|<nonce>|<idx>` → [host, pid, nonce] (pid as
|
|
228
|
-
# Integer), or all-nil when the suffix isn't a well-formed owner tail.
|
|
229
|
-
# Splitting the suffix off the known public-queue prefix tolerates a `|`
|
|
230
|
-
# inside the queue name itself, and leaves the tail unambiguous: exactly
|
|
231
|
-
# 4 segments for the current shape, exactly 3 for the pre-nonce one.
|
|
232
|
-
def parse_owner(public_q, key)
|
|
233
|
-
suffix = key.delete_prefix("#{public_q}|")
|
|
234
|
-
return [nil, nil, nil] if suffix == key
|
|
235
|
-
|
|
236
|
-
host, pid, nonce = owner_tails(suffix.split('|')).first
|
|
237
|
-
[host, pid, nonce]
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
# Owner segments of a private-list key, taken from the right, as
|
|
241
|
-
# [host, pid, nonce, segment_count] readings in preference order (empty
|
|
242
|
-
# when nothing parses). The wide shape is preferred: an all-digit nonce is
|
|
243
|
-
# rare but reachable, and reading such a key narrow would take the pid for
|
|
244
|
-
# the host and the nonce for the pid — draining a live owner's list out
|
|
245
|
-
# from under it.
|
|
246
|
-
def owner_tails(parts)
|
|
247
|
-
return [] unless parts.size >= 3 && integer?(parts[-1])
|
|
248
|
-
|
|
249
|
-
[wide_tail(parts), narrow_tail(parts)].compact
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
# `<host>|<pid>|<nonce>|<idx>` — the shape every current process writes.
|
|
253
|
-
def wide_tail(parts)
|
|
254
|
-
[parts[-4], parts[-3].to_i, parts[-2], 4] if parts.size >= 4 && integer?(parts[-3])
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
# `<host>|<pid>|<idx>` — written before the nonce existed. Such a list can
|
|
258
|
-
# still hold a pre-upgrade process's in-flight jobs across a rolling
|
|
259
|
-
# upgrade, so it stays reclaimable even though nothing writes it anymore.
|
|
260
|
-
def narrow_tail(parts)
|
|
261
|
-
[parts[-3], parts[-2].to_i, nil, 3] if integer?(parts[-2])
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def integer?(str)
|
|
265
|
-
str.is_a?(String) && str.match?(/\A\d+\z/)
|
|
266
|
-
end
|
|
267
|
-
|
|
268
234
|
# `Process.kill(0, pid)` answers "does this pid exist *in my PID
|
|
269
235
|
# namespace*", which is the question we're actually asking only when the
|
|
270
236
|
# key was written from that same namespace. A shared hostname does not
|
|
@@ -281,11 +247,26 @@ module Wurk
|
|
|
281
247
|
# Every other owner goes through the namespace-blind heartbeat — alive iff
|
|
282
248
|
# its identity is a live `processes` member. A pre-nonce key can only be
|
|
283
249
|
# matched on the `<host>:<pid>` prefix of that identity.
|
|
284
|
-
def
|
|
285
|
-
return local_pid_alive?(pid) if nonce == process_nonce && host == hostname
|
|
286
|
-
return owners.include?(
|
|
250
|
+
def orphaned?(key, host, pid, nonce, owners)
|
|
251
|
+
return !local_pid_alive?(pid) if nonce == process_nonce && host == hostname
|
|
252
|
+
return false if owners.include?(owner_key(host, pid, nonce))
|
|
253
|
+
|
|
254
|
+
settled_orphan?(key, host, pid, nonce)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def owner_key(host, pid, nonce)
|
|
258
|
+
nonce ? "#{host}:#{pid}:#{nonce}" : "#{host}:#{pid}"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# The re-check right before a heartbeat-judged drain (see the class doc):
|
|
262
|
+
# the `owners` snapshot may predate the owner's first beat, or the whole
|
|
263
|
+
# SCAN. Only reached for lists the snapshot already called orphaned, so
|
|
264
|
+
# its round trips are paid per orphan, never per live list.
|
|
265
|
+
def settled_orphan?(key, host, pid, nonce)
|
|
266
|
+
return false if @grace.recent?(key)
|
|
267
|
+
return !live_owners.include?(owner_key(host, pid, nil)) unless nonce
|
|
287
268
|
|
|
288
|
-
|
|
269
|
+
redis(idempotent: true) { |c| c.call('HGET', owner_key(host, pid, nonce), 'info') }.nil?
|
|
289
270
|
end
|
|
290
271
|
|
|
291
272
|
def local_pid_alive?(pid)
|
|
@@ -7,6 +7,7 @@ require_relative '../keys'
|
|
|
7
7
|
require_relative '../lua'
|
|
8
8
|
require_relative '../fetcher'
|
|
9
9
|
require_relative 'capped'
|
|
10
|
+
require_relative 'many_queues'
|
|
10
11
|
|
|
11
12
|
module Wurk
|
|
12
13
|
class Fetcher
|
|
@@ -23,10 +24,13 @@ module Wurk
|
|
|
23
24
|
# the paths that must send a held ACK before they stop fetching, and
|
|
24
25
|
# docs/idea/parity-divergences.md for the window that widens.
|
|
25
26
|
#
|
|
26
|
-
# Priority handling:
|
|
27
|
-
# LMOVE
|
|
28
|
-
#
|
|
29
|
-
#
|
|
27
|
+
# Priority handling: walk queues_cmd in order with a non-blocking claim —
|
|
28
|
+
# an LMOVE on the first queue, then one fetch_first script call over all
|
|
29
|
+
# the others (ManyQueues) — then fall back to a blocking BLMOVE
|
|
30
|
+
# on the first queue so an empty poll doesn't spin Redis. BLMOVE has no
|
|
31
|
+
# multi-key form, so blocking on a single queue is the best Redis gives us:
|
|
32
|
+
# a job landing on a later queue while every thread is parked waits for
|
|
33
|
+
# the next pass, at most one block timeout. The block
|
|
30
34
|
# timeout defaults to TIMEOUT (2s) and is overridable per the Pro
|
|
31
35
|
# super_fetch §3.3 `config.fetch_poll_interval` knob.
|
|
32
36
|
#
|
|
@@ -40,6 +44,7 @@ module Wurk
|
|
|
40
44
|
# that caps nothing, which is a stronger guarantee that the unconfigured
|
|
41
45
|
# path is unchanged than a second loop kept in step by hand.
|
|
42
46
|
prepend Capped
|
|
47
|
+
include ManyQueues
|
|
43
48
|
|
|
44
49
|
# Default BLMOVE block timeout; overridable via config.fetch_poll_interval.
|
|
45
50
|
TIMEOUT = 2
|
|
@@ -72,7 +77,7 @@ module Wurk
|
|
|
72
77
|
# namespaces are in play: a restarted container reuses both, so the
|
|
73
78
|
# reaper's `kill(0)` liveness check would read a dead owner's list as
|
|
74
79
|
# live (jobs stranded) or a live owner's as dead (job run twice). Keys
|
|
75
|
-
# written before the nonce existed stay reclaimable —
|
|
80
|
+
# written before the nonce existed stay reclaimable — PrivateListKey
|
|
76
81
|
# accepts both shapes.
|
|
77
82
|
def self.private_queue_name(public_queue, index = 0)
|
|
78
83
|
host = ENV['DYNO'] || Socket.gethostname
|
|
@@ -112,6 +117,7 @@ module Wurk
|
|
|
112
117
|
@queue_keys_pid = ::Process.pid
|
|
113
118
|
@prefixed_queues = nil
|
|
114
119
|
@prefixed_source = nil
|
|
120
|
+
@claim_keys = nil
|
|
115
121
|
end
|
|
116
122
|
|
|
117
123
|
# Take custody of a finished job's LREM instead of sending it now. One
|
|
@@ -246,20 +252,20 @@ module Wurk
|
|
|
246
252
|
config.config.handle_exception(ex, ctx)
|
|
247
253
|
end
|
|
248
254
|
|
|
249
|
-
# Non-blocking pass over the fetchable queues
|
|
250
|
-
#
|
|
251
|
-
#
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
+
# Non-blocking pass over the fetchable queues, then the blocking
|
|
256
|
+
# fall-through. The pending ACK rides the first LMOVE and only the first.
|
|
257
|
+
# By the time we fall through to BLMOVE this thread is holding nothing —
|
|
258
|
+
# which is the point, since a blocking call can't join a pipeline and
|
|
259
|
+
# would strand the ACK for a whole poll interval.
|
|
260
|
+
#
|
|
261
|
+
# The first queue is always a bare LMOVE, so the busy-first-queue steady
|
|
262
|
+
# state (and the one-queue capsule `rake bench` gates) costs exactly what
|
|
263
|
+
# it did. Only a miss there pays for the rest — see ManyQueues.
|
|
255
264
|
def walk(queues)
|
|
256
265
|
ack = take_pending_ack
|
|
257
|
-
queues.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return uow if uow
|
|
261
|
-
end
|
|
262
|
-
blmove(queues.first)
|
|
266
|
+
uow = lmove(queues.first, ack)
|
|
267
|
+
uow ||= claim_rest(queues) if queues.size > 1
|
|
268
|
+
uow || blmove(queues.first)
|
|
263
269
|
end
|
|
264
270
|
|
|
265
271
|
# Exactly one, even when a failed flush left this thread holding two: the
|