wurk 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +40 -2
- data/app/controllers/wurk/api/serializers.rb +51 -1
- data/app/controllers/wurk/api_controller.rb +42 -6
- data/app/controllers/wurk/dashboard_controller.rb +67 -10
- data/config/routes.rb +30 -3
- data/exe/wurk +6 -2
- data/lib/generators/wurk/install/install_generator.rb +3 -3
- data/lib/sidekiq/job_retry.rb +4 -0
- data/lib/sidekiq/manager.rb +4 -0
- data/lib/sidekiq/processor.rb +4 -0
- data/lib/wurk/api/app.rb +186 -0
- data/lib/wurk/api/auth.rb +167 -0
- data/lib/wurk/api/flows.rb +66 -0
- data/lib/wurk/api/idempotency.rb +168 -0
- data/lib/wurk/api/jobs.rb +180 -0
- data/lib/wurk/api/page.rb +98 -0
- data/lib/wurk/api/problem.rb +121 -0
- data/lib/wurk/api/queues.rb +126 -0
- data/lib/wurk/api/read_only.rb +68 -0
- data/lib/wurk/api/request.rb +46 -0
- data/lib/wurk/api/response.rb +28 -0
- data/lib/wurk/api/roll_up.rb +126 -0
- data/lib/wurk/api/router.rb +88 -0
- data/lib/wurk/api/serializers.rb +232 -0
- data/lib/wurk/api/swarm.rb +297 -0
- data/lib/wurk/api/throttle.rb +109 -0
- data/lib/wurk/api/validation.rb +306 -0
- data/lib/wurk/api.rb +96 -0
- data/lib/wurk/batch/server_middleware.rb +2 -2
- data/lib/wurk/batch.rb +12 -3
- data/lib/wurk/capsule.rb +8 -1
- data/lib/wurk/cli.rb +110 -8
- data/lib/wurk/client/buffered.rb +1 -1
- data/lib/wurk/client.rb +42 -3
- data/lib/wurk/collapse.rb +383 -0
- data/lib/wurk/command_builder.rb +49 -0
- data/lib/wurk/compat.rb +19 -0
- data/lib/wurk/component.rb +16 -3
- data/lib/wurk/configuration.rb +303 -4
- data/lib/wurk/debounce.rb +125 -0
- data/lib/wurk/encryption.rb +6 -1
- data/lib/wurk/fetcher/capped.rb +218 -0
- data/lib/wurk/fetcher/reliable.rb +22 -66
- data/lib/wurk/fetcher/unit_of_work.rb +107 -0
- data/lib/wurk/flow/builder.rb +271 -0
- data/lib/wurk/flow/chain.rb +42 -0
- data/lib/wurk/flow/completion.rb +114 -0
- data/lib/wurk/flow/creation.rb +254 -0
- data/lib/wurk/flow/node.rb +124 -0
- data/lib/wurk/flow/status.rb +206 -0
- data/lib/wurk/flow.rb +255 -0
- data/lib/wurk/flow_set.rb +53 -0
- data/lib/wurk/health.rb +8 -7
- data/lib/wurk/heartbeat.rb +21 -3
- data/lib/wurk/job/options.rb +11 -1
- data/lib/wurk/job.rb +19 -0
- data/lib/wurk/job_logger.rb +6 -2
- data/lib/wurk/job_retry.rb +6 -1
- data/lib/wurk/job_util.rb +110 -9
- data/lib/wurk/keys.rb +125 -0
- data/lib/wurk/launcher.rb +1 -1
- data/lib/wurk/leader.rb +1 -1
- data/lib/wurk/limiter/bucket.rb +1 -1
- data/lib/wurk/limiter/points.rb +1 -1
- data/lib/wurk/lua/debounce.lua +75 -0
- data/lib/wurk/lua/fetch_slot.lua +83 -0
- data/lib/wurk/lua/flow_abandon.lua +65 -0
- data/lib/wurk/lua/flow_advance.lua +177 -0
- data/lib/wurk/lua/flow_create.lua +141 -0
- data/lib/wurk/lua/flow_fail.lua +52 -0
- data/lib/wurk/lua/limiter_bucket_acquire.lua +26 -0
- data/lib/wurk/lua/limiter_concurrent_acquire.lua +33 -0
- data/lib/wurk/lua/limiter_concurrent_release.lua +7 -0
- data/lib/wurk/lua/limiter_leaky_acquire.lua +31 -0
- data/lib/wurk/lua/limiter_list_sweep.lua +30 -0
- data/lib/wurk/lua/limiter_points_acquire.lua +34 -0
- data/lib/wurk/lua/limiter_points_refund.lua +18 -0
- data/lib/wurk/lua/limiter_register.lua +27 -0
- data/lib/wurk/lua/limiter_window_acquire.lua +35 -0
- data/lib/wurk/lua/limiter_window_status.lua +22 -0
- data/lib/wurk/lua/loader.rb +23 -0
- data/lib/wurk/lua/queue_slot.lua +83 -0
- data/lib/wurk/lua/refresh_slots.lua +38 -0
- data/lib/wurk/lua/status_write.lua +29 -0
- data/lib/wurk/lua/throttle_slot.lua +71 -0
- data/lib/wurk/lua.rb +3 -3
- data/lib/wurk/manager.rb +12 -0
- data/lib/wurk/metrics/history.rb +34 -2
- data/lib/wurk/metrics/query.rb +1 -1
- data/lib/wurk/metrics/statsd.rb +14 -0
- data/lib/wurk/middleware/expiry.rb +71 -10
- data/lib/wurk/middleware/status.rb +274 -0
- data/lib/wurk/middleware/timeout.rb +137 -0
- data/lib/wurk/pid_cache.rb +43 -0
- data/lib/wurk/processor.rb +66 -22
- data/lib/wurk/profiler.rb +5 -3
- data/lib/wurk/queue_slot.rb +285 -0
- data/lib/wurk/rails.rb +3 -3
- data/lib/wurk/redis_pool.rb +5 -2
- data/lib/wurk/status/progress.rb +103 -0
- data/lib/wurk/status/record.rb +81 -0
- data/lib/wurk/status.rb +155 -0
- data/lib/wurk/telemetry/client_middleware.rb +59 -0
- data/lib/wurk/telemetry/server_middleware.rb +152 -0
- data/lib/wurk/telemetry.rb +137 -0
- data/lib/wurk/throttle.rb +142 -0
- data/lib/wurk/unique.rb +10 -2
- data/lib/wurk/version.rb +1 -1
- data/lib/wurk/watchdog.rb +188 -0
- data/lib/wurk/web/config.rb +77 -10
- data/lib/wurk/web/extension.rb +12 -4
- data/lib/wurk/web/locale_negotiator.rb +67 -0
- data/lib/wurk/web.rb +1 -0
- data/lib/wurk/worker.rb +39 -4
- data/lib/wurk.rb +42 -1
- data/vendor/assets/dashboard/assets/ArgsValue-D-x_ifLY.js +1 -0
- data/vendor/assets/dashboard/assets/BatchDetail-C39NJuew.js +1 -0
- data/vendor/assets/dashboard/assets/Batches-CSwo7Asa.js +1 -0
- data/vendor/assets/dashboard/assets/Busy-BOFMu-sq.js +1 -0
- data/vendor/assets/dashboard/assets/Cron-Dy8RQzDI.js +1 -0
- data/vendor/assets/dashboard/assets/Dashboard-BuTHI-O1.js +1 -0
- data/vendor/assets/dashboard/assets/Dead-B9KRvQ0N.js +1 -0
- data/vendor/assets/dashboard/assets/Extension-BnBVHfux.js +1 -0
- data/vendor/assets/dashboard/assets/FilterBox-DC24zite.js +1 -0
- data/vendor/assets/dashboard/assets/FlowDetail-DyLuzUvt.js +1 -0
- data/vendor/assets/dashboard/assets/FlowState-DAPKUahm.js +1 -0
- data/vendor/assets/dashboard/assets/Flows-Fr3rjZM_.js +1 -0
- data/vendor/assets/dashboard/assets/JobDetailModal-N6kiJXq3.js +2 -0
- data/vendor/assets/dashboard/assets/Limiters-kbFA7uS1.js +1 -0
- data/vendor/assets/dashboard/assets/Metrics-Dj2uoZ3o.js +1 -0
- data/vendor/assets/dashboard/assets/PageHeader-B_F94azl.js +1 -0
- data/vendor/assets/dashboard/assets/Profiles-D_DjEezN.js +1 -0
- data/vendor/assets/dashboard/assets/Queues-CO4V9hAz.js +1 -0
- data/vendor/assets/dashboard/assets/Retries-DCWnzeLa.js +1 -0
- data/vendor/assets/dashboard/assets/Scheduled-BebDUjLU.js +1 -0
- data/vendor/assets/dashboard/assets/Search-Cvr5fy4Y.js +1 -0
- data/vendor/assets/dashboard/assets/Skeleton-Bu3Ke6rV.js +1 -0
- data/vendor/assets/dashboard/assets/charts-BCs9bQKz.js +1 -0
- data/vendor/assets/dashboard/assets/index-BIwyOC5Q.js +141 -0
- data/vendor/assets/dashboard/assets/index-DBQN6Jk8.css +1 -0
- data/vendor/assets/dashboard/assets/useResetPageOnEmpty-Bzh-BJyL.js +1 -0
- data/vendor/assets/dashboard/assets/useSort-COA3fVJ5.js +1 -0
- data/vendor/assets/dashboard/assets/utils-BIrvZ1hi.js +1 -0
- data/vendor/assets/dashboard/index.html +42 -11
- data/vendor/assets/dashboard/wurk-manifest.json +2 -2
- metadata +102 -33
- data/vendor/assets/dashboard/assets/ArgsValue-CcR2ya6e.js +0 -1
- data/vendor/assets/dashboard/assets/BatchDetail-CUXJUQ3Q.js +0 -1
- data/vendor/assets/dashboard/assets/Batches-Cxan6Ngw.js +0 -1
- data/vendor/assets/dashboard/assets/Busy-DC5EGM0g.js +0 -1
- data/vendor/assets/dashboard/assets/Cron-Dlt8tXJA.js +0 -1
- data/vendor/assets/dashboard/assets/Dashboard-DNLu_WCg.js +0 -1
- data/vendor/assets/dashboard/assets/Dead-dZ7VGlKS.js +0 -1
- data/vendor/assets/dashboard/assets/Extension-DaFpEIJf.js +0 -1
- data/vendor/assets/dashboard/assets/FilterBox-CO3aYWIq.js +0 -1
- data/vendor/assets/dashboard/assets/JobDetailModal-DSWbT6G0.js +0 -2
- data/vendor/assets/dashboard/assets/Limiters-Cb4PKXNR.js +0 -1
- data/vendor/assets/dashboard/assets/Metrics-CCGzgCsT.js +0 -1
- data/vendor/assets/dashboard/assets/Modal-B86q6ruL.js +0 -1
- data/vendor/assets/dashboard/assets/PageHeader-fPrCcp_-.js +0 -1
- data/vendor/assets/dashboard/assets/Profiles-BnS82nR_.js +0 -1
- data/vendor/assets/dashboard/assets/Queues-CIyPevOy.js +0 -1
- data/vendor/assets/dashboard/assets/Retries-DopwXkXl.js +0 -1
- data/vendor/assets/dashboard/assets/Scheduled-1-Z7i1zE.js +0 -1
- data/vendor/assets/dashboard/assets/Search-ByA6eTma.js +0 -1
- data/vendor/assets/dashboard/assets/Skeleton-bC7HfQ9r.js +0 -1
- data/vendor/assets/dashboard/assets/charts-CLLzJ7vK.js +0 -1
- data/vendor/assets/dashboard/assets/index-B1N8hQUh.js +0 -141
- data/vendor/assets/dashboard/assets/index-BdiUEDXX.css +0 -1
- data/vendor/assets/dashboard/assets/useResetPageOnEmpty-DpBjkf6_.js +0 -1
- data/vendor/assets/dashboard/assets/useSort-DvpwuNQE.js +0 -1
- data/vendor/assets/dashboard/assets/utils-DDJC7tJV.js +0 -1
data/lib/wurk/client/buffered.rb
CHANGED
|
@@ -531,7 +531,7 @@ module Wurk
|
|
|
531
531
|
# next push — this is a Wurk extension for issue #19's "Background
|
|
532
532
|
# drain thread flushes on reconnect" so producer-stopped-mid-outage
|
|
533
533
|
# buffers don't sit idle until next push.
|
|
534
|
-
def reliable_push_drainer(interval: Buffered::Drainer::DEFAULT_INTERVAL)
|
|
534
|
+
def reliable_push_drainer(interval: Buffered::Drainer::DEFAULT_INTERVAL) # rubocop:disable Naming/PredicateMethod
|
|
535
535
|
Buffered.install!
|
|
536
536
|
Buffered.start_drainer!(interval: interval)
|
|
537
537
|
true
|
data/lib/wurk/client.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative 'iterable_job'
|
|
4
4
|
require_relative 'job_util'
|
|
5
5
|
require_relative 'lua'
|
|
6
|
+
require_relative 'status'
|
|
6
7
|
|
|
7
8
|
module Wurk
|
|
8
9
|
# Enqueue interface. Pipelined LPUSH / ZADD writes against the canonical
|
|
@@ -17,7 +18,7 @@ module Wurk
|
|
|
17
18
|
# Wurk::Client.new.push_bulk("class" => "MyJob", "args" => [[1], [2], [3]])
|
|
18
19
|
#
|
|
19
20
|
# Spec: docs/target/sidekiq-free.md §7.
|
|
20
|
-
class Client
|
|
21
|
+
class Client # rubocop:disable Metrics/ClassLength
|
|
21
22
|
include JobUtil
|
|
22
23
|
|
|
23
24
|
# Sidekiq mirrors these exactly. Tests against the upstream parity suite
|
|
@@ -86,9 +87,10 @@ module Wurk
|
|
|
86
87
|
validate_bulk_shape!(items, args)
|
|
87
88
|
return [] if args.empty?
|
|
88
89
|
|
|
90
|
+
base = bulk_base(items)
|
|
91
|
+
reject_bulk_collapse!(base)
|
|
89
92
|
at_values = expand_at(items, args.size)
|
|
90
93
|
batch_sz = items['batch_size'] || items[:batch_size] || (at_values ? SCHEDULED_BATCH_SIZE : DEFAULT_BATCH_SIZE)
|
|
91
|
-
base = bulk_base(items)
|
|
92
94
|
flush_bulk(args, at_values, base, batch_sz)
|
|
93
95
|
end
|
|
94
96
|
|
|
@@ -198,6 +200,19 @@ module Wurk
|
|
|
198
200
|
base
|
|
199
201
|
end
|
|
200
202
|
|
|
203
|
+
# Up front, before the first slice is written — a policy discovered halfway
|
|
204
|
+
# through would leave the slices already flushed in Redis. Two lookups and no
|
|
205
|
+
# allocation on the untouched path.
|
|
206
|
+
#
|
|
207
|
+
# `class_defaults_for` is the same resolution #normalize_item does per item,
|
|
208
|
+
# so this sees a policy in exactly the cases the middleware would: a bulk
|
|
209
|
+
# pushed by class *name* never merges class options at all, and so never
|
|
210
|
+
# carries one. Spelled out in {Wurk::Collapse.reject_bulk!}.
|
|
211
|
+
def reject_bulk_collapse!(base)
|
|
212
|
+
key = Wurk::Collapse::OPTION
|
|
213
|
+
Wurk::Collapse.reject_bulk!(base['class'], base[key] || class_defaults_for(base['class'])[key])
|
|
214
|
+
end
|
|
215
|
+
|
|
201
216
|
def flush_bulk(args, at_values, base, batch_size)
|
|
202
217
|
jids = []
|
|
203
218
|
args.each_slice(batch_size).with_index do |slice, slice_index|
|
|
@@ -324,7 +339,7 @@ module Wurk
|
|
|
324
339
|
|
|
325
340
|
def push_scheduled(conn, payloads)
|
|
326
341
|
args = payloads.flat_map do |hash|
|
|
327
|
-
[hash['at'].to_s,
|
|
342
|
+
[hash['at'].to_s, JobUtil.scheduled_member(hash)]
|
|
328
343
|
end
|
|
329
344
|
conn.call('ZADD', 'schedule', *args)
|
|
330
345
|
end
|
|
@@ -425,10 +440,29 @@ module Wurk
|
|
|
425
440
|
conn.pipelined do |pipe|
|
|
426
441
|
pipe.call('SADD', 'queues', queue)
|
|
427
442
|
pipe.call('LPUSH', "queue:#{queue}", *serialized)
|
|
443
|
+
track_enqueued(pipe, jobs, now)
|
|
428
444
|
end
|
|
429
445
|
mark_delivered(jobs)
|
|
430
446
|
end
|
|
431
447
|
|
|
448
|
+
# Ride the pipeline that is already open for the queue write, so a tracked
|
|
449
|
+
# push costs the same round trip an untracked one does. An app that never
|
|
450
|
+
# opts in pays one Hash lookup per payload here and appends nothing: the
|
|
451
|
+
# plain push stays exactly the two commands (SADD + LPUSH) it has always
|
|
452
|
+
# been, which is what `rake bench:command_count_tracked_off` asserts.
|
|
453
|
+
#
|
|
454
|
+
# The TTL is resolved once per group, and only when something is actually
|
|
455
|
+
# tracked — an untracked push never reads the config at all.
|
|
456
|
+
def track_enqueued(pipe, jobs, now)
|
|
457
|
+
ttl = nil
|
|
458
|
+
jobs.each do |job|
|
|
459
|
+
next unless job['track']
|
|
460
|
+
|
|
461
|
+
ttl ||= Wurk::Status.default_ttl(@config)
|
|
462
|
+
Wurk::Status.enqueued(pipe, job, now, ttl: ttl)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
432
466
|
# Batched jobs route through BATCH_PUSH: increments b-<bid> total+pending,
|
|
433
467
|
# SADDs jid into the live set, registers the queue, LPUSHes the payload —
|
|
434
468
|
# all atomically. The Lua binds per-job KEYS, so grouping N jobs into one
|
|
@@ -452,6 +486,11 @@ module Wurk
|
|
|
452
486
|
argv: [j['queue'], j['jid'], Wurk.dump_json(j), j['bid'], Wurk::Batch::DEFAULT_EXPIRY_SECONDS]
|
|
453
487
|
)
|
|
454
488
|
end
|
|
489
|
+
# Same pipeline, so a batched tracked push keeps its one round trip too.
|
|
490
|
+
# This slice is the one that gets replayed on NOSCRIPT (see
|
|
491
|
+
# #eval_batched_slice); HSET + EXPIRE are idempotent, so a replay writes
|
|
492
|
+
# the same row twice rather than double-counting anything.
|
|
493
|
+
track_enqueued(conn, payloads, now)
|
|
455
494
|
end
|
|
456
495
|
|
|
457
496
|
# Scheduled batched jobs route through BATCH_SCHEDULE: the SADD-guarded
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'debounce'
|
|
4
|
+
require_relative 'job_util'
|
|
5
|
+
require_relative 'middleware'
|
|
6
|
+
require_relative 'throttle'
|
|
7
|
+
require_relative 'unique'
|
|
8
|
+
|
|
9
|
+
module Wurk
|
|
10
|
+
# The enqueue door for Wurk's two collapse policies. {Wurk::Debounce} and
|
|
11
|
+
# {Wurk::Throttle} are the mechanisms; this is the single option a worker
|
|
12
|
+
# class writes to reach them, and the one place the company they cannot keep
|
|
13
|
+
# is refused.
|
|
14
|
+
#
|
|
15
|
+
# sidekiq_options collapse: { policy: :debounce, wait: 5, max_wait: 60 }
|
|
16
|
+
# sidekiq_options collapse: { policy: :throttle, slot: 60 }
|
|
17
|
+
#
|
|
18
|
+
# ## Why `collapse:` rather than `unique:`
|
|
19
|
+
#
|
|
20
|
+
# The slice plan sketched `unique: { policy: … }`, on the reasonable grounds
|
|
21
|
+
# that all three policies share one identity digest and one narrowing hook
|
|
22
|
+
# (`sidekiq_unique_context`). That name is taken. `sidekiq-unique-jobs` v8.1.0
|
|
23
|
+
# — the gem this feature replaces, and one Wurk's ecosystem suite runs against
|
|
24
|
+
# — still recognizes `sidekiq_options unique:` as the deprecated spelling of
|
|
25
|
+
# its own `lock:` (`SidekiqUniqueJobs::Lock::Validator::DEPRECATED_KEYS` maps
|
|
26
|
+
# `:unique => :lock`). Claiming the key would make this validator reject a
|
|
27
|
+
# host's working pre-v6 declaration, which is the drop-in contract breaking on
|
|
28
|
+
# exactly the gem it is meant to make unnecessary. `collapse` is unclaimed
|
|
29
|
+
# there, and says what both policies do.
|
|
30
|
+
#
|
|
31
|
+
# ## One policy per class
|
|
32
|
+
#
|
|
33
|
+
# `collapse:` and `unique_for:` answer the same question differently — collapse
|
|
34
|
+
# a burst, or reject the duplicate — and they do not compose: a lock drops the
|
|
35
|
+
# very re-enqueue debounce needs in order to extend a burst, and which of the
|
|
36
|
+
# two won would come down to client-chain registration order. A class declares
|
|
37
|
+
# one; declaring both is refused where it was written.
|
|
38
|
+
#
|
|
39
|
+
# ## What a collapsed enqueue returns
|
|
40
|
+
#
|
|
41
|
+
# nil, from both policies. For a dropped throttle that is {Wurk::Throttle}'s
|
|
42
|
+
# documented answer and {Wurk::Unique}'s long-standing one. For a debounce it
|
|
43
|
+
# is the less obvious half: this push's payload really is in `schedule`, under
|
|
44
|
+
# this push's jid — but only until the next enqueue of the same identity
|
|
45
|
+
# replaces it, so a jid handed back here invites a caller to poll, or cancel, a
|
|
46
|
+
# member a sibling push has already ZREM'd. {Wurk::Debounce.schedule} returns
|
|
47
|
+
# its {Wurk::Debounce::Outcome} for code that needs the detail; the enqueue
|
|
48
|
+
# door does not.
|
|
49
|
+
module Collapse
|
|
50
|
+
# The job-hash key, and the `sidekiq_options` key it is written as.
|
|
51
|
+
OPTION = 'collapse'
|
|
52
|
+
|
|
53
|
+
# Everything refused here is a configuration mistake, and every door this
|
|
54
|
+
# guards already answers a bad option with an ArgumentError
|
|
55
|
+
# (`JobUtil.validate_track!`, `.validate_bounds!`). Subclassing keeps a host
|
|
56
|
+
# that rescues ArgumentError around a declaration working, while leaving the
|
|
57
|
+
# conflicts nameable on their own.
|
|
58
|
+
class ConfigurationError < ArgumentError; end
|
|
59
|
+
|
|
60
|
+
# A parsed declaration: the policy's name, plus the keyword arguments its
|
|
61
|
+
# module takes, ready to splat into {Wurk::Debounce.schedule} or
|
|
62
|
+
# {Wurk::Throttle.admit}. Parsing straight into the callee's own keywords is
|
|
63
|
+
# what stops this file from growing a second opinion about what `wait` means.
|
|
64
|
+
Policy = Struct.new(:name, :params)
|
|
65
|
+
|
|
66
|
+
# Per-policy parameter rules. `whole` marks a slot boundary, which is a
|
|
67
|
+
# wall-clock landmark every producer has to land on identically and so
|
|
68
|
+
# cannot be a fraction of a second ({Wurk::Throttle} refuses one too).
|
|
69
|
+
#
|
|
70
|
+
# Values are checked here and not left to the two modules, which raise at
|
|
71
|
+
# enqueue: a `slot: 1.5` that only fails the first time a rarely-exercised
|
|
72
|
+
# worker is pushed has already shipped.
|
|
73
|
+
PARAMS = {
|
|
74
|
+
debounce: { required: %i[wait].freeze, optional: %i[max_wait].freeze, whole: false },
|
|
75
|
+
throttle: { required: %i[slot].freeze, optional: [].freeze, whole: true }
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
78
|
+
class << self
|
|
79
|
+
# The collapse policy a set of job options declares, or nil for none.
|
|
80
|
+
#
|
|
81
|
+
# Parsing and validating are one pass, shared by every door: both
|
|
82
|
+
# `sidekiq_options` copies (worker and ActiveJob), which keep only the
|
|
83
|
+
# raising, and {ClientMiddleware}, which acts on the result. One definition
|
|
84
|
+
# means "valid where it was written" and "usable where it is read" cannot
|
|
85
|
+
# drift apart — the same reason {Wurk::JobUtil.positive_seconds?} is shared
|
|
86
|
+
# rather than reimplemented per reader.
|
|
87
|
+
#
|
|
88
|
+
# Strict on every axis, deliberately: `slot:` written on a debounce is a
|
|
89
|
+
# typo for `wait:`, and a silently ignored one leaves a worker collapsing
|
|
90
|
+
# on a default nobody chose.
|
|
91
|
+
#
|
|
92
|
+
# @param options [Hash] string-keyed job options or a job payload
|
|
93
|
+
# @return [Policy, nil]
|
|
94
|
+
# @raise [ConfigurationError]
|
|
95
|
+
def policy_for(options)
|
|
96
|
+
declared = options[OPTION]
|
|
97
|
+
return nil if declared.nil?
|
|
98
|
+
|
|
99
|
+
reject_unique!(options, declared)
|
|
100
|
+
params = symbolized(declared)
|
|
101
|
+
name = policy_name!(params.delete(:policy), declared)
|
|
102
|
+
check_keys!(name, params, declared)
|
|
103
|
+
check_seconds!(name, params, declared)
|
|
104
|
+
reject_short_cap!(params, declared) if name == :debounce
|
|
105
|
+
Policy.new(name, params)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# `push_bulk` exists to amortize one round trip across many jobs: a
|
|
109
|
+
# same-queue bulk of 1,000 is still the one SADD + variadic LPUSH pipeline
|
|
110
|
+
# a single push is. A collapse policy is a decision per job — an EVALSHA
|
|
111
|
+
# whose KEYS are that job's own identity — so it cannot ride that pipeline,
|
|
112
|
+
# and applying it per item turns the one round trip into N. Measured here,
|
|
113
|
+
# 1,000 jobs onto a loopback Redis, `INFO commandstats` either side:
|
|
114
|
+
#
|
|
115
|
+
# push_bulk, no policy ~14 ms 2 commands, 1 round trip
|
|
116
|
+
# per-item collapse ~320 ms 6,000 commands, 1,000 round trips
|
|
117
|
+
#
|
|
118
|
+
# ~22x wall clock and three orders of magnitude of round trips, and both
|
|
119
|
+
# grow with the batch. That is a structural cost, not a constant worth
|
|
120
|
+
# tuning away, which is what settles step 6 of the slice plan against
|
|
121
|
+
# applying policies per item.
|
|
122
|
+
#
|
|
123
|
+
# It is also meaningless in the shape bulk is used: `perform_bulk` pushes
|
|
124
|
+
# one class with N distinct argument lists, and distinct arguments are
|
|
125
|
+
# distinct identities, so nothing collapses with anything. Every jid in the
|
|
126
|
+
# returned array would come back nil, which in `push_bulk`'s contract means
|
|
127
|
+
# "middleware halted this one" — indistinguishable from a debounce that did
|
|
128
|
+
# write. Raising is the only outcome that is neither a silent regression
|
|
129
|
+
# nor a lie about what happened.
|
|
130
|
+
#
|
|
131
|
+
# @raise [ConfigurationError] when the class or the call declares a policy
|
|
132
|
+
def reject_bulk!(klass, declared)
|
|
133
|
+
return if declared.nil?
|
|
134
|
+
|
|
135
|
+
raise ConfigurationError,
|
|
136
|
+
"#{klass} declares `collapse: #{declared.inspect}` and cannot be enqueued with push_bulk: " \
|
|
137
|
+
'a collapse policy decides per job, which would cost one Redis round trip per item where ' \
|
|
138
|
+
'bulk enqueue costs one for the whole batch, and every returned jid would be nil. ' \
|
|
139
|
+
'Push these jobs individually, or drop the policy on this worker.'
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
# On whatever options hash the caller passed, so both the class-definition
|
|
145
|
+
# door (the merged options, where a subclass adding `collapse:` to a
|
|
146
|
+
# parent's `unique_for:` becomes visible) and the payload door (a per-call
|
|
147
|
+
# `set(collapse:)` over a class-level `unique_for:`) are covered by the one
|
|
148
|
+
# check. `unique_for: false` is the documented opt-out and reads as absent,
|
|
149
|
+
# which is what {Wurk::Unique.coerce_ttl} already decides.
|
|
150
|
+
def reject_unique!(options, declared)
|
|
151
|
+
return unless Wurk::Unique.coerce_ttl(options['unique_for'])
|
|
152
|
+
|
|
153
|
+
raise ConfigurationError,
|
|
154
|
+
"#{options['class']} sets both `unique_for: #{options['unique_for'].inspect}` and " \
|
|
155
|
+
"`collapse: #{declared.inspect}`. They are two answers to the same question and do not " \
|
|
156
|
+
'compose: the lock drops the re-enqueue a debounce needs to extend its burst, and which ' \
|
|
157
|
+
'one wins would depend on client middleware registration order. Keep one.'
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def symbolized(declared)
|
|
161
|
+
unless declared.is_a?(::Hash)
|
|
162
|
+
raise ConfigurationError,
|
|
163
|
+
"Job 'collapse' must be a Hash naming a policy, e.g. " \
|
|
164
|
+
"`collapse: { policy: :debounce, wait: 5 }`: #{declared.inspect}"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
declared.to_h { |key, value| [key.to_sym, value] }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Symbol or String, because a payload that has crossed Redis carries the
|
|
171
|
+
# JSON spelling of whatever the class declared.
|
|
172
|
+
def policy_name!(raw, declared)
|
|
173
|
+
name = raw.to_sym if raw.respond_to?(:to_sym)
|
|
174
|
+
return name if PARAMS.key?(name)
|
|
175
|
+
|
|
176
|
+
raise ConfigurationError,
|
|
177
|
+
"unknown collapse policy #{raw.inspect} in #{declared.inspect}; " \
|
|
178
|
+
"valid policies are #{PARAMS.keys.map(&:inspect).join(' and ')}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Missing and unknown are reported together: `{ policy: :debounce, slot: 5 }`
|
|
182
|
+
# is one mistake — `slot:` written where `wait:` belongs — and naming only
|
|
183
|
+
# half of it sends the reader round twice.
|
|
184
|
+
def check_keys!(name, params, declared)
|
|
185
|
+
spec = PARAMS.fetch(name)
|
|
186
|
+
faults = key_faults(spec, params.keys)
|
|
187
|
+
return if faults.empty?
|
|
188
|
+
|
|
189
|
+
raise ConfigurationError,
|
|
190
|
+
"collapse policy #{name.inspect} in #{declared.inspect} #{faults.join(' and ')}; " \
|
|
191
|
+
"it takes #{list(spec[:required] + spec[:optional])}"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def key_faults(spec, given)
|
|
195
|
+
missing = spec[:required] - given
|
|
196
|
+
unknown = given - spec[:required] - spec[:optional]
|
|
197
|
+
faults = []
|
|
198
|
+
faults << "requires #{list(missing)}" unless missing.empty?
|
|
199
|
+
faults << "does not take #{list(unknown)}" unless unknown.empty?
|
|
200
|
+
faults
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def list(keys) = keys.map(&:inspect).join(', ')
|
|
204
|
+
|
|
205
|
+
def check_seconds!(name, params, declared)
|
|
206
|
+
whole = PARAMS.fetch(name)[:whole]
|
|
207
|
+
params.each do |key, value|
|
|
208
|
+
next if usable_seconds?(value, whole)
|
|
209
|
+
|
|
210
|
+
raise ConfigurationError,
|
|
211
|
+
"collapse #{name} #{key} must be a positive #{'whole ' if whole}number of " \
|
|
212
|
+
"seconds: #{declared.inspect}"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def usable_seconds?(value, whole)
|
|
217
|
+
return false unless Wurk::JobUtil.positive_seconds?(value)
|
|
218
|
+
|
|
219
|
+
!whole || (value.to_f % 1).zero?
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# A cap below the quiet period fires every burst at `first + max_wait`
|
|
223
|
+
# regardless of traffic, which is a throttle wearing a debounce's name.
|
|
224
|
+
# {Wurk::Debounce} refuses it too; refusing it here is what makes the
|
|
225
|
+
# refusal land at the class rather than at the first push.
|
|
226
|
+
def reject_short_cap!(params, declared)
|
|
227
|
+
cap = params[:max_wait]
|
|
228
|
+
return if cap.nil? || cap.to_f >= params[:wait].to_f
|
|
229
|
+
|
|
230
|
+
raise ConfigurationError,
|
|
231
|
+
"collapse debounce max_wait (#{cap}) must be at least wait (#{params[:wait]}): #{declared.inspect}"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Client middleware — the collapse decision, taken once every other
|
|
236
|
+
# middleware has had the payload.
|
|
237
|
+
#
|
|
238
|
+
# Prepended, and it decides *after* its yield. Both halves are load-bearing.
|
|
239
|
+
# Nothing in the client chain writes to Redis — {Wurk::Client#push} does that
|
|
240
|
+
# only once `invoke_chain` has handed a payload back — so a middleware is
|
|
241
|
+
# free to run the rest of the chain and then halt the push by returning nil.
|
|
242
|
+
# Deciding on the way out means the payload {Wurk::Debounce} stores in
|
|
243
|
+
# `schedule` carries everything the enrichers stamped on it (`bid`, `locale`,
|
|
244
|
+
# `cattr`, `traceparent`), so a debounced job really is the scheduled job a
|
|
245
|
+
# `perform_in` would have written rather than a stripped-down cousin. Sitting
|
|
246
|
+
# outermost is what keeps that true for the middleware a host installs later
|
|
247
|
+
# at boot: `Unique.enable!`, `Encryption.enable!` and `Telemetry.install!`
|
|
248
|
+
# all `add`, which appends *inside* this one.
|
|
249
|
+
class ClientMiddleware
|
|
250
|
+
include Wurk::Middleware::ClientMiddleware
|
|
251
|
+
# For #verify_json alone. Client walks the args of what the chain hands
|
|
252
|
+
# back (client.rb:156) — after this middleware has already halted it — so
|
|
253
|
+
# the one payload Client never verifies is the one written from in here.
|
|
254
|
+
include Wurk::JobUtil
|
|
255
|
+
|
|
256
|
+
def call(_worker, job, _queue, redis_pool)
|
|
257
|
+
# One Hash lookup for a job that declares nothing, which is every job in
|
|
258
|
+
# an app that uses neither policy.
|
|
259
|
+
return yield if job[Wurk::Collapse::OPTION].nil?
|
|
260
|
+
|
|
261
|
+
policy = Wurk::Collapse.policy_for(job)
|
|
262
|
+
payload = yield
|
|
263
|
+
return nil if payload.nil?
|
|
264
|
+
|
|
265
|
+
decide(policy, payload, redis_pool)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
private
|
|
269
|
+
|
|
270
|
+
# The policy comes off the payload the decision hands on, and that is not
|
|
271
|
+
# tidying. `Scheduled::Enq#push_promoted` re-pushes a promoted job through
|
|
272
|
+
# {Wurk::Client#push}, which re-runs this chain: a `schedule` member still
|
|
273
|
+
# carrying `collapse` is debounced again on every promotion and so never
|
|
274
|
+
# runs at all, and an admitted throttled job's retry — promoted the same
|
|
275
|
+
# way, minutes later, into a slot a fresh sibling has since taken — is
|
|
276
|
+
# judged a second time and silently dropped. A collapse decision belongs
|
|
277
|
+
# to the enqueue door and is taken exactly once, on the push that made it.
|
|
278
|
+
#
|
|
279
|
+
# Identity is unaffected: {Wurk::Unique.digest_for} reads
|
|
280
|
+
# `[class, queue, args]`, so the next push of the same job still collapses
|
|
281
|
+
# into this one's key.
|
|
282
|
+
def decide(policy, job, pool)
|
|
283
|
+
reject_incompatible!(job, policy)
|
|
284
|
+
carried = job.except(Wurk::Collapse::OPTION)
|
|
285
|
+
return collapse(carried, policy, pool) if policy.name == :debounce
|
|
286
|
+
|
|
287
|
+
admit(carried, policy, pool)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Returns nil unconditionally: the job is already in `schedule`, and the
|
|
291
|
+
# jid it is under is not the caller's to act on (see the module docs).
|
|
292
|
+
def collapse(job, policy, pool)
|
|
293
|
+
verify_json(job)
|
|
294
|
+
outcome = Wurk::Debounce.schedule(job, pool: pool, **policy.params)
|
|
295
|
+
log_collapsed(job, outcome)
|
|
296
|
+
nil
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# The payload on admission, so the push continues exactly as it would with
|
|
300
|
+
# no policy at all; nil on a drop, halting it.
|
|
301
|
+
def admit(job, policy, pool)
|
|
302
|
+
outcome = Wurk::Throttle.admit(job, pool: pool, **policy.params)
|
|
303
|
+
return job if outcome.admitted?
|
|
304
|
+
|
|
305
|
+
log_dropped(job, outcome)
|
|
306
|
+
nil
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def reject_incompatible!(job, policy)
|
|
310
|
+
reject_encrypted!(job)
|
|
311
|
+
return unless policy.name == :debounce
|
|
312
|
+
|
|
313
|
+
reject_batched!(job)
|
|
314
|
+
reject_scheduled!(job)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Encryption rewrites the last argument with a fresh IV on every push, so
|
|
318
|
+
# the identity digest — {Wurk::Unique}'s, shared by all three policies —
|
|
319
|
+
# never repeats and nothing ever collapses. Same failure and same answer as
|
|
320
|
+
# `Unique::ClientMiddleware#reject_encrypted!`: fail the push, because
|
|
321
|
+
# silence here is a policy that is configured, running, and doing nothing.
|
|
322
|
+
def reject_encrypted!(job)
|
|
323
|
+
return unless job['encrypt']
|
|
324
|
+
return unless defined?(Wurk::Encryption) && Wurk::Encryption.enabled?
|
|
325
|
+
|
|
326
|
+
raise Wurk::Collapse::ConfigurationError,
|
|
327
|
+
"#{job['class']} sets both `collapse:` and `encrypt: true`, which cannot work: " \
|
|
328
|
+
'encryption rewrites the last argument with a fresh IV on every push, so the ' \
|
|
329
|
+
'identity digest never repeats and no two jobs ever collapse. Drop one of the two.'
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# A debounced job is written straight into `schedule` by the Lua script,
|
|
333
|
+
# which is the write BATCH_SCHEDULE would otherwise have made — so the
|
|
334
|
+
# batch never counts a job that will nevertheless run and decrement its
|
|
335
|
+
# `pending` on the way out, and the batch completes early or goes negative.
|
|
336
|
+
# A throttled one is safe either way: admitted, it takes the ordinary
|
|
337
|
+
# batched path; dropped, it is simply not in the batch, exactly as a
|
|
338
|
+
# duplicate dropped by {Wurk::Unique} is not.
|
|
339
|
+
def reject_batched!(job)
|
|
340
|
+
return unless job['bid']
|
|
341
|
+
|
|
342
|
+
raise Wurk::Collapse::ConfigurationError,
|
|
343
|
+
"#{job['class']} declares a debounce policy and cannot be enqueued inside a batch: " \
|
|
344
|
+
'the collapsed job is written straight to `schedule`, so the batch would never count ' \
|
|
345
|
+
'a job that still decrements its pending counter when it runs.'
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# `at` is not the caller's to set on a debounced job: the policy owns the
|
|
349
|
+
# fire time, and JobUtil.scheduled_member strips `at` off the stored member,
|
|
350
|
+
# so a `perform_in(1.hour)` here would quietly fire in `wait` seconds.
|
|
351
|
+
def reject_scheduled!(job)
|
|
352
|
+
return unless job['at']
|
|
353
|
+
|
|
354
|
+
raise Wurk::Collapse::ConfigurationError,
|
|
355
|
+
"#{job['class']} declares a debounce policy, which owns the fire time, so it cannot " \
|
|
356
|
+
'also be scheduled with perform_in/perform_at/set(wait:). Use `wait:` and `max_wait:`.'
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Info, matching Unique's dropped-duplicate line: something the caller
|
|
360
|
+
# asked for did not happen, and the incumbent's jid is the fact a loser
|
|
361
|
+
# cannot recover for itself afterwards. Unguarded: Configuration#logger
|
|
362
|
+
# memoizes a default rather than ever handing back nil.
|
|
363
|
+
def log_dropped(job, outcome)
|
|
364
|
+
Wurk.logger.info do
|
|
365
|
+
"Wurk::Collapse: throttled #{job['class']} dropped " \
|
|
366
|
+
"(jid=#{job['jid']} blocked by jid=#{outcome.jid})"
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Debug, unlike the drop above. A burst is many pushes by construction and
|
|
371
|
+
# every one of them lands here, so info would be a log line per enqueue at
|
|
372
|
+
# exactly the rate this policy exists to absorb.
|
|
373
|
+
def log_collapsed(job, outcome)
|
|
374
|
+
return unless outcome.replaced?
|
|
375
|
+
|
|
376
|
+
Wurk.logger.debug do
|
|
377
|
+
"Wurk::Collapse: debounced #{job['class']} replaced the pending job " \
|
|
378
|
+
"(jid=#{job['jid']} fires at #{outcome.at})"
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'redis-client'
|
|
4
|
+
|
|
5
|
+
module Wurk
|
|
6
|
+
# Fast path for redis-client's command normalization.
|
|
7
|
+
#
|
|
8
|
+
# `RedisClient::CommandBuilder.generate` splices Hash arguments with a
|
|
9
|
+
# `flat_map` and then stringifies Symbols/Integers/Floats with a `map!` —
|
|
10
|
+
# two array allocations and two per-element type dispatches for a command
|
|
11
|
+
# whose arguments are already Strings. Every command Wurk sends per job is
|
|
12
|
+
# exactly that shape (LMOVE, LREM, DEL, LPUSH, SADD), and the fetch pipeline
|
|
13
|
+
# sends three of them. Measured ~4x faster on those commands (2.2µs → 0.6µs
|
|
14
|
+
# each) with `bench/command_builder.rb`.
|
|
15
|
+
#
|
|
16
|
+
# Anything else — a Hash to splice, a Symbol or a number to stringify,
|
|
17
|
+
# keyword arguments, an empty command — falls straight through to
|
|
18
|
+
# redis-client's own builder. This is a shortcut, never a second
|
|
19
|
+
# implementation of the semantics, so a host app calling
|
|
20
|
+
# `Sidekiq.redis { |c| c.call("HSET", key, hash) }` is unaffected.
|
|
21
|
+
#
|
|
22
|
+
# The array is duped rather than handed back: `call_v` and the pipelined
|
|
23
|
+
# forms pass the caller's own array through, and redis-client's builder
|
|
24
|
+
# always returns a fresh one that middleware is free to mutate in place.
|
|
25
|
+
module CommandBuilder
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def generate(args, kwargs = nil)
|
|
29
|
+
return args.dup if fast?(args, kwargs)
|
|
30
|
+
|
|
31
|
+
::RedisClient::CommandBuilder.generate(args, kwargs)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Split out, and public, because the two branches produce byte-identical
|
|
35
|
+
# output by construction — which leaves allocation count as the only
|
|
36
|
+
# runtime difference between "the shortcut fired" and "it silently didn't",
|
|
37
|
+
# and that is not something a test can assert safely in a process running
|
|
38
|
+
# other suites. So the decision itself is what the suite asserts.
|
|
39
|
+
#
|
|
40
|
+
# `kwargs` is nil only from `call_v` and the scan helpers. Every `call`,
|
|
41
|
+
# `blocking_call` and pipelined `call` arrives with `**kwargs` already
|
|
42
|
+
# splatted into a Hash — EMPTY when the caller passed no keywords — so a
|
|
43
|
+
# guard that only tested `.nil?` sent the entire hot path down the slow
|
|
44
|
+
# branch while every direct-call test still passed.
|
|
45
|
+
def fast?(args, kwargs)
|
|
46
|
+
(kwargs.nil? || kwargs.empty?) && !args.empty? && args.all?(String)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/wurk/compat.rb
CHANGED
|
@@ -120,6 +120,15 @@ module Sidekiq
|
|
|
120
120
|
Deploy = Wurk::Deploy
|
|
121
121
|
Embedded = Wurk::Embedded
|
|
122
122
|
Encryption = Wurk::Encryption
|
|
123
|
+
# A Wurk extra, not parity — aliased because nothing else claims the name.
|
|
124
|
+
# Checked before adding it, the way `Sidekiq::Cron` (#204) and
|
|
125
|
+
# `Sidekiq::Status` taught us to: upstream Sidekiq defines no `Flow` anywhere
|
|
126
|
+
# in `lib/` at the pinned parity SHA, RubyGems has no `sidekiq-flow` or
|
|
127
|
+
# `sidekiq_flow` gem, and the only `Sidekiq::Flow` in public Ruby is a
|
|
128
|
+
# `FlowIdMiddleware` nested under a gem's *own* `Sidekiq` module, which this
|
|
129
|
+
# constant cannot reach.
|
|
130
|
+
Flow = Wurk::Flow
|
|
131
|
+
FlowSet = Wurk::FlowSet
|
|
123
132
|
History = Wurk::History
|
|
124
133
|
IterableJob = Wurk::IterableJob
|
|
125
134
|
IterableJobQuery = Wurk::IterableJobQuery
|
|
@@ -152,6 +161,16 @@ module Sidekiq
|
|
|
152
161
|
Shutdown = Wurk::Shutdown
|
|
153
162
|
SortedEntry = Wurk::SortedEntry
|
|
154
163
|
Stats = Wurk::Stats
|
|
164
|
+
# No `Sidekiq::Status` alias on purpose, same reason as `Sidekiq::Cron`
|
|
165
|
+
# (#204): the sidekiq-status gem owns that namespace and opens it with
|
|
166
|
+
# `module Sidekiq::Status`, which resolves an existing constant rather than
|
|
167
|
+
# defining a fresh module — so the alias would make the gem reopen
|
|
168
|
+
# Wurk::Status and, loading second, silently replace `.get`/`.delete` with its
|
|
169
|
+
# own two-arg-shaped versions while nesting its Storage/Worker/middlewares
|
|
170
|
+
# inside ours. Upstream Sidekiq's only Status is `Sidekiq::Batch::Status`
|
|
171
|
+
# (sidekiq-pro.md §2.5), which *is* aliased; the top-level name is not a
|
|
172
|
+
# Sidekiq surface at all. Wurk's tracking is `Wurk::Status`, reached by name,
|
|
173
|
+
# and its `status:<jid>` rows are disjoint from the gem's `sidekiq:status:<jid>`.
|
|
155
174
|
Testing = Wurk::Testing
|
|
156
175
|
TransactionAwareClient = Wurk::TransactionAwareClient
|
|
157
176
|
Queues = Wurk::Queues
|
data/lib/wurk/component.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'socket'
|
|
4
4
|
require 'securerandom'
|
|
5
|
+
require_relative 'pid_cache'
|
|
5
6
|
|
|
6
7
|
module Wurk
|
|
7
8
|
# Shared mixin for runtime components (Launcher, Manager, Processor, Fetcher,
|
|
@@ -55,7 +56,7 @@ module Wurk
|
|
|
55
56
|
def self.tid
|
|
56
57
|
thread = Thread.current
|
|
57
58
|
memo = thread.thread_variable_get(:wurk_tid)
|
|
58
|
-
pid =
|
|
59
|
+
pid = PidCache.pid
|
|
59
60
|
return memo[1] if memo && memo[0] == pid
|
|
60
61
|
|
|
61
62
|
id = (thread.object_id ^ pid).to_s(36).freeze
|
|
@@ -67,16 +68,28 @@ module Wurk
|
|
|
67
68
|
Component.tid
|
|
68
69
|
end
|
|
69
70
|
|
|
70
|
-
def hostname
|
|
71
|
+
def self.hostname
|
|
71
72
|
ENV['DYNO'] || Socket.gethostname
|
|
72
73
|
end
|
|
73
74
|
|
|
75
|
+
# Class-level so a plain module with no Component instance to hand — the
|
|
76
|
+
# queue-slot holder token, which has to name this process the same way the
|
|
77
|
+
# `processes` set does — reaches the one definition instead of copying the
|
|
78
|
+
# format. The instance methods below are the same string.
|
|
79
|
+
def self.identity
|
|
80
|
+
"#{hostname}:#{::Process.pid}:#{PROCESS_NONCE}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def hostname
|
|
84
|
+
Component.hostname
|
|
85
|
+
end
|
|
86
|
+
|
|
74
87
|
def process_nonce
|
|
75
88
|
PROCESS_NONCE
|
|
76
89
|
end
|
|
77
90
|
|
|
78
91
|
def identity
|
|
79
|
-
|
|
92
|
+
Component.identity
|
|
80
93
|
end
|
|
81
94
|
|
|
82
95
|
def default_tag(dir = Dir.pwd)
|