wurk 1.9.0 → 1.10.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/app/controllers/wurk/api_controller.rb +4 -1
- data/lib/wurk/boot_checks.rb +87 -0
- data/lib/wurk/capsule.rb +33 -8
- data/lib/wurk/cli.rb +73 -8
- data/lib/wurk/client/buffered.rb +240 -26
- data/lib/wurk/compat.rb +11 -0
- data/lib/wurk/configuration.rb +114 -16
- data/lib/wurk/cron.rb +45 -0
- data/lib/wurk/debounce.rb +2 -2
- data/lib/wurk/error_coalescer.rb +35 -0
- data/lib/wurk/fetcher/at_maxmemory.rb +79 -0
- data/lib/wurk/fetcher/capped.rb +4 -0
- data/lib/wurk/fetcher/many_queues.rb +17 -1
- data/lib/wurk/fetcher/oom_drain.rb +102 -0
- data/lib/wurk/fetcher/orphan_grace.rb +60 -44
- data/lib/wurk/fetcher/private_list_key.rb +1 -16
- data/lib/wurk/fetcher/reaper.rb +169 -127
- data/lib/wurk/fetcher/reliable.rb +26 -4
- data/lib/wurk/fetcher/self_reconcile.rb +175 -0
- data/lib/wurk/fetcher/unit_of_work.rb +32 -8
- data/lib/wurk/health.rb +23 -4
- data/lib/wurk/health_probe.rb +64 -0
- data/lib/wurk/heartbeat.rb +55 -3
- data/lib/wurk/job/options.rb +9 -1
- data/lib/wurk/job.rb +2 -24
- data/lib/wurk/job_record.rb +17 -9
- data/lib/wurk/job_retry.rb +38 -10
- data/lib/wurk/job_set.rb +13 -5
- data/lib/wurk/launcher.rb +134 -52
- data/lib/wurk/leader.rb +9 -3
- data/lib/wurk/limiter/base.rb +17 -1
- data/lib/wurk/lua/debounce.lua +11 -6
- data/lib/wurk/lua/loader.rb +13 -1
- data/lib/wurk/lua.rb +71 -8
- data/lib/wurk/manager.rb +1 -1
- data/lib/wurk/metrics/process_counters.rb +27 -0
- data/lib/wurk/metrics/prometheus.rb +35 -14
- data/lib/wurk/middleware/current_attributes.rb +26 -10
- data/lib/wurk/middleware/poison_pill.rb +1 -2
- data/lib/wurk/process_set.rb +3 -1
- data/lib/wurk/processor.rb +82 -18
- data/lib/wurk/queue_consumers.rb +134 -0
- data/lib/wurk/rails_boot.rb +40 -2
- data/lib/wurk/rails_reloader.rb +51 -0
- data/lib/wurk/railtie.rb +20 -1
- data/lib/wurk/redact.rb +12 -0
- data/lib/wurk/redis_pool.rb +34 -9
- data/lib/wurk/scheduled.rb +142 -67
- data/lib/wurk/sentry/death_handler.rb +29 -0
- data/lib/wurk/sentry/error_handler.rb +52 -10
- data/lib/wurk/sentry/middleware.rb +8 -1
- data/lib/wurk/sentry/retry_policy.rb +12 -3
- data/lib/wurk/sentry.rb +42 -5
- data/lib/wurk/sorted_entry.rb +38 -13
- data/lib/wurk/swarm/child_boot.rb +42 -31
- data/lib/wurk/swarm/crash_watch.rb +65 -0
- data/lib/wurk/swarm.rb +79 -11
- data/lib/wurk/testing.rb +66 -0
- data/lib/wurk/version.rb +1 -1
- data/lib/wurk/web/extension.rb +6 -1
- data/lib/wurk/work_set.rb +6 -0
- data/lib/wurk/worker/setter.rb +11 -6
- data/lib/wurk/worker.rb +17 -1
- data/lib/wurk.rb +1 -1
- data/vendor/assets/dashboard/assets/{ArgsValue-DDqX6XWH.js → ArgsValue-CmaXArv8.js} +1 -1
- data/vendor/assets/dashboard/assets/{BatchDetail-lIytcn2C.js → BatchDetail-nc5yePZW.js} +1 -1
- data/vendor/assets/dashboard/assets/{Batches-BEumt-1O.js → Batches-DgEcyOZ_.js} +1 -1
- data/vendor/assets/dashboard/assets/{Busy-Vk9HmZcN.js → Busy-CUZncvCy.js} +1 -1
- data/vendor/assets/dashboard/assets/{Cron-CWcM-Cfr.js → Cron-CIZbaaZs.js} +1 -1
- data/vendor/assets/dashboard/assets/{Dashboard-Bgv1kusv.js → Dashboard-BWmfAook.js} +1 -1
- data/vendor/assets/dashboard/assets/{Dead-Di5Rct2H.js → Dead-CGKsTZHq.js} +1 -1
- data/vendor/assets/dashboard/assets/{Extension-LZY5-imc.js → Extension-Doy4bJoA.js} +1 -1
- data/vendor/assets/dashboard/assets/{FilterBox-BDo-7IAy.js → FilterBox-C4bO26_d.js} +1 -1
- data/vendor/assets/dashboard/assets/{FlowDetail-CehwRLTt.js → FlowDetail-Dh3SJdFh.js} +1 -1
- data/vendor/assets/dashboard/assets/{FlowState-glK2Tnf8.js → FlowState-yb7virqR.js} +1 -1
- data/vendor/assets/dashboard/assets/{Flows-cgy-Fsbp.js → Flows-C5SMRuhq.js} +1 -1
- data/vendor/assets/dashboard/assets/{JobDetailModal-Cuk3jruU.js → JobDetailModal-Cg6sAsJB.js} +1 -1
- data/vendor/assets/dashboard/assets/{Limiters-Ci8G2yZG.js → Limiters-BIxwDOJM.js} +1 -1
- data/vendor/assets/dashboard/assets/{Metrics-CQFUeZTR.js → Metrics-Qp9q0nF8.js} +1 -1
- data/vendor/assets/dashboard/assets/{NotFound-CYjhuDsI.js → NotFound-DwjPxFjU.js} +1 -1
- data/vendor/assets/dashboard/assets/{PageHeader-DjIAFTyX.js → PageHeader-vWeBbSPF.js} +1 -1
- data/vendor/assets/dashboard/assets/{Profiles-DTowqIRg.js → Profiles-7dabYVe3.js} +1 -1
- data/vendor/assets/dashboard/assets/Queues-CMg-gyP8.js +1 -0
- data/vendor/assets/dashboard/assets/{Retries-SJ3BaKC0.js → Retries-CNX8ZtTH.js} +1 -1
- data/vendor/assets/dashboard/assets/{Scheduled-DxSRpmFZ.js → Scheduled-ClVBj0nc.js} +1 -1
- data/vendor/assets/dashboard/assets/{Search-bBbi5JNK.js → Search-BAsVE2qw.js} +1 -1
- data/vendor/assets/dashboard/assets/{charts-Bg2YBcaD.js → charts-DZs138BV.js} +1 -1
- data/vendor/assets/dashboard/assets/i18n-CBewVzQ_.js +1 -0
- data/vendor/assets/dashboard/assets/index--xvvNlYx.css +1 -0
- data/vendor/assets/dashboard/assets/{index-BOTVSP9M.js → index-CMzPFxNE.js} +3 -3
- data/vendor/assets/dashboard/assets/{useResetPageOnEmpty-BRPNyV2M.js → useResetPageOnEmpty-DxkLZCIm.js} +1 -1
- data/vendor/assets/dashboard/assets/{useSort-B-BLJik7.js → useSort-jBYVwXvg.js} +1 -1
- data/vendor/assets/dashboard/assets/{utils-CGyrKpTh.js → utils-B0kQFNE8.js} +1 -1
- data/vendor/assets/dashboard/index.html +3 -3
- data/vendor/assets/dashboard/wurk-manifest.json +2 -2
- metadata +41 -30
- data/vendor/assets/dashboard/assets/Queues-DUGRPjME.js +0 -1
- data/vendor/assets/dashboard/assets/i18n-BZClP62Q.js +0 -1
- data/vendor/assets/dashboard/assets/index-Rewk4eSE.css +0 -1
data/lib/wurk/client/buffered.rb
CHANGED
|
@@ -9,15 +9,28 @@ module Wurk
|
|
|
9
9
|
# creation or batch-context pushes (`bid` on payload): BATCH_PUSH has
|
|
10
10
|
# atomic counter side-effects we can't safely replay.
|
|
11
11
|
#
|
|
12
|
+
# A failed push opens a circuit for that Redis: later pushes buffer without
|
|
13
|
+
# touching the socket, so one caller pays the connection backoff and the rest
|
|
14
|
+
# do not. The Drainer thread, started on the first failure if none is running,
|
|
15
|
+
# is what probes the server and closes the circuit.
|
|
16
|
+
#
|
|
12
17
|
# One buffer serves every pool, so each entry remembers the Redis it was
|
|
13
18
|
# headed for (its {Origin}) and is only ever replayed there: a sharded app
|
|
14
19
|
# pushing through `Client.via(shard_a)` must not have its outage backlog
|
|
15
20
|
# land on shard B because B's producer happened to push next.
|
|
16
21
|
#
|
|
17
22
|
# Spec: docs/target/sidekiq-pro.md §5.
|
|
18
|
-
module Buffered
|
|
23
|
+
module Buffered # rubocop:disable Metrics/ModuleLength
|
|
19
24
|
DEFAULT_BUFFER_CAP = 1_000
|
|
20
25
|
DRAINING_KEY = :wurk_reliable_push_draining
|
|
26
|
+
UNBUFFERED_KEY = :wurk_reliable_push_unbuffered
|
|
27
|
+
|
|
28
|
+
# A clean exit tries to deliver what is still in memory, but never holds the
|
|
29
|
+
# process up for longer than this: a hung Redis would otherwise stall shutdown.
|
|
30
|
+
EXIT_FLUSH_SECONDS = 5
|
|
31
|
+
# Paces flush's retries as well as its checks, so a server that keeps refusing
|
|
32
|
+
# gets a few attempts in that window and not hundreds of reports.
|
|
33
|
+
FLUSH_POLL = 0.25
|
|
21
34
|
|
|
22
35
|
# Overflow modes. `:drop_oldest` is the spec default (Sidekiq Pro §5
|
|
23
36
|
# ring buffer). `:raise` lets callers decide what to do on backpressure
|
|
@@ -70,6 +83,67 @@ module Wurk
|
|
|
70
83
|
end
|
|
71
84
|
end
|
|
72
85
|
|
|
86
|
+
# Which Redis servers are refusing connections, by pool identity. An entry
|
|
87
|
+
# exists only while its circuit is open, so a rebuilt pool leaves nothing behind.
|
|
88
|
+
class Circuit
|
|
89
|
+
# A circuit closes itself after this long, so a dead drainer costs a caller
|
|
90
|
+
# one attempt per window instead of buffering forever. A failed probe renews it.
|
|
91
|
+
OPEN_SECONDS = 30.0
|
|
92
|
+
|
|
93
|
+
def initialize
|
|
94
|
+
@lock = Mutex.new
|
|
95
|
+
@open_until = {}.compare_by_identity
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def open?(pool)
|
|
99
|
+
return false if @open_until.empty?
|
|
100
|
+
|
|
101
|
+
@lock.synchronize do
|
|
102
|
+
deadline = @open_until[pool]
|
|
103
|
+
next false unless deadline
|
|
104
|
+
next true if monotonic < deadline
|
|
105
|
+
|
|
106
|
+
@open_until.delete(pool)
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def trip(pool)
|
|
112
|
+
@lock.synchronize { @open_until[pool] = monotonic + OPEN_SECONDS }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def close(pool)
|
|
116
|
+
@lock.synchronize { @open_until.delete(pool) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def any_open?
|
|
120
|
+
!@open_until.empty?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# For a circuit with nothing buffered behind it (a batched push tripped it,
|
|
124
|
+
# or `:raise` overflow kept the buffer empty): no replay exists to prove the
|
|
125
|
+
# server is back. Any answer means reachable, and an error such as LOADING
|
|
126
|
+
# belongs to the next real push to report, not to the circuit.
|
|
127
|
+
def ping_all(except:)
|
|
128
|
+
@lock.synchronize { @open_until.keys }.each { |pool| ping(pool) unless except.any? { |o| o.equal?(pool) } }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def ping(pool)
|
|
134
|
+
pool.with { |conn| conn.call('PING') }
|
|
135
|
+
close(pool)
|
|
136
|
+
rescue RedisClient::ConnectionError, ConnectionPool::TimeoutError
|
|
137
|
+
trip(pool)
|
|
138
|
+
rescue RedisClient::CommandError
|
|
139
|
+
close(pool)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def monotonic
|
|
143
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
73
147
|
# Eagerly initialized: `||=` inside an accessor is not atomic — two
|
|
74
148
|
# threads racing first-touch could end up holding distinct Mutex
|
|
75
149
|
# instances and lose all synchronization on the shared buffer.
|
|
@@ -84,6 +158,8 @@ module Wurk
|
|
|
84
158
|
# buys immunity from both.
|
|
85
159
|
@install_mutex = Mutex.new
|
|
86
160
|
@buffer_mutex = Mutex.new
|
|
161
|
+
@replaying = 0
|
|
162
|
+
@circuit = Circuit.new
|
|
87
163
|
|
|
88
164
|
# Process that owns the state above; a mismatch means we're running in a
|
|
89
165
|
# fork and the inherited copy has to go.
|
|
@@ -99,6 +175,7 @@ module Wurk
|
|
|
99
175
|
|
|
100
176
|
Wurk::Client.prepend(InstanceMethods)
|
|
101
177
|
@installed = true
|
|
178
|
+
at_exit { Buffered.flush }
|
|
102
179
|
end
|
|
103
180
|
end
|
|
104
181
|
|
|
@@ -145,7 +222,9 @@ module Wurk
|
|
|
145
222
|
@buffer = []
|
|
146
223
|
@buffer_cap = nil
|
|
147
224
|
@overflow_mode = nil
|
|
225
|
+
@replaying = 0
|
|
148
226
|
end
|
|
227
|
+
@circuit = Circuit.new
|
|
149
228
|
@drops = DropLog.new
|
|
150
229
|
# Stop before dropping: an unstopped drainer thread would otherwise
|
|
151
230
|
# tick on forever, unreachable, leaking the thread.
|
|
@@ -184,8 +263,11 @@ module Wurk
|
|
|
184
263
|
@install_mutex = Mutex.new
|
|
185
264
|
@buffer_mutex = Mutex.new
|
|
186
265
|
@buffer = []
|
|
266
|
+
@replaying = 0
|
|
267
|
+
@circuit = Circuit.new
|
|
187
268
|
@drops = DropLog.new
|
|
188
|
-
interval = @drainer&.interval
|
|
269
|
+
interval = @drainer&.interval unless @implicit_drainer
|
|
270
|
+
@implicit_drainer = false
|
|
189
271
|
@drainer = nil
|
|
190
272
|
start_drainer!(interval: interval) if interval
|
|
191
273
|
true
|
|
@@ -258,32 +340,63 @@ module Wurk
|
|
|
258
340
|
# not emit — the replay is where the job actually reaches Redis, so a
|
|
259
341
|
# buffered-then-drained job counts once as enqueued and once as
|
|
260
342
|
# recovered.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
343
|
+
#
|
|
344
|
+
# Skipped outright while the circuit is open, unless this is the probe.
|
|
345
|
+
def drain!(client, probe: false)
|
|
264
346
|
target = client.send(:pool)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
break unless replay(client, entry)
|
|
347
|
+
return 0 if !probe && circuit.open?(target)
|
|
348
|
+
return 0 if buffer_mutex.synchronize { buffer.empty? }
|
|
268
349
|
|
|
269
|
-
|
|
270
|
-
end
|
|
350
|
+
drained = replay_all(client, target)
|
|
271
351
|
drops.end_burst if drained.positive?
|
|
272
352
|
drained
|
|
273
353
|
end
|
|
274
354
|
|
|
355
|
+
# Pushes inside the block reach Redis or raise. For a caller that goes on to
|
|
356
|
+
# delete its own copy of the job once push returns, which must not count
|
|
357
|
+
# memory as delivered: a crash would then lose the only copy.
|
|
358
|
+
def unbuffered
|
|
359
|
+
previous = Thread.current[UNBUFFERED_KEY]
|
|
360
|
+
Thread.current[UNBUFFERED_KEY] = true
|
|
361
|
+
yield
|
|
362
|
+
ensure
|
|
363
|
+
Thread.current[UNBUFFERED_KEY] = previous
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def unbuffered?
|
|
367
|
+
Thread.current[UNBUFFERED_KEY] == true
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Replays whatever is still buffered, whichever circuit it is behind, for up
|
|
371
|
+
# to `timeout`. Returns how many jobs are left. Runs on a clean exit; a
|
|
372
|
+
# Launcher or host shutting down earlier can call it too. Forked children
|
|
373
|
+
# inherit the at_exit hook, but start with an empty buffer of their own.
|
|
374
|
+
def flush(timeout: EXIT_FLUSH_SECONDS)
|
|
375
|
+
return 0 if undelivered_count.zero?
|
|
376
|
+
|
|
377
|
+
retry_until_delivered(flush_clock + timeout)
|
|
378
|
+
undelivered_count.tap { |left| report_undelivered(left) if left.positive? }
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def trip!(pool)
|
|
382
|
+
circuit.trip(pool)
|
|
383
|
+
ensure_drainer
|
|
384
|
+
end
|
|
385
|
+
|
|
275
386
|
# The background drainer's pass: every origin in the buffer, each
|
|
276
387
|
# through a client of its own, so one Redis still down does not hold
|
|
277
388
|
# back another that has recovered. Returns the total replayed; the
|
|
278
389
|
# first non-transient error is re-raised after every origin had its turn.
|
|
279
390
|
def drain_all!
|
|
280
391
|
failure = nil
|
|
281
|
-
|
|
282
|
-
|
|
392
|
+
pools = buffered_pools
|
|
393
|
+
drained = pools.sum do |pool|
|
|
394
|
+
drain!(Wurk::Client.new(pool: pool), probe: true)
|
|
283
395
|
rescue StandardError => e
|
|
284
396
|
failure ||= e
|
|
285
397
|
0
|
|
286
398
|
end
|
|
399
|
+
circuit.ping_all(except: pools)
|
|
287
400
|
raise failure if failure
|
|
288
401
|
|
|
289
402
|
drained
|
|
@@ -294,7 +407,7 @@ module Wurk
|
|
|
294
407
|
@buffer ||= []
|
|
295
408
|
end
|
|
296
409
|
|
|
297
|
-
attr_reader :drops
|
|
410
|
+
attr_reader :drops, :circuit
|
|
298
411
|
|
|
299
412
|
# Start a background drain thread that wakes every `interval`
|
|
300
413
|
# seconds and tries to flush the buffer. Idempotent — replaces
|
|
@@ -305,6 +418,7 @@ module Wurk
|
|
|
305
418
|
def start_drainer!(interval: Drainer::DEFAULT_INTERVAL)
|
|
306
419
|
install_mutex.synchronize do
|
|
307
420
|
@drainer&.stop
|
|
421
|
+
@implicit_drainer = false
|
|
308
422
|
@drainer = Drainer.new(interval: interval)
|
|
309
423
|
@drainer.start
|
|
310
424
|
end
|
|
@@ -325,14 +439,83 @@ module Wurk
|
|
|
325
439
|
|
|
326
440
|
attr_reader :install_mutex, :buffer_mutex
|
|
327
441
|
|
|
442
|
+
# A replay holds its entry outside the buffer until it lands or goes back, so
|
|
443
|
+
# an empty buffer alone does not mean every job reached Redis.
|
|
444
|
+
def undelivered_count
|
|
445
|
+
buffer_mutex.synchronize { buffer.size + @replaying }
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def retry_until_delivered(deadline)
|
|
449
|
+
worker = nil
|
|
450
|
+
until undelivered_count.zero? || flush_clock >= deadline
|
|
451
|
+
worker = start_flush_attempt unless worker&.alive? || buffer_size.zero?
|
|
452
|
+
sleep(FLUSH_POLL)
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def flush_clock
|
|
457
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def start_flush_attempt
|
|
461
|
+
Thread.new do
|
|
462
|
+
drain_all!
|
|
463
|
+
rescue StandardError => e
|
|
464
|
+
Wurk.configuration.handle_exception(e, { context: 'reliable_push flush' })
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def report_undelivered(left)
|
|
469
|
+
Wurk.configuration.logger.error do
|
|
470
|
+
"reliable_push: #{left} buffered job(s) could not reach Redis before exit and are lost"
|
|
471
|
+
end
|
|
472
|
+
rescue StandardError
|
|
473
|
+
nil
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Leaves the circuit open on the first failed replay and closed once the
|
|
477
|
+
# backlog for this Redis has run out.
|
|
478
|
+
def replay_all(client, target)
|
|
479
|
+
drained = 0
|
|
480
|
+
while (entry = take_next(target))
|
|
481
|
+
unless replay(client, entry)
|
|
482
|
+
trip!(target)
|
|
483
|
+
return drained
|
|
484
|
+
end
|
|
485
|
+
drained += 1
|
|
486
|
+
end
|
|
487
|
+
circuit.close(target)
|
|
488
|
+
drained
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# An app's own drainer keeps its interval; only the absence of one makes
|
|
492
|
+
# this implicit, which is what stops `reset_after_fork!` re-arming it in
|
|
493
|
+
# children that never pushed.
|
|
494
|
+
def ensure_drainer
|
|
495
|
+
install_mutex.synchronize do
|
|
496
|
+
next if @drainer&.running?
|
|
497
|
+
|
|
498
|
+
@implicit_drainer = @drainer.nil?
|
|
499
|
+
@drainer ||= Drainer.new
|
|
500
|
+
@drainer.start
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
328
504
|
# The oldest entry bound for `target`, removed from the buffer.
|
|
329
505
|
def take_next(target)
|
|
330
506
|
buffer_mutex.synchronize do
|
|
331
507
|
index = buffer.index { |entry| entry.origin.resolve.equal?(target) }
|
|
332
|
-
|
|
508
|
+
next unless index
|
|
509
|
+
|
|
510
|
+
@replaying += 1
|
|
511
|
+
buffer.delete_at(index)
|
|
333
512
|
end
|
|
334
513
|
end
|
|
335
514
|
|
|
515
|
+
def replay_settled
|
|
516
|
+
buffer_mutex.synchronize { @replaying = [@replaying - 1, 0].max }
|
|
517
|
+
end
|
|
518
|
+
|
|
336
519
|
def buffered_pools
|
|
337
520
|
buffer_mutex.synchronize { buffer.map { |entry| entry.origin.resolve } }.uniq(&:object_id)
|
|
338
521
|
end
|
|
@@ -348,8 +531,10 @@ module Wurk
|
|
|
348
531
|
# Non-connection failures (OOM, LOADING, READONLY…) must not drop
|
|
349
532
|
# the taken payload — restore it before propagating, or a
|
|
350
533
|
# recovering-but-not-ready Redis silently eats one buffered job per
|
|
351
|
-
# drain tick.
|
|
534
|
+
# drain tick. Redis answered, so the circuit closes and pushes go back
|
|
535
|
+
# to surfacing the refusal to their callers.
|
|
352
536
|
restore(entry)
|
|
537
|
+
circuit.close(client.send(:pool))
|
|
353
538
|
raise
|
|
354
539
|
end
|
|
355
540
|
unless replayed
|
|
@@ -360,6 +545,8 @@ module Wurk
|
|
|
360
545
|
client.send(:emit_enqueued, [entry.payload])
|
|
361
546
|
Wurk::Metrics::Statsd.increment('jobs.recovered.push')
|
|
362
547
|
entry
|
|
548
|
+
ensure
|
|
549
|
+
replay_settled
|
|
363
550
|
end
|
|
364
551
|
|
|
365
552
|
def restore(entry)
|
|
@@ -450,8 +637,10 @@ module Wurk
|
|
|
450
637
|
# racing first pushes different logs.
|
|
451
638
|
@drops = DropLog.new
|
|
452
639
|
|
|
453
|
-
# Background drain thread
|
|
454
|
-
#
|
|
640
|
+
# Background drain thread and the circuit's only prober. Started on the first
|
|
641
|
+
# failed push if the app did not start one; wakes every `interval` seconds and
|
|
642
|
+
# runs `Buffered.drain_all!`, which costs no Redis round trip while the buffer
|
|
643
|
+
# is empty and no circuit is open. drain! already short-circuits on the first
|
|
455
644
|
# transient failure, so a still-down Redis just leaves its entries alone
|
|
456
645
|
# for this tick — no exponential backoff or explicit "reconnect
|
|
457
646
|
# detection" needed; the inner connection retry already lives inside
|
|
@@ -459,6 +648,9 @@ module Wurk
|
|
|
459
648
|
class Drainer
|
|
460
649
|
DEFAULT_INTERVAL = 2.0
|
|
461
650
|
STOP_JOIN_TIMEOUT = 5.0
|
|
651
|
+
# While a circuit is open, pushes are only in memory, so finding out that
|
|
652
|
+
# Redis is back quickly matters more than the interval an idle drainer keeps.
|
|
653
|
+
PROBE_PAUSE = 0.25
|
|
462
654
|
|
|
463
655
|
# Read by `Buffered.reset_after_fork!` off the *inherited* drainer, to
|
|
464
656
|
# rebuild an equivalent one in the child without touching its lock.
|
|
@@ -530,7 +722,8 @@ module Wurk
|
|
|
530
722
|
# Mutex+ConditionVariable lets `stop` wake the thread immediately
|
|
531
723
|
# instead of waiting up to `interval` seconds for sleep to return.
|
|
532
724
|
def wait_interval
|
|
533
|
-
|
|
725
|
+
pause = Buffered.circuit.any_open? ? [@interval, PROBE_PAUSE].min : @interval
|
|
726
|
+
@lock.synchronize { @wake.wait(@lock, pause) unless @done }
|
|
534
727
|
end
|
|
535
728
|
end
|
|
536
729
|
|
|
@@ -540,12 +733,12 @@ module Wurk
|
|
|
540
733
|
# (ConnectionPool::TimeoutError) — and buffers non-batched payloads.
|
|
541
734
|
module InstanceMethods
|
|
542
735
|
def push(item)
|
|
543
|
-
Buffered.drain!(self)
|
|
736
|
+
Buffered.drain!(self) unless Buffered.unbuffered?
|
|
544
737
|
super
|
|
545
738
|
end
|
|
546
739
|
|
|
547
740
|
def push_bulk(items)
|
|
548
|
-
Buffered.drain!(self)
|
|
741
|
+
Buffered.drain!(self) unless Buffered.unbuffered?
|
|
549
742
|
super
|
|
550
743
|
end
|
|
551
744
|
|
|
@@ -555,21 +748,42 @@ module Wurk
|
|
|
555
748
|
# than in Client because this rescue is its only reader: a Client
|
|
556
749
|
# without reliable_push! never allocates it.
|
|
557
750
|
def raw_push(payloads)
|
|
751
|
+
return buffer_without_trying(payloads) if circuit_open_for?(payloads)
|
|
752
|
+
|
|
558
753
|
Thread.current[DELIVERED_KEY] = []
|
|
559
754
|
super
|
|
560
755
|
rescue RedisClient::ConnectionError, ConnectionPool::TimeoutError
|
|
561
|
-
raise if Thread.current[Buffered::DRAINING_KEY]
|
|
756
|
+
raise if Thread.current[Buffered::DRAINING_KEY] || Buffered.unbuffered?
|
|
562
757
|
|
|
758
|
+
buffer_undelivered(payloads)
|
|
759
|
+
ensure
|
|
760
|
+
Thread.current[DELIVERED_KEY] = nil
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
# Runs inside raw_push's rescue, so a bare `raise` is its connection error.
|
|
764
|
+
# Client#push subtracts the returned payloads from the enqueued metric: they
|
|
765
|
+
# are in the buffer, not in Redis. Whatever the group did deliver stays out
|
|
766
|
+
# of the set and still counts.
|
|
767
|
+
def buffer_undelivered(payloads)
|
|
768
|
+
Buffered.trip!(pool)
|
|
563
769
|
bidless, batched = undelivered(payloads).partition { |p| !p['bid'] }
|
|
564
770
|
enbuffer_bidless(bidless, batched)
|
|
565
771
|
raise unless batched.empty?
|
|
566
772
|
|
|
567
|
-
# Client#push subtracts these from the enqueued metric: they are in
|
|
568
|
-
# the buffer, not in Redis. Whatever the group did deliver stays out
|
|
569
|
-
# of the set and still counts.
|
|
570
773
|
bidless
|
|
571
|
-
|
|
572
|
-
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
# Batched payloads never buffer, so they still take the real attempt and
|
|
777
|
+
# report its outcome to the caller.
|
|
778
|
+
def circuit_open_for?(payloads)
|
|
779
|
+
return false if Thread.current[Buffered::DRAINING_KEY] || Buffered.unbuffered?
|
|
780
|
+
|
|
781
|
+
Buffered.circuit.open?(pool) && payloads.none? { |p| p['bid'] }
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
def buffer_without_trying(payloads)
|
|
785
|
+
Buffered.enbuffer(payloads, client: self)
|
|
786
|
+
payloads
|
|
573
787
|
end
|
|
574
788
|
|
|
575
789
|
# The payloads this push is not known to have written. A push spanning
|
data/lib/wurk/compat.rb
CHANGED
|
@@ -113,6 +113,11 @@ module Sidekiq
|
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
module RailsNamespace
|
|
117
|
+
Reloader = Wurk::RailsReloader
|
|
118
|
+
end
|
|
119
|
+
private_constant :RailsNamespace
|
|
120
|
+
|
|
116
121
|
BasicFetch = Wurk::Fetcher::Reliable
|
|
117
122
|
Batch = Wurk::Batch
|
|
118
123
|
BatchSet = Wurk::BatchSet
|
|
@@ -224,6 +229,12 @@ module Sidekiq
|
|
|
224
229
|
Wurk.default_job_options = hash
|
|
225
230
|
end
|
|
226
231
|
|
|
232
|
+
# Answered from const_missing rather than defined as a constant, which would
|
|
233
|
+
# shadow ::Rails for every bare `Rails.logger` written inside `module Sidekiq`.
|
|
234
|
+
def const_missing(name)
|
|
235
|
+
name == :Rails ? RailsNamespace : super
|
|
236
|
+
end
|
|
237
|
+
|
|
227
238
|
def strict_args!(mode = :raise) = Wurk.strict_args!(mode)
|
|
228
239
|
def transactional_push! = Wurk.transactional_push!
|
|
229
240
|
def testing!(mode = :fake, &) = Wurk.testing!(mode, &)
|