wurk 1.7.5 → 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 +9 -6
- 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 +53 -14
- 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
|
@@ -7,6 +7,7 @@ require_relative '../keys'
|
|
|
7
7
|
require_relative '../lua'
|
|
8
8
|
require_relative '../fetcher'
|
|
9
9
|
require_relative 'capped'
|
|
10
|
+
require_relative 'many_queues'
|
|
10
11
|
|
|
11
12
|
module Wurk
|
|
12
13
|
class Fetcher
|
|
@@ -23,10 +24,13 @@ module Wurk
|
|
|
23
24
|
# the paths that must send a held ACK before they stop fetching, and
|
|
24
25
|
# docs/idea/parity-divergences.md for the window that widens.
|
|
25
26
|
#
|
|
26
|
-
# Priority handling:
|
|
27
|
-
# LMOVE
|
|
28
|
-
#
|
|
29
|
-
#
|
|
27
|
+
# Priority handling: walk queues_cmd in order with a non-blocking claim —
|
|
28
|
+
# an LMOVE on the first queue, then one fetch_first script call over all
|
|
29
|
+
# the others (ManyQueues) — then fall back to a blocking BLMOVE
|
|
30
|
+
# on the first queue so an empty poll doesn't spin Redis. BLMOVE has no
|
|
31
|
+
# multi-key form, so blocking on a single queue is the best Redis gives us:
|
|
32
|
+
# a job landing on a later queue while every thread is parked waits for
|
|
33
|
+
# the next pass, at most one block timeout. The block
|
|
30
34
|
# timeout defaults to TIMEOUT (2s) and is overridable per the Pro
|
|
31
35
|
# super_fetch §3.3 `config.fetch_poll_interval` knob.
|
|
32
36
|
#
|
|
@@ -40,6 +44,7 @@ module Wurk
|
|
|
40
44
|
# that caps nothing, which is a stronger guarantee that the unconfigured
|
|
41
45
|
# path is unchanged than a second loop kept in step by hand.
|
|
42
46
|
prepend Capped
|
|
47
|
+
include ManyQueues
|
|
43
48
|
|
|
44
49
|
# Default BLMOVE block timeout; overridable via config.fetch_poll_interval.
|
|
45
50
|
TIMEOUT = 2
|
|
@@ -72,7 +77,7 @@ module Wurk
|
|
|
72
77
|
# namespaces are in play: a restarted container reuses both, so the
|
|
73
78
|
# reaper's `kill(0)` liveness check would read a dead owner's list as
|
|
74
79
|
# live (jobs stranded) or a live owner's as dead (job run twice). Keys
|
|
75
|
-
# written before the nonce existed stay reclaimable —
|
|
80
|
+
# written before the nonce existed stay reclaimable — PrivateListKey
|
|
76
81
|
# accepts both shapes.
|
|
77
82
|
def self.private_queue_name(public_queue, index = 0)
|
|
78
83
|
host = ENV['DYNO'] || Socket.gethostname
|
|
@@ -112,6 +117,7 @@ module Wurk
|
|
|
112
117
|
@queue_keys_pid = ::Process.pid
|
|
113
118
|
@prefixed_queues = nil
|
|
114
119
|
@prefixed_source = nil
|
|
120
|
+
@claim_keys = nil
|
|
115
121
|
end
|
|
116
122
|
|
|
117
123
|
# Take custody of a finished job's LREM instead of sending it now. One
|
|
@@ -246,20 +252,20 @@ module Wurk
|
|
|
246
252
|
config.config.handle_exception(ex, ctx)
|
|
247
253
|
end
|
|
248
254
|
|
|
249
|
-
# Non-blocking pass over the fetchable queues
|
|
250
|
-
#
|
|
251
|
-
#
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
+
# Non-blocking pass over the fetchable queues, then the blocking
|
|
256
|
+
# fall-through. The pending ACK rides the first LMOVE and only the first.
|
|
257
|
+
# By the time we fall through to BLMOVE this thread is holding nothing —
|
|
258
|
+
# which is the point, since a blocking call can't join a pipeline and
|
|
259
|
+
# would strand the ACK for a whole poll interval.
|
|
260
|
+
#
|
|
261
|
+
# The first queue is always a bare LMOVE, so the busy-first-queue steady
|
|
262
|
+
# state (and the one-queue capsule `rake bench` gates) costs exactly what
|
|
263
|
+
# it did. Only a miss there pays for the rest — see ManyQueues.
|
|
255
264
|
def walk(queues)
|
|
256
265
|
ack = take_pending_ack
|
|
257
|
-
queues.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return uow if uow
|
|
261
|
-
end
|
|
262
|
-
blmove(queues.first)
|
|
266
|
+
uow = lmove(queues.first, ack)
|
|
267
|
+
uow ||= claim_rest(queues) if queues.size > 1
|
|
268
|
+
uow || blmove(queues.first)
|
|
263
269
|
end
|
|
264
270
|
|
|
265
271
|
# Exactly one, even when a failed flush left this thread holding two: the
|
|
@@ -98,8 +98,26 @@ module Wurk
|
|
|
98
98
|
config.redis(idempotent: true) { |conn| conn.call('ZREM', slot_key, slot_token) }
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
# Puts a claimed job back without running it — the public half of the
|
|
102
|
+
# Sidekiq UnitOfWork contract that throttling/rate-limiting gems call
|
|
103
|
+
# (sidekiq-throttled's requeue path, anything holding a BasicFetch UoW,
|
|
104
|
+
# which is this one under the `Sidekiq::BasicFetch` alias). Sidekiq's
|
|
105
|
+
# BasicFetch has no private list, so its bare RPUSH is the whole move;
|
|
106
|
+
# here the private copy has to go in the same step, or the job sits in
|
|
107
|
+
# both lists and runs twice — once from the public queue, once when the
|
|
108
|
+
# reaper reclaims this process's private list.
|
|
109
|
+
#
|
|
110
|
+
# LREM then RPUSH in one MULTI, unconditionally: unlike bulk_requeue
|
|
111
|
+
# (which races a Processor ACKing the same UoW and so guards the push on
|
|
112
|
+
# the LREM), the caller here owns the unit, and a job whose ACK already
|
|
113
|
+
# went out must still be re-queued rather than dropped.
|
|
101
114
|
def requeue
|
|
102
|
-
config.redis
|
|
115
|
+
config.redis do |conn|
|
|
116
|
+
conn.multi do |tx|
|
|
117
|
+
tx.call('LREM', private_queue, LREM_COUNT, job)
|
|
118
|
+
tx.call('RPUSH', queue, job)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
103
121
|
end
|
|
104
122
|
end
|
|
105
123
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../keys'
|
|
4
|
+
|
|
5
|
+
module Wurk
|
|
6
|
+
class Fetcher
|
|
7
|
+
# The Reaper's report on a `queue:*|*` key that is not a private list it
|
|
8
|
+
# can read the owner of — a Sidekiq Pro super_fetch list in a shape Wurk
|
|
9
|
+
# does not write, say, left behind by a live migration. Nothing will ever
|
|
10
|
+
# reclaim its jobs, so it is said once per key per process (WARN, with the
|
|
11
|
+
# list's length) rather than skipped in silence on every sweep.
|
|
12
|
+
#
|
|
13
|
+
# A public queue whose own name contains `|` matches the same SCAN pattern,
|
|
14
|
+
# so a key whose name is in the `queues` SET is not reported.
|
|
15
|
+
class UnparseableKeys
|
|
16
|
+
# Distinct keys remembered as already reported. Bounded so a keyspace
|
|
17
|
+
# full of odd keys cannot grow it without limit; past the bound the
|
|
18
|
+
# memory is cleared and keys are reported again.
|
|
19
|
+
MEMORY = 1000
|
|
20
|
+
|
|
21
|
+
def initialize(config)
|
|
22
|
+
@config = config
|
|
23
|
+
@reported = ::Set.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def report(key)
|
|
27
|
+
return if @reported.include?(key)
|
|
28
|
+
|
|
29
|
+
public_queue, size = inspect_key(key)
|
|
30
|
+
return if public_queue
|
|
31
|
+
|
|
32
|
+
@reported.clear if @reported.size >= MEMORY
|
|
33
|
+
@reported << key
|
|
34
|
+
@config.logger.warn do
|
|
35
|
+
"reaper: #{key.inspect} looks like a reliable-fetch private list but does not match " \
|
|
36
|
+
"queue:<queue>|<host>|<pid>|<nonce>|<index>; its #{size} job(s) will not be recovered " \
|
|
37
|
+
'automatically. Inspect it and LMOVE them back onto their queue by hand if they belong to a dead process.'
|
|
38
|
+
end
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
@config.handle_exception(e, context: 'wurk-reaper')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def inspect_key(key)
|
|
46
|
+
member, size = @config.redis(idempotent: true) do |conn|
|
|
47
|
+
conn.pipelined do |pipe|
|
|
48
|
+
pipe.call('SISMEMBER', Keys::QUEUES_SET, key.delete_prefix(Keys::QUEUE_PREFIX))
|
|
49
|
+
pipe.call('LLEN', key)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
[member == 1, size]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/wurk/flow/completion.rb
CHANGED
|
@@ -59,19 +59,44 @@ module Wurk
|
|
|
59
59
|
# spent: a replay after a lost reply finds the node already out of the
|
|
60
60
|
# state it claims from and writes nothing, so the pool may retry a
|
|
61
61
|
# connection error rather than give up on a callback that fires once.
|
|
62
|
+
#
|
|
63
|
+
# The graph around the node is read first so the script is handed every
|
|
64
|
+
# key it may write as a declared key (Redis Cluster and Dragonfly refuse
|
|
65
|
+
# one built inside Lua). Safe to read ahead: a node's jid, its
|
|
66
|
+
# dependents, and their bids and queues are all fixed at creation; the
|
|
67
|
+
# counters and states the decision turns on are read inside the script.
|
|
62
68
|
def advance(fid, index)
|
|
63
69
|
Wurk.redis(idempotent: true) do |conn|
|
|
70
|
+
node_key = Keys.flow_node(fid, index)
|
|
71
|
+
jid, dependents = conn.call('HMGET', node_key, 'jid', 'dependents')
|
|
72
|
+
deps = dependents ? Wurk.load_json(dependents) : []
|
|
64
73
|
Wurk::Lua::Loader.eval_cached(
|
|
65
74
|
conn, :flow_advance,
|
|
66
|
-
keys: [Keys.flow(fid), 'queues'
|
|
67
|
-
|
|
75
|
+
keys: [Keys.flow(fid), 'queues', Keys::SCHEDULE, Keys.flow_dead(fid), node_key,
|
|
76
|
+
Keys.status(jid.to_s), *dependent_keys(conn, fid, deps)],
|
|
77
|
+
argv: [index, now_seconds, now_millis, *deps.map(&:to_s)]
|
|
68
78
|
)
|
|
69
79
|
end
|
|
70
80
|
end
|
|
71
81
|
|
|
82
|
+
# Four keys per dependent, in `deps` order: its record, its batch, the
|
|
83
|
+
# batch's live-jid set, and the queue it is released onto.
|
|
84
|
+
def dependent_keys(conn, fid, deps)
|
|
85
|
+
return [] if deps.empty?
|
|
86
|
+
|
|
87
|
+
rows = conn.pipelined { |pipe| deps.each { |d| pipe.call('HMGET', Keys.flow_node(fid, d), 'bid', 'queue') } }
|
|
88
|
+
deps.zip(rows).flat_map do |d, (bid, queue)|
|
|
89
|
+
[Keys.flow_node(fid, d), "b-#{bid}", "b-#{bid}-jids", Keys.queue(queue)]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
72
93
|
def mark_dead(fid, index)
|
|
73
94
|
Wurk.redis(idempotent: true) do |conn|
|
|
74
|
-
Wurk::Lua::Loader.eval_cached(
|
|
95
|
+
Wurk::Lua::Loader.eval_cached(
|
|
96
|
+
conn, :flow_fail,
|
|
97
|
+
keys: [Keys.flow(fid), Keys.flow_node(fid, index), Keys.flow_dead(fid)],
|
|
98
|
+
argv: [index, now_seconds]
|
|
99
|
+
)
|
|
75
100
|
end
|
|
76
101
|
end
|
|
77
102
|
|
data/lib/wurk/flow/creation.rb
CHANGED
|
@@ -160,13 +160,24 @@ module Wurk
|
|
|
160
160
|
# pool may retry a connection error that a non-idempotent write would have
|
|
161
161
|
# to give up on.
|
|
162
162
|
def write(payloads)
|
|
163
|
-
keys = [Keys.flow(@flow.fid), Keys::FLOWS_SET, 'batches', 'queues']
|
|
163
|
+
keys = [Keys.flow(@flow.fid), Keys::FLOWS_SET, 'batches', 'queues', Keys::SCHEDULE, *node_keys(payloads)]
|
|
164
164
|
argv = script_argv(payloads)
|
|
165
165
|
Wurk.redis(idempotent: true) do |conn|
|
|
166
166
|
Wurk::Lua::Loader.eval_cached(conn, :flow_create, keys: keys, argv: argv)
|
|
167
167
|
end
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
+
# Four declared keys per node, in the same topological order as the
|
|
171
|
+
# envelopes in {#script_argv}: its record, its batch, the batch's live-jid
|
|
172
|
+
# set, and its queue.
|
|
173
|
+
def node_keys(payloads)
|
|
174
|
+
@flow.nodes.flat_map do |node|
|
|
175
|
+
payload = payloads[node.index]
|
|
176
|
+
bid = payload['bid']
|
|
177
|
+
[Keys.flow_node(@flow.fid, node.index), "b-#{bid}", "b-#{bid}-jids", Keys.queue(payload['queue'])]
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
170
181
|
def script_argv(payloads)
|
|
171
182
|
now = ::Process.clock_gettime(::Process::CLOCK_REALTIME)
|
|
172
183
|
cutoff, rank = Wurk::Batch.trim_bounds
|
|
@@ -185,19 +196,28 @@ module Wurk
|
|
|
185
196
|
# `enqueued_at` marks arrival on an immediate queue, so only the nodes
|
|
186
197
|
# this write actually queues carry one. A waiting node's stored payload
|
|
187
198
|
# gets its stamp from the push that releases it.
|
|
199
|
+
#
|
|
200
|
+
# A node declared with `at:` is deferred, root or not, so its payload is
|
|
201
|
+
# stored as the `schedule` member Client#push would write — `at` is the
|
|
202
|
+
# score, and the promoter stamps `enqueued_at` — and the epoch rides
|
|
203
|
+
# beside it for whichever script releases the node.
|
|
188
204
|
def envelope(node, payload)
|
|
189
|
-
payload['enqueued_at'] = now_in_millis if node.root?
|
|
205
|
+
payload['enqueued_at'] = now_in_millis if node.root? && !payload.key?('at')
|
|
190
206
|
Wurk.dump_json(node_fields(node, payload).merge(edge_fields(node)))
|
|
191
207
|
end
|
|
192
208
|
|
|
193
209
|
def node_fields(node, payload)
|
|
194
|
-
json =
|
|
210
|
+
json = stored_payload(payload)
|
|
195
211
|
{ 'i' => node.index.to_s, 'name' => node.name.to_s, 'class' => payload['class'],
|
|
196
212
|
'queue' => payload['queue'], 'jid' => payload['jid'], 'bid' => payload['bid'],
|
|
197
|
-
'state' => node.root? ? ENQUEUED : WAITING, 'payload' => json,
|
|
213
|
+
'state' => node.root? ? ENQUEUED : WAITING, 'payload' => json, 'at' => payload['at'].to_s,
|
|
198
214
|
'desc' => node.label, 'cb' => callbacks_json(node), 'pipe' => pipe_field(node, json) }
|
|
199
215
|
end
|
|
200
216
|
|
|
217
|
+
def stored_payload(payload)
|
|
218
|
+
payload.key?('at') ? JobUtil.scheduled_member(payload) : Wurk.dump_json(payload)
|
|
219
|
+
end
|
|
220
|
+
|
|
201
221
|
# Empty for an ordinary node, and the sentinel — exactly as it appears in
|
|
202
222
|
# the stored bytes — for a piped one, which is both the flag the release
|
|
203
223
|
# tests and the needle it splices at.
|
data/lib/wurk/flow.rb
CHANGED
|
@@ -179,14 +179,36 @@ module Wurk
|
|
|
179
179
|
def abandon(fid) # rubocop:disable Naming/PredicateMethod
|
|
180
180
|
now = ::Process.clock_gettime(::Process::CLOCK_REALTIME).to_s
|
|
181
181
|
released = Wurk.redis(idempotent: true) do |conn|
|
|
182
|
+
bids = node_bids(conn, fid)
|
|
182
183
|
Wurk::Lua::Loader.eval_cached(
|
|
183
184
|
conn, :flow_abandon,
|
|
184
|
-
keys: [Keys.flow(fid), 'batches', 'dead-batches'],
|
|
185
|
-
argv: [now, *
|
|
185
|
+
keys: [Keys.flow(fid), 'batches', 'dead-batches', *abandoned_keys(fid, bids)],
|
|
186
|
+
argv: [now, *bids.compact]
|
|
186
187
|
)
|
|
187
188
|
end
|
|
188
189
|
released.to_i >= 0
|
|
189
190
|
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
# Every node's bid, in index order, nil where a record is gone. Read ahead
|
|
195
|
+
# of the script so it can be handed declared keys: a node's bid is fixed at
|
|
196
|
+
# creation, and the script's own state claim still decides whether
|
|
197
|
+
# anything is released.
|
|
198
|
+
def node_bids(conn, fid)
|
|
199
|
+
total = conn.call('HGET', Keys.flow(fid), 'total').to_i
|
|
200
|
+
return [] if total.zero?
|
|
201
|
+
|
|
202
|
+
conn.pipelined { |pipe| total.times { |i| pipe.call('HGET', Keys.flow_node(fid, i), 'bid') } }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def abandoned_keys(fid, bids)
|
|
206
|
+
keys = bids.each_with_index.flat_map do |bid, i|
|
|
207
|
+
batch = bid ? ["b-#{bid}", *Wurk::Batch::KEY_SUFFIXES.map { |suffix| "b-#{bid}-#{suffix}" }] : []
|
|
208
|
+
[Keys.flow_node(fid, i), *batch]
|
|
209
|
+
end
|
|
210
|
+
keys << Keys.flow_dead(fid)
|
|
211
|
+
end
|
|
190
212
|
end
|
|
191
213
|
|
|
192
214
|
def initialize(&block)
|
data/lib/wurk/health.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'socket'
|
|
4
4
|
require 'json'
|
|
5
|
+
require_relative 'metrics/prometheus'
|
|
5
6
|
|
|
6
7
|
module Wurk
|
|
7
8
|
# Thin HTTP listener for k8s liveness/readiness probes. Optional, off by
|
|
@@ -10,10 +11,16 @@ module Wurk
|
|
|
10
11
|
# Endpoints:
|
|
11
12
|
# * GET /live → 200 while the Launcher is running (not in quiet/stop).
|
|
12
13
|
# * GET /ready → 200 only when Redis is reachable AND the heartbeat has
|
|
13
|
-
# fired within `ready_window` seconds
|
|
14
|
+
# fired within `ready_window` seconds AND, inside a swarm
|
|
15
|
+
# child, enough of the swarm's children have (see
|
|
16
|
+
# Health::Fleet). 503 otherwise.
|
|
17
|
+
# * GET /metrics → Prometheus text exposition (Wurk::Metrics::Prometheus).
|
|
18
|
+
# On whenever the listener is; `metrics: false` in
|
|
19
|
+
# `:health_check_options` turns it into a 404.
|
|
14
20
|
# Anything else returns 404 JSON.
|
|
15
21
|
#
|
|
16
|
-
# The server uses a raw TCPServer
|
|
22
|
+
# The server uses a raw TCPServer, one accept thread and a short-lived
|
|
23
|
+
# thread per connection (bounded, deadline-capped). No Rack, no
|
|
17
24
|
# dependencies — it lives inside every worker process where Rails may or
|
|
18
25
|
# may not exist (standalone CLI, Embedded, swarm child). Bound to a
|
|
19
26
|
# dedicated port so it does not collide with the host application's HTTP.
|
|
@@ -23,6 +30,19 @@ module Wurk
|
|
|
23
30
|
DEFAULT_PORT = 7433
|
|
24
31
|
DEFAULT_BIND = '0.0.0.0'
|
|
25
32
|
DEFAULT_READY_WINDOW = 30
|
|
33
|
+
JSON_TYPE = 'application/json'
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# Children the swarm was configured with, set by Swarm::ChildBoot inside
|
|
37
|
+
# each child (inherited from nowhere: the parent never runs a listener).
|
|
38
|
+
# nil outside a swarm child — standalone and embedded processes are a
|
|
39
|
+
# fleet of one and /ready judges them on their own heartbeat alone.
|
|
40
|
+
attr_accessor :fleet_size
|
|
41
|
+
|
|
42
|
+
def json(status, **extra)
|
|
43
|
+
::JSON.generate({ status: status }.merge(extra))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
26
46
|
|
|
27
47
|
# The HTTP listener. Owns one TCPServer + one accept thread. Idempotent
|
|
28
48
|
# start/stop; safe to call from Launcher#run / Launcher#stop.
|
|
@@ -32,6 +52,13 @@ module Wurk
|
|
|
32
52
|
# that probes come back quickly after the owner dies, long enough not to
|
|
33
53
|
# spin. See #start_retry_loop.
|
|
34
54
|
RETRY_INTERVAL = 5
|
|
55
|
+
# Overall budget for one request head. A kubelet probe sends its head in
|
|
56
|
+
# one segment; anything slower is broken or hostile.
|
|
57
|
+
REQUEST_TIMEOUT = 1.0
|
|
58
|
+
MAX_REQUEST_BYTES = 8192
|
|
59
|
+
MAX_CONNECTIONS = 16
|
|
60
|
+
REASONS = { 200 => 'OK', 404 => 'Not Found', 405 => 'Method Not Allowed',
|
|
61
|
+
503 => 'Service Unavailable' }.freeze
|
|
35
62
|
|
|
36
63
|
attr_reader :port, :bind
|
|
37
64
|
|
|
@@ -47,6 +74,9 @@ module Wurk
|
|
|
47
74
|
@thread = nil
|
|
48
75
|
@retry_thread = nil
|
|
49
76
|
@done = false
|
|
77
|
+
@slots = ::Mutex.new
|
|
78
|
+
@connections = 0
|
|
79
|
+
@fleet = nil
|
|
50
80
|
end
|
|
51
81
|
|
|
52
82
|
def start
|
|
@@ -133,7 +163,7 @@ module Wurk
|
|
|
133
163
|
|
|
134
164
|
begin
|
|
135
165
|
client, _addr = @server.accept_nonblock(exception: false)
|
|
136
|
-
|
|
166
|
+
dispatch(client) if client
|
|
137
167
|
rescue ::IO::WaitReadable
|
|
138
168
|
next
|
|
139
169
|
rescue ::StandardError => e
|
|
@@ -145,41 +175,105 @@ module Wurk
|
|
|
145
175
|
# Server was closed during shutdown — expected.
|
|
146
176
|
end
|
|
147
177
|
|
|
148
|
-
|
|
149
|
-
|
|
178
|
+
# Each connection gets its own short-lived thread so a client that
|
|
179
|
+
# dribbles bytes (slowloris) cannot hold the accept loop — and with it
|
|
180
|
+
# every kubelet probe — hostage. Past MAX_CONNECTIONS the socket is
|
|
181
|
+
# closed unanswered rather than queued: a probe that loses that race
|
|
182
|
+
# retries, an attacker gains nothing.
|
|
183
|
+
def dispatch(client)
|
|
184
|
+
return client.close unless claim_slot
|
|
185
|
+
|
|
186
|
+
spawn_handler(client)
|
|
187
|
+
rescue ::ThreadError
|
|
188
|
+
release_slot
|
|
189
|
+
client.close
|
|
190
|
+
end
|
|
150
191
|
|
|
151
|
-
|
|
152
|
-
|
|
192
|
+
def spawn_handler(client)
|
|
193
|
+
::Thread.new(client) do |sock|
|
|
194
|
+
::Thread.current.name = 'wurk-health-conn'
|
|
195
|
+
::Thread.current.report_on_exception = false
|
|
196
|
+
handle(sock)
|
|
197
|
+
ensure
|
|
198
|
+
release_slot
|
|
199
|
+
end
|
|
200
|
+
end
|
|
153
201
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
line = client.gets("\r\n")
|
|
162
|
-
break if line.nil? || line == "\r\n"
|
|
202
|
+
def claim_slot
|
|
203
|
+
@slots.synchronize do
|
|
204
|
+
return false if @connections >= MAX_CONNECTIONS
|
|
205
|
+
|
|
206
|
+
@connections += 1
|
|
207
|
+
true
|
|
163
208
|
end
|
|
209
|
+
end
|
|
164
210
|
|
|
165
|
-
|
|
166
|
-
|
|
211
|
+
def release_slot
|
|
212
|
+
@slots.synchronize { @connections -= 1 }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def handle(client)
|
|
216
|
+
request_line = read_request_line(client)
|
|
217
|
+
return if request_line.nil?
|
|
218
|
+
|
|
219
|
+
method, path, = request_line.strip.split(' ', 3)
|
|
220
|
+
body, status, type = response_for(method, path)
|
|
221
|
+
write_response(client, status, body, type || JSON_TYPE)
|
|
167
222
|
rescue ::StandardError => e
|
|
168
223
|
logger&.error { "Wurk::Health request: #{e.class}: #{e.message}" }
|
|
169
224
|
ensure
|
|
170
225
|
client&.close
|
|
171
226
|
end
|
|
172
227
|
|
|
228
|
+
# Reads the head under one overall deadline and a MAX_REQUEST_BYTES cap.
|
|
229
|
+
# Headers are read only to drain them (closing a socket with unread bytes
|
|
230
|
+
# makes the kernel answer RST, which can eat the response); the answer
|
|
231
|
+
# depends on the request line alone, so a client that sends that line
|
|
232
|
+
# and then stalls is still answered when the deadline expires.
|
|
233
|
+
def read_request_line(client)
|
|
234
|
+
buffer = +''
|
|
235
|
+
deadline = monotonic + REQUEST_TIMEOUT
|
|
236
|
+
until buffer.include?("\r\n\r\n") || buffer.bytesize >= MAX_REQUEST_BYTES
|
|
237
|
+
chunk = read_chunk(client, MAX_REQUEST_BYTES - buffer.bytesize, deadline)
|
|
238
|
+
break unless chunk
|
|
239
|
+
|
|
240
|
+
buffer << chunk
|
|
241
|
+
end
|
|
242
|
+
buffer[/\A[^\r\n]*(?=\r\n)/]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# nil once the deadline passes or the peer closes.
|
|
246
|
+
def read_chunk(client, max_bytes, deadline)
|
|
247
|
+
remaining = deadline - monotonic
|
|
248
|
+
return unless remaining.positive? && client.wait_readable(remaining)
|
|
249
|
+
|
|
250
|
+
chunk = client.read_nonblock(max_bytes, exception: false)
|
|
251
|
+
chunk == :wait_readable ? '' : chunk
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def monotonic
|
|
255
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
256
|
+
end
|
|
257
|
+
|
|
173
258
|
def response_for(method, path)
|
|
174
259
|
return [json('error', message: 'method not allowed'), 405] unless method == 'GET'
|
|
175
260
|
|
|
176
261
|
case path
|
|
177
262
|
when '/live' then live_response
|
|
178
263
|
when '/ready' then ready_response
|
|
179
|
-
|
|
264
|
+
when '/metrics' then fleet.metrics_response || not_found(path)
|
|
265
|
+
else not_found(path)
|
|
180
266
|
end
|
|
181
267
|
end
|
|
182
268
|
|
|
269
|
+
def not_found(path)
|
|
270
|
+
[json('error', message: 'not found', path: path), 404]
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def fleet
|
|
274
|
+
@fleet ||= Fleet.new(@config, @ready_window)
|
|
275
|
+
end
|
|
276
|
+
|
|
183
277
|
def live_response
|
|
184
278
|
if @launcher.stopping?
|
|
185
279
|
[json('down', check: 'live', reason: 'stopping'), 503]
|
|
@@ -189,15 +283,10 @@ module Wurk
|
|
|
189
283
|
end
|
|
190
284
|
|
|
191
285
|
def ready_response
|
|
192
|
-
|
|
193
|
-
|
|
286
|
+
return [json('down', check: 'ready', reason: 'redis unreachable'), 503] unless ping_redis
|
|
287
|
+
return [json('down', check: 'ready', reason: 'heartbeat stale'), 503] unless heartbeat_fresh?
|
|
194
288
|
|
|
195
|
-
|
|
196
|
-
[json('ok', check: 'ready'), 200]
|
|
197
|
-
else
|
|
198
|
-
reason = redis_ok ? 'heartbeat stale' : 'redis unreachable'
|
|
199
|
-
[json('down', check: 'ready', reason: reason), 503]
|
|
200
|
-
end
|
|
289
|
+
fleet.ready_response
|
|
201
290
|
end
|
|
202
291
|
|
|
203
292
|
def ping_redis
|
|
@@ -216,22 +305,13 @@ module Wurk
|
|
|
216
305
|
(::Time.now.to_f - last) < @ready_window
|
|
217
306
|
end
|
|
218
307
|
|
|
219
|
-
def json(
|
|
220
|
-
::JSON.generate({ status: status }.merge(extra))
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def write_response(client, status, body)
|
|
224
|
-
reason = case status
|
|
225
|
-
when 200 then 'OK'
|
|
226
|
-
when 404 then 'Not Found'
|
|
227
|
-
when 405 then 'Method Not Allowed'
|
|
228
|
-
when 503 then 'Service Unavailable'
|
|
229
|
-
else 'Status'
|
|
230
|
-
end
|
|
308
|
+
def json(...) = Health.json(...)
|
|
231
309
|
|
|
310
|
+
def write_response(client, status, body, type = JSON_TYPE)
|
|
311
|
+
reason = REASONS.fetch(status, 'Status')
|
|
232
312
|
client.write(
|
|
233
313
|
"HTTP/1.1 #{status} #{reason}\r\n" \
|
|
234
|
-
"Content-Type:
|
|
314
|
+
"Content-Type: #{type}\r\n" \
|
|
235
315
|
"Content-Length: #{body.bytesize}\r\n" \
|
|
236
316
|
"Connection: close\r\n\r\n" \
|
|
237
317
|
"#{body}"
|
|
@@ -242,5 +322,67 @@ module Wurk
|
|
|
242
322
|
@config&.logger
|
|
243
323
|
end
|
|
244
324
|
end
|
|
325
|
+
|
|
326
|
+
# The swarm-wide half of the listener: the /metrics body and the fleet
|
|
327
|
+
# check behind /ready. One Prometheus collector serves both, so a scrape and
|
|
328
|
+
# a probe landing in the same second cost one Redis refresh between them.
|
|
329
|
+
#
|
|
330
|
+
# A swarm pod is one probe target however many children it runs, and the
|
|
331
|
+
# listener lives in whichever child won the port. Judging readiness on that
|
|
332
|
+
# child alone said "ready" while its siblings crash-looped. Ready now also
|
|
333
|
+
# needs `min_ready` children (default: half the fleet, rounded up) with a
|
|
334
|
+
# heartbeat inside `ready_window` — a majority-ish bar rather than all, so
|
|
335
|
+
# one slot in respawn backoff does not pull a pod that is still doing most
|
|
336
|
+
# of its work out of a rollout. /live stays local on purpose: failing it
|
|
337
|
+
# restarts the whole pod, and replacing one wedged child is the swarm
|
|
338
|
+
# parent's job (Swarm::Liveness).
|
|
339
|
+
class Fleet
|
|
340
|
+
def initialize(config, ready_window)
|
|
341
|
+
@config = config
|
|
342
|
+
@ready_window = ready_window
|
|
343
|
+
@collector = nil
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# [body, status, content type]; nil when `metrics: false`.
|
|
347
|
+
def metrics_response
|
|
348
|
+
return nil if option(:metrics) == false
|
|
349
|
+
|
|
350
|
+
body = collector.render(expected_children: Health.fleet_size, fresh_window: @ready_window)
|
|
351
|
+
[body, 200, Metrics::Prometheus::CONTENT_TYPE]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# The /ready answer once the local checks (Redis, own heartbeat) passed.
|
|
355
|
+
def ready_response
|
|
356
|
+
expected = Health.fleet_size
|
|
357
|
+
return [Health.json('ok', check: 'ready'), 200] unless expected
|
|
358
|
+
|
|
359
|
+
needed = min_ready(expected)
|
|
360
|
+
fresh = collector.fresh_local(collector.snapshot, @ready_window).size
|
|
361
|
+
if fresh >= needed
|
|
362
|
+
[Health.json('ok', check: 'ready', children: fresh, expected: expected), 200]
|
|
363
|
+
else
|
|
364
|
+
[Health.json('down', check: 'ready', reason: 'too few live children', children: fresh,
|
|
365
|
+
needed: needed, expected: expected), 503]
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
private
|
|
370
|
+
|
|
371
|
+
def collector
|
|
372
|
+
@collector ||= Metrics::Prometheus.new(@config)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def min_ready(expected)
|
|
376
|
+
configured = option(:min_ready)
|
|
377
|
+
return [Integer(configured), expected].min if configured
|
|
378
|
+
|
|
379
|
+
(expected / 2.0).ceil
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def option(key)
|
|
383
|
+
opts = @config.respond_to?(:[]) ? @config[:health_check_options] : nil
|
|
384
|
+
opts.is_a?(Hash) ? opts[key] : nil
|
|
385
|
+
end
|
|
386
|
+
end
|
|
245
387
|
end
|
|
246
388
|
end
|