wurk 1.7.6 → 1.8.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 +7 -4
- 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 +33 -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-DsPb_KZ-.js +1 -0
- data/vendor/assets/dashboard/assets/BatchDetail-BCIXJOpZ.js +1 -0
- data/vendor/assets/dashboard/assets/Batches-IjaQfXW6.js +1 -0
- data/vendor/assets/dashboard/assets/Busy-CaQrf1Ve.js +1 -0
- data/vendor/assets/dashboard/assets/Cron-B18EHn_Y.js +1 -0
- data/vendor/assets/dashboard/assets/Dashboard-DdT5ZaUg.js +1 -0
- data/vendor/assets/dashboard/assets/Dead-DWIy-Mu8.js +1 -0
- data/vendor/assets/dashboard/assets/Extension-BjBUWhHf.js +1 -0
- data/vendor/assets/dashboard/assets/FilterBox-DwH3PXXt.js +1 -0
- data/vendor/assets/dashboard/assets/FlowDetail-DnexL72n.js +1 -0
- data/vendor/assets/dashboard/assets/FlowState-Cr4aX6-d.js +1 -0
- data/vendor/assets/dashboard/assets/Flows-DO6EimbZ.js +1 -0
- data/vendor/assets/dashboard/assets/JobDetailModal-aPjgq-U9.js +2 -0
- data/vendor/assets/dashboard/assets/Limiters-DxpwWCNe.js +1 -0
- data/vendor/assets/dashboard/assets/Metrics-C3JL1GIH.js +1 -0
- data/vendor/assets/dashboard/assets/NotFound-BfP9uAV9.js +1 -0
- data/vendor/assets/dashboard/assets/PageHeader-DbC97afG.js +1 -0
- data/vendor/assets/dashboard/assets/Profiles-BcaHreMe.js +1 -0
- data/vendor/assets/dashboard/assets/Queues-ZlBc5tyD.js +1 -0
- data/vendor/assets/dashboard/assets/Retries-Bg3nkeiL.js +1 -0
- data/vendor/assets/dashboard/assets/Scheduled-BrMZPZGl.js +1 -0
- data/vendor/assets/dashboard/assets/Search-B5X5pGht.js +1 -0
- data/vendor/assets/dashboard/assets/charts-uDyf4PN0.js +1 -0
- data/vendor/assets/dashboard/assets/i18n-CJeYfeVt.js +1 -0
- data/vendor/assets/dashboard/assets/index-B36QCAf-.css +1 -0
- data/vendor/assets/dashboard/assets/index-DQujgZgH.js +141 -0
- data/vendor/assets/dashboard/assets/useResetPageOnEmpty-O3-5rMk3.js +1 -0
- data/vendor/assets/dashboard/assets/useSort-xfnwjQAu.js +1 -0
- data/vendor/assets/dashboard/assets/utils-BIKHwNKB.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
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Drop-in require path (see lib/sidekiq.rb): defines Sidekiq::CurrentAttributes
|
|
4
|
+
# (`persist`, `Save`, `Load`). Nothing is registered until `persist` is called,
|
|
5
|
+
# as upstream.
|
|
6
|
+
require 'wurk'
|
|
7
|
+
require 'wurk/middleware/current_attributes'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Drop-in require path, deprecated upstream exactly like sidekiq/testing.
|
|
4
|
+
require 'wurk'
|
|
5
|
+
|
|
6
|
+
Wurk.testing!(:inline)
|
|
7
|
+
warn('⛔️ `require "sidekiq/testing/inline"` is deprecated and will be removed in Sidekiq 9.0. ' \
|
|
8
|
+
'Use `Sidekiq.testing!(:inline)` instead.')
|
data/lib/sidekiq-ent.rb
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# rubocop:disable Naming/FileName -- the gem's own require name, hyphen included
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Drop-in require path (sidekiq-ent.md, top): Wurk ships every Enterprise
|
|
5
|
+
# feature in the one gem, so this only has to load it.
|
|
6
|
+
require 'wurk'
|
|
7
|
+
# rubocop:enable Naming/FileName
|
data/lib/sidekiq-pro.rb
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# rubocop:disable Naming/FileName -- the gem's own require name, hyphen included
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Drop-in require path (sidekiq-pro.md, top): Wurk ships every Pro feature in
|
|
5
|
+
# the one gem, so this only has to load it.
|
|
6
|
+
require 'wurk'
|
|
7
|
+
# rubocop:enable Naming/FileName
|
data/lib/wurk/api/fast.rb
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../lua'
|
|
4
|
-
require_relative '../job_record'
|
|
5
4
|
|
|
6
5
|
module Wurk
|
|
7
6
|
module API
|
|
8
7
|
# Pro parity (§11): Lua-backed O(1)-round-trip replacements for the
|
|
9
|
-
# ruby-side LRANGE
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# with Pro consumer code that drops in on a one-line require swap.
|
|
8
|
+
# ruby-side LRANGE loops in Queue. Mixed into the existing data API class
|
|
9
|
+
# so the surface is `Sidekiq::Queue#delete_job(jid)` — wire-compat with Pro
|
|
10
|
+
# consumer code that drops in on a one-line require swap.
|
|
13
11
|
#
|
|
14
12
|
# We don't reimplement `Queue#size` (already LLEN, unchanged per spec).
|
|
15
13
|
module Fast
|
|
16
14
|
# Extension to Wurk::Queue. Pure server-side delete by jid / class — no
|
|
17
|
-
# network round-trips per match.
|
|
15
|
+
# network round-trips per match.
|
|
18
16
|
module QueueExt
|
|
19
|
-
# @return [
|
|
20
|
-
# the
|
|
17
|
+
# @return [String, nil] the deleted job's JSON, or nil when no job in
|
|
18
|
+
# the queue has that jid — Pro's contract, so `if q.delete_job(jid)`
|
|
19
|
+
# branches on whether anything was removed.
|
|
21
20
|
def delete_job(jid)
|
|
22
21
|
raise ArgumentError, 'jid required' if jid.nil? || jid.to_s.empty?
|
|
23
22
|
|
|
@@ -28,7 +27,7 @@ module Wurk
|
|
|
28
27
|
keys: [Keys.queue(name)],
|
|
29
28
|
argv: [jid.to_s]
|
|
30
29
|
)
|
|
31
|
-
end
|
|
30
|
+
end
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
# @param klass [Class, String, Symbol]
|
|
@@ -47,32 +46,8 @@ module Wurk
|
|
|
47
46
|
end.to_i
|
|
48
47
|
end
|
|
49
48
|
end
|
|
50
|
-
|
|
51
|
-
# Extension to Wurk::SortedSet / JobSet. The base `scan(match, count)`
|
|
52
|
-
# already yields `(value, score)` pairs via ZSCAN — Pro promotes the
|
|
53
|
-
# surface to yield `JobRecord` directly so callers can `entry.delete`
|
|
54
|
-
# / `entry.retry` without re-parsing JSON.
|
|
55
|
-
#
|
|
56
|
-
# The new behavior is enabled by *block arity*: a 1-arg block (or
|
|
57
|
-
# block.lambda? + a 1-param signature) receives `JobRecord`; legacy
|
|
58
|
-
# 2-arg blocks (`|value, score|`) keep the raw shape. This preserves
|
|
59
|
-
# the existing two-arg contract used by JobSet#find_job internally.
|
|
60
|
-
module SortedSetExt
|
|
61
|
-
def scan(match, count = 100, &block)
|
|
62
|
-
return enum_for(:scan, match, count) unless block
|
|
63
|
-
|
|
64
|
-
if block.arity == 1
|
|
65
|
-
super do |value, score|
|
|
66
|
-
block.call(Wurk::SortedEntry.new(self, score, value))
|
|
67
|
-
end
|
|
68
|
-
else
|
|
69
|
-
super
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
49
|
end
|
|
74
50
|
end
|
|
75
51
|
end
|
|
76
52
|
|
|
77
53
|
Wurk::Queue.include(Wurk::API::Fast::QueueExt)
|
|
78
|
-
Wurk::SortedSet.prepend(Wurk::API::Fast::SortedSetExt)
|
data/lib/wurk/api/idempotency.rb
CHANGED
|
@@ -95,17 +95,40 @@ module Wurk
|
|
|
95
95
|
reserved ? nil : existing.to_s
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
# Errors that prove the write they interrupted never applied: refused
|
|
99
|
+
# while dialing, a starved checkout, or a reply in which Redis itself
|
|
100
|
+
# rejected the command.
|
|
101
|
+
PRE_APPLY_ERRORS = [
|
|
102
|
+
::RedisClient::CannotConnectError, ::RedisClient::FailoverError,
|
|
103
|
+
::RedisClient::CommandError, ::ConnectionPool::TimeoutError
|
|
104
|
+
].freeze
|
|
105
|
+
|
|
98
106
|
# Only a success is worth replaying. A rejected body is a request the
|
|
99
|
-
# client should be able to correct and send again under the same key,
|
|
100
|
-
#
|
|
101
|
-
#
|
|
107
|
+
# client should be able to correct and send again under the same key, so
|
|
108
|
+
# it releases. A raise releases only when nothing can have reached Redis
|
|
109
|
+
# — before any write went out, or on an error that proves the one in
|
|
110
|
+
# flight never applied. Anything else (a lost reply, a bulk request that
|
|
111
|
+
# died after an earlier slice landed) is an outcome nobody knows: the key
|
|
112
|
+
# stays pending until its TTL, so the client's retry is told "in flight"
|
|
113
|
+
# instead of enqueueing a second copy.
|
|
102
114
|
def settle(slot, fingerprint)
|
|
115
|
+
Thread.current[::Wurk::Client::WRITE_STATE_KEY] = :clean
|
|
103
116
|
response = yield
|
|
104
117
|
response[0] < 300 ? record(slot, fingerprint, response) : release(slot)
|
|
105
118
|
response
|
|
106
|
-
rescue StandardError
|
|
107
|
-
release(slot)
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
release(slot) unless outcome_unknown?(e)
|
|
108
121
|
raise
|
|
122
|
+
ensure
|
|
123
|
+
Thread.current[::Wurk::Client::WRITE_STATE_KEY] = nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def outcome_unknown?(error)
|
|
127
|
+
case Thread.current[::Wurk::Client::WRITE_STATE_KEY]
|
|
128
|
+
when :applied then true
|
|
129
|
+
when :attempted then PRE_APPLY_ERRORS.none? { |klass| error.is_a?(klass) }
|
|
130
|
+
else false
|
|
131
|
+
end
|
|
109
132
|
end
|
|
110
133
|
|
|
111
134
|
# KEEPTTL so the window is the first request's, not this write's, and XX
|
data/lib/wurk/api/swarm.rb
CHANGED
|
@@ -134,7 +134,7 @@ module Wurk
|
|
|
134
134
|
def limiters(request)
|
|
135
135
|
query = Page.query!(request)
|
|
136
136
|
window = Page.window(query)
|
|
137
|
-
names = ::Wurk::Web::Enterprise::Limits.list(filter: filter!(query))
|
|
137
|
+
names = ::Wurk::Web::Enterprise::Limits.list(filter: filter!(query), sweep: !ReadOnly.enabled?(request))
|
|
138
138
|
rows = Page.slice(names, window) { |name| limiter_row(name) }
|
|
139
139
|
Response.json(200, total: names.size, page: window.page, count: window.count, limiters: rows)
|
|
140
140
|
end
|
data/lib/wurk/batch/callbacks.rb
CHANGED
|
@@ -1,45 +1,73 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require_relative '../lua'
|
|
4
5
|
|
|
5
6
|
module Wurk
|
|
6
7
|
class Batch
|
|
8
|
+
# Raised by `Callbacks.enqueue_callbacks` once every callback for the event
|
|
9
|
+
# has been attempted and at least one could not be enqueued, so the caller
|
|
10
|
+
# leaves the event's dedup marker unwritten and the next fire retries it.
|
|
11
|
+
class CallbackEnqueueError < StandardError; end
|
|
12
|
+
|
|
7
13
|
# Fires batch callbacks (`:success`, `:complete`, `:death`) by enqueuing
|
|
8
|
-
# them as ordinary jobs on the batch's `callback_queue`.
|
|
9
|
-
# b-<bid>-notify so the same callback can't be enqueued twice even
|
|
10
|
-
# if multiple workers race to ack the final job.
|
|
14
|
+
# them as ordinary jobs on the batch's `callback_queue`.
|
|
11
15
|
#
|
|
12
16
|
# Callback wrapper job: Wurk::Batch::CallbackJob — given a target spec
|
|
13
17
|
# ("Klass" or "Klass#method") and options hash, it instantiates and
|
|
14
18
|
# invokes on_<event> (or the named method) with the Status snapshot.
|
|
19
|
+
#
|
|
20
|
+
# A child batch's `:complete` and `:success` callback jobs are enqueued
|
|
21
|
+
# *into its parent batch* (their payload carries the parent's `bid`), so
|
|
22
|
+
# they are live jids of the parent: the parent cannot drain until they
|
|
23
|
+
# have run. That is what makes the spec §2.9 step workflow work —
|
|
24
|
+
# `step1_done` reopens the parent and adds `step2` before the parent can
|
|
25
|
+
# fire. A callback job that fails counts against the parent like any of
|
|
26
|
+
# its jobs (a death suppresses the parent's `:success`). `:death`
|
|
27
|
+
# callbacks stay outside: they fire while the child is still running, and
|
|
28
|
+
# a notification must not hold or poison the parent.
|
|
15
29
|
module Callbacks
|
|
16
30
|
module_function
|
|
17
31
|
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
|
|
32
|
+
# In-process re-drive for the fire and ack paths. Nothing else re-drives
|
|
33
|
+
# a fire once the job that drained the batch has acked, so a transient
|
|
34
|
+
# Redis error there would otherwise strand the callbacks for good.
|
|
35
|
+
REDRIVE_ATTEMPTS = 3
|
|
36
|
+
REDRIVE_BACKOFF = 0.05
|
|
37
|
+
|
|
38
|
+
# Runs on the post-state of a batch script that moved a member out of the
|
|
39
|
+
# live jids or pending-children set (see the gate note heading
|
|
40
|
+
# lua/batch_ack_success.lua). Fires `:complete` once both sets are empty, and
|
|
41
|
+
# `:success` when pending is also 0 and nothing in the subtree is dead;
|
|
42
|
+
# then hands the drain to the parent.
|
|
22
43
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# parent's
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# re-invocation, so at most one of the racing paths reaches a fire and
|
|
30
|
-
# the callback markers absorb the rest (see `fire_complete` for the one
|
|
31
|
-
# window that can still duplicate).
|
|
32
|
-
def maybe_fire(bid, pending:, live:)
|
|
33
|
-
return unless live.zero?
|
|
34
|
-
return unless kids_finished?(bid)
|
|
44
|
+
# Spec §2.4: child `:complete`/`:success` fire before the parent's, so a
|
|
45
|
+
# parent whose own jobs are done while a child batch is still running
|
|
46
|
+
# waits here (kids > 0); the child's `propagate_to_parent` re-enters with
|
|
47
|
+
# the parent's fresh state when it finishes.
|
|
48
|
+
def maybe_fire(bid, pending:, live:, kids:)
|
|
49
|
+
return unless live.zero? && kids.zero?
|
|
35
50
|
|
|
36
51
|
fire_complete(bid)
|
|
37
52
|
fire_success(bid) if pending.zero? && !subtree_dead?(bid)
|
|
38
53
|
propagate_to_parent(bid)
|
|
39
54
|
end
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
# Yields the attempt number (0-based) and retries a StandardError up to
|
|
57
|
+
# REDRIVE_ATTEMPTS times in total, re-raising the last one. Every block
|
|
58
|
+
# handed to it is idempotent: the ack scripts are SREM/SADD-guarded and
|
|
59
|
+
# the fires are marker-guarded.
|
|
60
|
+
def retrying(attempts: REDRIVE_ATTEMPTS)
|
|
61
|
+
attempt = 0
|
|
62
|
+
begin
|
|
63
|
+
yield attempt
|
|
64
|
+
rescue StandardError
|
|
65
|
+
attempt += 1
|
|
66
|
+
raise if attempt >= attempts
|
|
67
|
+
|
|
68
|
+
sleep(REDRIVE_BACKOFF * attempt)
|
|
69
|
+
retry
|
|
70
|
+
end
|
|
43
71
|
end
|
|
44
72
|
|
|
45
73
|
# Fired from Wurk::Batch::DeathHandler whenever a death makes the died
|
|
@@ -58,13 +86,20 @@ module Wurk
|
|
|
58
86
|
# dashboard and `subtree_dead?` all still see a dead batch. `:complete`
|
|
59
87
|
# and `:success` have no such fallback — the callback is their whole
|
|
60
88
|
# signal — and this claim additionally gates `cascade_death`, which
|
|
61
|
-
# would otherwise re-walk the ancestor chain on every re-invocation.
|
|
89
|
+
# would otherwise re-walk the ancestor chain on every re-invocation. An
|
|
90
|
+
# enqueue that *raises* is different from a crash: the claim is handed
|
|
91
|
+
# back so the caller's re-drive fires `:death` again.
|
|
62
92
|
def fire_death(bid)
|
|
63
93
|
record_event(bid, 'death_at')
|
|
64
94
|
index_dead(bid)
|
|
65
95
|
return unless dedup_set(bid, 'death')
|
|
66
96
|
|
|
67
|
-
|
|
97
|
+
begin
|
|
98
|
+
enqueue_callbacks(bid, 'death')
|
|
99
|
+
rescue CallbackEnqueueError
|
|
100
|
+
Wurk.redis { |conn| conn.call('DEL', "b-#{bid}-death") }
|
|
101
|
+
raise
|
|
102
|
+
end
|
|
68
103
|
cascade_death(bid)
|
|
69
104
|
end
|
|
70
105
|
|
|
@@ -95,22 +130,19 @@ module Wurk
|
|
|
95
130
|
end
|
|
96
131
|
|
|
97
132
|
# `:complete` and `:success` mark their dedup key *after* the enqueue,
|
|
98
|
-
# never before (F16)
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
133
|
+
# never before (F16): a claim-then-enqueue ordering turns a crash in
|
|
134
|
+
# between into callbacks nobody ever enqueues. Enqueuing first makes the
|
|
135
|
+
# durable side effect happen before the marker that suppresses it, and an
|
|
136
|
+
# enqueue that raised leaves the marker unwritten so the re-drive (the
|
|
137
|
+
# caller's `retrying`, or a reclaimed re-run of the draining job) fires
|
|
138
|
+
# the event again.
|
|
104
139
|
#
|
|
105
140
|
# The accepted direction is a duplicate over a lost callback: callback
|
|
106
141
|
# jobs retry like any other job and must already be idempotent (spec
|
|
107
142
|
# §2.4, §12 "Callback retries"), so firing one twice is a cost the app
|
|
108
143
|
# is required to absorb, while losing one silently strands the batch.
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
# reclaimed child re-running propagate_to_parent, a second DeathHandler
|
|
112
|
-
# pass — so the duplicate window is only two genuinely concurrent acks
|
|
113
|
-
# interleaving between each other's check and mark.
|
|
144
|
+
# Concurrent duplicates are confined to re-drives: the fire gate admits
|
|
145
|
+
# exactly one caller per drain transition.
|
|
114
146
|
#
|
|
115
147
|
# `record_event` stays ahead of the enqueue: the callback job reads a
|
|
116
148
|
# Status snapshot and must see `complete_at`/`success_at` already set.
|
|
@@ -140,11 +172,8 @@ module Wurk
|
|
|
140
172
|
# full success. `created_at` shares the CLOCK_REALTIME epoch we record it
|
|
141
173
|
# with. No-op without a dogstatsd client.
|
|
142
174
|
#
|
|
143
|
-
# Strictly best-effort:
|
|
144
|
-
# enqueue
|
|
145
|
-
# Redis hiccup on the HGET) would abort `fire_success` with nothing left
|
|
146
|
-
# to re-drive it — the success callbacks and linger would be stranded for
|
|
147
|
-
# good. Swallow and log instead.
|
|
175
|
+
# Strictly best-effort: a raise here would abort `fire_success` ahead of
|
|
176
|
+
# the enqueue for the sake of a metric. Swallow and log instead.
|
|
148
177
|
def emit_duration_metric(bid)
|
|
149
178
|
created = Wurk.redis { |conn| conn.call('HGET', "b-#{bid}", 'created_at') }
|
|
150
179
|
return if created.nil? || created.to_s.empty?
|
|
@@ -163,14 +192,13 @@ module Wurk
|
|
|
163
192
|
raw = Wurk.redis { |conn| conn.call('HGET', "b-#{bid}", 'linger') }
|
|
164
193
|
seconds = raw.nil? || raw.to_s.empty? ? Batch::POST_SUCCESS_EXPIRY_SECONDS : raw.to_i
|
|
165
194
|
Wurk.redis do |conn|
|
|
166
|
-
Batch.keys_for(bid).each { |key|
|
|
195
|
+
conn.pipelined { |pipe| Batch.keys_for(bid).each { |key| pipe.call('EXPIRE', key, seconds) } }
|
|
167
196
|
end
|
|
168
197
|
end
|
|
169
198
|
|
|
170
199
|
# True once `b-<bid>-<event>` exists, i.e. an enqueue pass for `event`
|
|
171
200
|
# has completed. The read-side half of the enqueue-then-mark ordering in
|
|
172
|
-
# `fire_complete`/`fire_success
|
|
173
|
-
# because its `dedup_set` still doubles as the claim.
|
|
201
|
+
# `fire_complete`/`fire_success`.
|
|
174
202
|
def dedup_marked?(bid, event)
|
|
175
203
|
Wurk.redis { |conn| conn.call('EXISTS', "b-#{bid}-#{event}") }.to_i == 1
|
|
176
204
|
end
|
|
@@ -190,17 +218,17 @@ module Wurk
|
|
|
190
218
|
end
|
|
191
219
|
end
|
|
192
220
|
|
|
193
|
-
# The
|
|
194
|
-
# already expired
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
# post-success `linger` window, untouched.
|
|
221
|
+
# The HSET resurrects the hash when a callback fires for a batch whose keys
|
|
222
|
+
# already expired, so the write is followed by an NX stamp — without it the
|
|
223
|
+
# resurrected hash would have no clock at all. NX leaves a live batch's
|
|
224
|
+
# expiry, and the shorter post-success `linger` window, untouched.
|
|
198
225
|
def record_event(bid, field)
|
|
199
226
|
now = ::Process.clock_gettime(::Process::CLOCK_REALTIME)
|
|
200
227
|
Wurk.redis do |conn|
|
|
201
|
-
conn.
|
|
202
|
-
|
|
203
|
-
|
|
228
|
+
conn.pipelined do |pipe|
|
|
229
|
+
pipe.call('HSET', "b-#{bid}", field, now.to_s, field.delete_suffix('_at'), '1')
|
|
230
|
+
pipe.call('EXPIRE', "b-#{bid}", Batch::DEFAULT_EXPIRY_SECONDS, 'NX')
|
|
231
|
+
end
|
|
204
232
|
end
|
|
205
233
|
end
|
|
206
234
|
|
|
@@ -233,13 +261,13 @@ module Wurk
|
|
|
233
261
|
# last dead job is manually retried back to success, the descendant
|
|
234
262
|
# clears its OWN death mark (#212, in BATCH_PUSH) — but every ancestor
|
|
235
263
|
# was marked by the death *cascade*, not by a jid in its own died set,
|
|
236
|
-
# so nothing
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
#
|
|
242
|
-
#
|
|
264
|
+
# so nothing there ever clears them. Re-evaluated whenever a child
|
|
265
|
+
# drains (propagate_to_parent): drop the durable death mark and
|
|
266
|
+
# `dead-batches` membership once the batch's own died set is empty AND
|
|
267
|
+
# no child still carries a death mark. The `b-<bid>-death` notify dedup
|
|
268
|
+
# key is deliberately left intact, so a later re-death re-marks the batch
|
|
269
|
+
# (fire_death restores the flag before its own dedup guard) without ever
|
|
270
|
+
# re-enqueuing `:death`.
|
|
243
271
|
def clear_death_on_recovery(bid)
|
|
244
272
|
return unless death_fired?(bid)
|
|
245
273
|
return if own_died_remaining?(bid)
|
|
@@ -261,26 +289,46 @@ module Wurk
|
|
|
261
289
|
end
|
|
262
290
|
|
|
263
291
|
# Per-callback rescue: one bad spec or a transient enqueue failure must
|
|
264
|
-
# not
|
|
265
|
-
#
|
|
292
|
+
# not keep the remaining callbacks for this event from being attempted.
|
|
293
|
+
# Each failure reaches the error handlers; once all were attempted, any
|
|
294
|
+
# failure raises so the caller does not write the event's marker.
|
|
266
295
|
def enqueue_callbacks(bid, event)
|
|
267
|
-
|
|
296
|
+
failed = callback_items(bid, event).count { |item| !push_callback(bid, event, item) }
|
|
297
|
+
return if failed.zero?
|
|
298
|
+
|
|
299
|
+
raise CallbackEnqueueError, "batch #{bid}: #{failed} #{event} callback(s) failed to enqueue"
|
|
300
|
+
end
|
|
268
301
|
|
|
269
|
-
|
|
302
|
+
def callback_items(bid, event)
|
|
303
|
+
callbacks, queue, parent_bid = callback_specs_for(bid)
|
|
304
|
+
parent_bid = nil if event == 'death'
|
|
305
|
+
callbacks.filter_map do |(cb_event, target, options)|
|
|
270
306
|
next unless cb_event == event
|
|
271
307
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
308
|
+
item = callback_item(bid, target, event, options, queue)
|
|
309
|
+
item['bid'] = parent_bid if parent_bid
|
|
310
|
+
item
|
|
275
311
|
end
|
|
276
312
|
end
|
|
277
313
|
|
|
314
|
+
# True when the callback job was enqueued.
|
|
315
|
+
def push_callback(bid, event, item)
|
|
316
|
+
Batch.with_thread_batch(nil, nil) { Wurk::Client.push(item) }
|
|
317
|
+
true
|
|
318
|
+
rescue StandardError => e
|
|
319
|
+
Wurk.configuration.handle_exception(
|
|
320
|
+
e, { context: "batch #{bid}: #{event} callback #{item['args'][1].inspect} enqueue failed", bid: bid }
|
|
321
|
+
)
|
|
322
|
+
false
|
|
323
|
+
end
|
|
324
|
+
|
|
278
325
|
def callback_specs_for(bid)
|
|
279
|
-
|
|
280
|
-
|
|
326
|
+
callbacks_json, queue, parent_bid = Wurk.redis do |conn|
|
|
327
|
+
conn.call('HMGET', "b-#{bid}", 'callbacks', 'callback_queue', 'parent_bid')
|
|
328
|
+
end
|
|
281
329
|
queue = 'default' if queue.nil? || queue.empty?
|
|
282
|
-
|
|
283
|
-
[
|
|
330
|
+
parent_bid = nil if parent_bid.nil? || parent_bid.empty?
|
|
331
|
+
[parse_callbacks(callbacks_json), queue, parent_bid]
|
|
284
332
|
end
|
|
285
333
|
|
|
286
334
|
def parse_callbacks(raw)
|
|
@@ -291,46 +339,49 @@ module Wurk
|
|
|
291
339
|
[]
|
|
292
340
|
end
|
|
293
341
|
|
|
294
|
-
|
|
295
|
-
|
|
342
|
+
# Pushed (by push_callback) with the batch thread-locals cleared: a
|
|
343
|
+
# fire can run on a thread that is inside some other batch's `#jobs`
|
|
344
|
+
# block (a hold released at the end of a nested block), and the client
|
|
345
|
+
# middleware would otherwise stamp that batch's bid over the parent bid
|
|
346
|
+
# — or the buffer would swallow the push until that block exits.
|
|
347
|
+
def callback_item(bid, target, event, options, queue)
|
|
348
|
+
{
|
|
296
349
|
'class' => 'Wurk::Batch::CallbackJob',
|
|
297
350
|
'args' => [bid, target, event, options],
|
|
298
351
|
'queue' => queue,
|
|
299
352
|
'retry' => true
|
|
300
|
-
|
|
353
|
+
}
|
|
301
354
|
end
|
|
302
355
|
|
|
303
|
-
# When a child batch
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
#
|
|
356
|
+
# When a child batch drains (by success or death), drop it from the
|
|
357
|
+
# parent's pending children and re-evaluate the parent with the state
|
|
358
|
+
# BATCH_KID_DONE read atomically alongside the SREM. A re-run of the
|
|
359
|
+
# child (a reclaimed job, a re-drive) removes nothing but still reports
|
|
360
|
+
# the parent's state, so a parent fire lost to a crash is re-driven too.
|
|
361
|
+
#
|
|
362
|
+
# A recovered child may have lifted the last death from the parent's
|
|
363
|
+
# subtree — clear the parent's cascaded mark first, so the parent's
|
|
364
|
+
# `:success` can fire whenever it next drains (here, or when this
|
|
365
|
+
# child's callback jobs ack into it). Harmless on the death path: the
|
|
366
|
+
# dying child still carries its mark, so any_child_dead? keeps the
|
|
367
|
+
# parent dead.
|
|
308
368
|
def propagate_to_parent(bid)
|
|
309
369
|
parent_bid = parent_bid_for(bid)
|
|
310
370
|
return if parent_bid.nil? || parent_bid.empty?
|
|
311
|
-
return unless pkids_drained?(parent_bid, bid)
|
|
312
371
|
|
|
313
|
-
# A recovered child may have lifted the last death from the parent's
|
|
314
|
-
# subtree — clear the parent's cascaded mark before its gate runs, so
|
|
315
|
-
# `:success` can fire. Harmless on the death path: the dying child
|
|
316
|
-
# still carries its mark, so any_child_dead? keeps the parent dead.
|
|
317
372
|
clear_death_on_recovery(parent_bid)
|
|
318
|
-
|
|
373
|
+
pending, live, kids = Wurk.redis do |conn|
|
|
374
|
+
Wurk::Lua::Loader.eval_cached(
|
|
375
|
+
conn, :batch_kid_done,
|
|
376
|
+
keys: ["b-#{parent_bid}", "b-#{parent_bid}-jids", "b-#{parent_bid}-pkids"], argv: [bid]
|
|
377
|
+
)
|
|
378
|
+
end
|
|
379
|
+
maybe_fire(parent_bid, pending: pending.to_i, live: live.to_i, kids: kids.to_i)
|
|
319
380
|
end
|
|
320
381
|
|
|
321
382
|
def parent_bid_for(bid)
|
|
322
383
|
Wurk.redis { |conn| conn.call('HGET', "b-#{bid}", 'parent_bid') }
|
|
323
384
|
end
|
|
324
|
-
|
|
325
|
-
def pkids_drained?(parent_bid, child_bid)
|
|
326
|
-
Wurk.redis do |conn|
|
|
327
|
-
conn.call('SREM', "b-#{parent_bid}-pkids", child_bid)
|
|
328
|
-
conn.call('SCARD', "b-#{parent_bid}-pkids").to_i.zero?
|
|
329
|
-
end
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
def pending_for(bid) = Wurk.redis { |conn| conn.call('HGET', "b-#{bid}", 'pending') }.to_i
|
|
333
|
-
def live_for(bid) = Wurk.redis { |conn| conn.call('SCARD', "b-#{bid}-jids") }.to_i
|
|
334
385
|
end
|
|
335
386
|
end
|
|
336
387
|
end
|