cogworker 0.5.0 → 0.6.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/LICENSE.txt +21 -0
- data/README.md +608 -0
- data/lib/cogworker/basic_fetch.rb +2 -2
- data/lib/cogworker/best_effort.rb +23 -0
- data/lib/cogworker/cli.rb +3 -2
- data/lib/cogworker/client.rb +17 -4
- data/lib/cogworker/config.rb +17 -3
- data/lib/cogworker/deferred_releases.rb +46 -0
- data/lib/cogworker/heartbeat.rb +56 -12
- data/lib/cogworker/history/middleware.rb +9 -5
- data/lib/cogworker/job_util.rb +17 -0
- data/lib/cogworker/key_guard.rb +36 -1
- data/lib/cogworker/launcher.rb +4 -0
- data/lib/cogworker/lua_script.rb +3 -3
- data/lib/cogworker/manager.rb +19 -1
- data/lib/cogworker/periodic/claim.lua +5 -2
- data/lib/cogworker/periodic/manager.rb +5 -0
- data/lib/cogworker/periodic/release_middleware.rb +13 -2
- data/lib/cogworker/periodic/running_lock.rb +12 -0
- data/lib/cogworker/periodic/ticker.rb +59 -18
- data/lib/cogworker/process_set.rb +3 -0
- data/lib/cogworker/processor.rb +33 -9
- data/lib/cogworker/redis_errors.rb +93 -0
- data/lib/cogworker/redis_keys.rb +4 -0
- data/lib/cogworker/reliable_fetch.rb +155 -40
- data/lib/cogworker/scheduled.rb +15 -8
- data/lib/cogworker/status/server_middleware.rb +13 -7
- data/lib/cogworker/swarm.rb +2 -0
- data/lib/cogworker/unique_jobs/release_middleware.rb +16 -5
- data/lib/cogworker/version.rb +1 -1
- data/lib/cogworker/web/layout.rb +3 -1
- data/lib/cogworker/web/routes/jobs.rb +4 -4
- data/lib/cogworker/web/routes/overview.rb +4 -2
- data/lib/cogworker/work_set.rb +3 -0
- data/lib/cogworker.rb +1 -1
- metadata +6 -1
|
@@ -29,7 +29,7 @@ module Cogworker
|
|
|
29
29
|
# separate call made first, its failure (Redis down — the usual reason
|
|
30
30
|
# the push failed) skipped the rollback altogether.
|
|
31
31
|
ROLLBACK_SCRIPT = <<~LUA
|
|
32
|
-
if redis.
|
|
32
|
+
if redis.pcall("GET", KEYS[1]) == ARGV[1] then
|
|
33
33
|
if ARGV[2] == "" then
|
|
34
34
|
redis.call("SET", KEYS[1], tostring(tonumber(ARGV[1]) - 1))
|
|
35
35
|
else
|
|
@@ -46,14 +46,16 @@ module Cogworker
|
|
|
46
46
|
return 1
|
|
47
47
|
LUA
|
|
48
48
|
|
|
49
|
-
def initialize(manager, entries, catch_up: true, ready: -> { true })
|
|
49
|
+
def initialize(manager, entries, catch_up: true, replace: false, ready: -> { true })
|
|
50
50
|
@manager = manager
|
|
51
51
|
@entries = entries
|
|
52
52
|
@catch_up = catch_up
|
|
53
|
+
@replace = replace
|
|
53
54
|
@ready = ready
|
|
54
55
|
@last_checked_slot = {}
|
|
55
56
|
@cron_cache = {}
|
|
56
57
|
@pending_rollbacks = []
|
|
58
|
+
@blocked_since = {} # pjid => [slot, monotonic time it was first found blocked]
|
|
57
59
|
@failures = Hash.new(0) # pjid => consecutive failed ticks
|
|
58
60
|
@retry_at = {} # pjid => monotonic time it may be tried again
|
|
59
61
|
end
|
|
@@ -83,8 +85,12 @@ module Cogworker
|
|
|
83
85
|
# here (not at DSL-registration time in Config#periodic) so it never
|
|
84
86
|
# depends on `config.redis =` having already run — every process that
|
|
85
87
|
# boots re-writes the same idempotent entries regardless of ordering.
|
|
88
|
+
# With `replace` (Config#periodic(replace: true)) the hash is swapped
|
|
89
|
+
# out wholesale in one MULTI, dropping entries no longer registered
|
|
90
|
+
# (their `last_slot`/`disabled` state is left alone: an old process
|
|
91
|
+
# still ticking them mid-deploy must not re-fire an already-run slot).
|
|
86
92
|
def publish_schedule!
|
|
87
|
-
return if @entries.empty?
|
|
93
|
+
return if @entries.empty? && !@replace
|
|
88
94
|
|
|
89
95
|
payloads = @entries.each_with_object({}) do |entry, h|
|
|
90
96
|
h[entry.pjid] = JSON.generate(
|
|
@@ -92,7 +98,14 @@ module Cogworker
|
|
|
92
98
|
'unique' => entry.unique, 'args' => entry.args
|
|
93
99
|
)
|
|
94
100
|
end
|
|
95
|
-
Cogworker.config.redis
|
|
101
|
+
Cogworker.config.redis do |c|
|
|
102
|
+
next c.hset(RedisKeys::PERIODIC_SCHEDULE, *payloads.to_a.flatten) unless @replace
|
|
103
|
+
|
|
104
|
+
c.multi do |tx|
|
|
105
|
+
tx.del(RedisKeys::PERIODIC_SCHEDULE)
|
|
106
|
+
tx.hset(RedisKeys::PERIODIC_SCHEDULE, *payloads.to_a.flatten) unless payloads.empty?
|
|
107
|
+
end
|
|
108
|
+
end
|
|
96
109
|
end
|
|
97
110
|
|
|
98
111
|
# A failed tick is logged and retried next interval rather than ending
|
|
@@ -113,12 +126,16 @@ module Cogworker
|
|
|
113
126
|
|
|
114
127
|
def tick
|
|
115
128
|
retry_pending_rollbacks
|
|
129
|
+
# This process's own delayed lock releases first (see BLOCKED_GRACE).
|
|
130
|
+
BestEffort.call('Deferred lock releases') { Cogworker.config.redis { |c| DeferredReleases.retry_all(c) } }
|
|
116
131
|
now = Time.now
|
|
117
132
|
@entries.each do |entry|
|
|
118
133
|
next if backing_off?(entry.pjid)
|
|
119
134
|
|
|
120
|
-
tick_entry(entry, now)
|
|
135
|
+
next if tick_entry(entry, now) == :skipped # (no Redis call made: proves nothing)
|
|
136
|
+
|
|
121
137
|
@failures.delete(entry.pjid)
|
|
138
|
+
RedisErrors.recovered('Periodic tick')
|
|
122
139
|
rescue StandardError => e
|
|
123
140
|
# Per entry: one entry failing (its keys of the wrong type, its
|
|
124
141
|
# push failing) used to skip every entry after it, on every tick.
|
|
@@ -128,8 +145,8 @@ module Cogworker
|
|
|
128
145
|
# a lost connection, though: that's Redis being away, not the
|
|
129
146
|
# entry failing — backing off every entry on it left cron idle for
|
|
130
147
|
# minutes after Redis came back.
|
|
131
|
-
delay =
|
|
132
|
-
|
|
148
|
+
delay = RedisErrors.unavailable?(e) ? TICK_INTERVAL : back_off(entry.pjid)
|
|
149
|
+
RedisErrors.report('Periodic tick', e) { "#{entry.pjid} failed (next try in #{delay}s)" }
|
|
133
150
|
end
|
|
134
151
|
end
|
|
135
152
|
|
|
@@ -149,12 +166,32 @@ module Cogworker
|
|
|
149
166
|
|
|
150
167
|
def tick_entry(entry, now)
|
|
151
168
|
slot = cron_for(entry).previous_time(now).to_i
|
|
152
|
-
return if @last_checked_slot[entry.pjid] == slot
|
|
169
|
+
return :skipped if @last_checked_slot[entry.pjid] == slot
|
|
153
170
|
|
|
154
171
|
jid = SecureRandom.hex(12)
|
|
155
172
|
previous = claim(entry, slot, jid) unless disabled?(entry)
|
|
156
|
-
|
|
173
|
+
return if previous == :blocked && still_blocked_in_grace?(entry.pjid, slot)
|
|
174
|
+
|
|
175
|
+
enqueue(entry, slot, jid, previous) if previous && previous != :blocked
|
|
157
176
|
@last_checked_slot[entry.pjid] = slot
|
|
177
|
+
@blocked_since.delete(entry.pjid)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# A slot blocked by the entry's running lock is retried for
|
|
181
|
+
# BLOCKED_GRACE before it counts as skipped. After an outage, the lock
|
|
182
|
+
# may still be held only because its finished run's release was
|
|
183
|
+
# deferred (DeferredReleases — retried on the next heartbeat, in
|
|
184
|
+
# whichever process ran it); marking the slot checked straight away
|
|
185
|
+
# lost it, in every process, until the entry's next slot — an hour,
|
|
186
|
+
# for an hourly entry. A run genuinely still going keeps the lock past
|
|
187
|
+
# the grace, and its slot is skipped as `until_executed` intends.
|
|
188
|
+
BLOCKED_GRACE = 3 * Heartbeat::INTERVAL
|
|
189
|
+
|
|
190
|
+
def still_blocked_in_grace?(pjid, slot)
|
|
191
|
+
now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
192
|
+
since_slot, since = @blocked_since[pjid]
|
|
193
|
+
@blocked_since[pjid] = [slot, since = now] unless since_slot == slot
|
|
194
|
+
now - since < BLOCKED_GRACE
|
|
158
195
|
end
|
|
159
196
|
|
|
160
197
|
# The usual reason a push fails — Redis unreachable — makes this fail
|
|
@@ -175,7 +212,7 @@ module Cogworker
|
|
|
175
212
|
end
|
|
176
213
|
true
|
|
177
214
|
rescue StandardError => e
|
|
178
|
-
|
|
215
|
+
RedisErrors.report('Periodic rollback', e) { "#{entry.pjid}: couldn't roll back slot #{slot} (will retry)" }
|
|
179
216
|
@pending_rollbacks << [entry, slot, previous_slot, jid]
|
|
180
217
|
e # (truthy, but not `true`: the caller tells a lost connection from a failing rollback)
|
|
181
218
|
end
|
|
@@ -194,7 +231,8 @@ module Cogworker
|
|
|
194
231
|
if result == true
|
|
195
232
|
@failures.delete(key)
|
|
196
233
|
@retry_at.delete(key)
|
|
197
|
-
|
|
234
|
+
RedisErrors.recovered('Periodic rollback')
|
|
235
|
+
elsif !RedisErrors.unavailable?(result)
|
|
198
236
|
back_off(key) # only a rollback failing on its own backs off; see #tick
|
|
199
237
|
end
|
|
200
238
|
end
|
|
@@ -218,9 +256,9 @@ module Cogworker
|
|
|
218
256
|
# connection still raises (the tick is then retried as a whole).
|
|
219
257
|
def disabled?(entry)
|
|
220
258
|
Cogworker.config.redis { |c| c.sismember(RedisKeys::PERIODIC_DISABLED, entry.pjid) }
|
|
221
|
-
rescue Redis::BaseConnectionError
|
|
222
|
-
raise
|
|
223
259
|
rescue StandardError => e
|
|
260
|
+
raise if RedisErrors.unavailable?(e)
|
|
261
|
+
|
|
224
262
|
Cogworker.logger.error { "Periodic: can't read #{RedisKeys::PERIODIC_DISABLED} (#{e.class}: #{e.message}); treating #{entry.pjid} as enabled" }
|
|
225
263
|
false
|
|
226
264
|
end
|
|
@@ -229,17 +267,20 @@ module Cogworker
|
|
|
229
267
|
@cron_cache[entry.pjid] ||= Fugit::Cron.parse(entry.cron)
|
|
230
268
|
end
|
|
231
269
|
|
|
232
|
-
# nil if this process didn't win the slot
|
|
233
|
-
#
|
|
270
|
+
# nil if this process didn't win the slot, :blocked if the entry's
|
|
271
|
+
# running lock stopped it; otherwise the previous `last_slot` value
|
|
272
|
+
# ("" if none), for #rollback_claim.
|
|
234
273
|
def claim(entry, slot, jid)
|
|
235
274
|
return nil if !@catch_up && priming_first_slot?(entry, slot)
|
|
236
275
|
|
|
237
276
|
result = Cogworker.config.redis do |c|
|
|
238
277
|
LuaScript.run(c, CLAIM_SCRIPT,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
278
|
+
keys: [RedisKeys.periodic_running(entry.pjid), RedisKeys.periodic_last_slot(entry.pjid),
|
|
279
|
+
RedisKeys.periodic_lock(entry.pjid, slot)],
|
|
280
|
+
argv: [slot, entry.unique.to_s, LOCK_TTL, jid, RunningLock.queued_ttl])
|
|
242
281
|
end
|
|
282
|
+
return :blocked if result == 2
|
|
283
|
+
|
|
243
284
|
result.is_a?(Array) ? result[1].to_s : nil
|
|
244
285
|
end
|
|
245
286
|
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
5
|
module Cogworker
|
|
6
|
+
# Every live worker process, as published by its Heartbeat — one
|
|
7
|
+
# Cogworker::Process per `cogworker:processes` member whose presence key
|
|
8
|
+
# still exists (stale members are pruned while iterating).
|
|
6
9
|
class ProcessSet
|
|
7
10
|
include Enumerable
|
|
8
11
|
|
data/lib/cogworker/processor.rb
CHANGED
|
@@ -56,9 +56,9 @@ module Cogworker
|
|
|
56
56
|
def run
|
|
57
57
|
until @manager.stopping?
|
|
58
58
|
begin
|
|
59
|
-
process_one
|
|
59
|
+
RedisErrors.recovered('Processor') unless process_one == :idle
|
|
60
60
|
rescue StandardError => e
|
|
61
|
-
|
|
61
|
+
RedisErrors.report('Processor', e)
|
|
62
62
|
sleep(ERROR_BACKOFF)
|
|
63
63
|
end
|
|
64
64
|
end
|
|
@@ -67,7 +67,7 @@ module Cogworker
|
|
|
67
67
|
def process_one
|
|
68
68
|
if @manager.quiet?
|
|
69
69
|
sleep(0.5)
|
|
70
|
-
return
|
|
70
|
+
return :idle # (no Redis call made)
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
work = begin
|
|
@@ -89,11 +89,13 @@ module Cogworker
|
|
|
89
89
|
@manager.job_started(work.raw_job)
|
|
90
90
|
@busy = true
|
|
91
91
|
@ran = nil
|
|
92
|
+
@performed = false
|
|
92
93
|
disposition = nil
|
|
93
94
|
begin
|
|
94
95
|
disposition = execute(work)
|
|
95
96
|
ensure
|
|
96
97
|
@busy = false
|
|
98
|
+
@manager.job_performed(work.raw_job) unless @performed
|
|
97
99
|
@manager.processor_idle!
|
|
98
100
|
# `execute` raising leaves `disposition` unset: before the job ran
|
|
99
101
|
# that's a hand-back; once it ran, whatever its outcome dictates
|
|
@@ -155,7 +157,11 @@ module Cogworker
|
|
|
155
157
|
@manager.settle_later(work.raw_job, [set, score, encoded]) if fetcher.is_a?(ReliableFetch)
|
|
156
158
|
raise
|
|
157
159
|
end
|
|
158
|
-
|
|
160
|
+
if set == RedisKeys::RETRY
|
|
161
|
+
best_effort('lock extension') { extend_locks_for_retry(job, delay) }
|
|
162
|
+
else
|
|
163
|
+
best_effort('lock release') { release_locks(payload) }
|
|
164
|
+
end
|
|
159
165
|
reason = if set == RedisKeys::RETRY
|
|
160
166
|
"retrying in #{delay}s (interruption #{count} of at most #{MAX_INTERRUPTS})"
|
|
161
167
|
elsif out_of_retries
|
|
@@ -176,9 +182,11 @@ module Cogworker
|
|
|
176
182
|
end
|
|
177
183
|
|
|
178
184
|
def remember_for_dead(work, error)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
# The original job's own fields where readable (so filing it releases
|
|
186
|
+
# its locks); just enough to show in Dead otherwise.
|
|
187
|
+
base = ReliableFetch.parse_hash(work.raw_job) || { 'raw_payload' => JobUtil.safe_string(work.raw_job, 100_000) }
|
|
188
|
+
entry = JSON.generate(base.merge('jid' => jid_of(work), 'error_class' => error.class.name.to_s,
|
|
189
|
+
'error_message' => JobUtil.error_message(error), 'failed_at' => Time.now.to_f))
|
|
182
190
|
@manager.settle_later(work.raw_job, [RedisKeys::DEAD, Time.now.to_f, entry])
|
|
183
191
|
rescue StandardError
|
|
184
192
|
nil
|
|
@@ -230,6 +238,8 @@ module Cogworker
|
|
|
230
238
|
Cogworker.logger.info { "start: #{job['class']} jid=#{job['jid']}" }
|
|
231
239
|
begin
|
|
232
240
|
error = run_job(work, job)
|
|
241
|
+
@manager.job_performed(work.raw_job)
|
|
242
|
+
@performed = true
|
|
233
243
|
@ran = error ? [:interrupt, job, error] : :acknowledge
|
|
234
244
|
error ? finish_failure(job, error) : finish_success(job)
|
|
235
245
|
ensure
|
|
@@ -286,7 +296,9 @@ module Cogworker
|
|
|
286
296
|
Cogworker.config.redis { |c| c.incr(RedisKeys::STATS_FAILED) }
|
|
287
297
|
Throughput.record('failed')
|
|
288
298
|
end
|
|
289
|
-
Cogworker.logger.warn
|
|
299
|
+
Cogworker.logger.warn do
|
|
300
|
+
"fail: #{job['class']} jid=#{job['jid']}: #{error.class}: #{JobUtil.error_message(error)}"
|
|
301
|
+
end
|
|
290
302
|
route_failure(job, error) ? :acknowledge : [:interrupt, job, error]
|
|
291
303
|
end
|
|
292
304
|
|
|
@@ -350,7 +362,12 @@ module Cogworker
|
|
|
350
362
|
begin
|
|
351
363
|
payload = JSON.generate(job)
|
|
352
364
|
Cogworker.config.redis do |c|
|
|
353
|
-
retrying
|
|
365
|
+
if retrying
|
|
366
|
+
c.zadd(RedisKeys::RETRY, Time.now.to_f + delay,
|
|
367
|
+
payload)
|
|
368
|
+
else
|
|
369
|
+
c.zadd(RedisKeys::DEAD, Time.now.to_f, payload)
|
|
370
|
+
end
|
|
354
371
|
end
|
|
355
372
|
rescue StandardError => e
|
|
356
373
|
Cogworker.logger.error { "couldn't record failure of jid=#{job['jid']}: #{e.class}: #{e.message}" }
|
|
@@ -380,6 +397,13 @@ module Cogworker
|
|
|
380
397
|
end
|
|
381
398
|
end
|
|
382
399
|
|
|
400
|
+
# Into dead by way of `interrupt` — possibly with retries still left,
|
|
401
|
+
# so the middleware that normally releases a terminal failure's locks
|
|
402
|
+
# didn't: release them here (owner-checked).
|
|
403
|
+
def release_locks(job)
|
|
404
|
+
Cogworker.config.redis { |c| JobUtil.release_terminal_locks(c, job) }
|
|
405
|
+
end
|
|
406
|
+
|
|
383
407
|
# Grows with the retry count, with jitter to avoid a thundering herd of
|
|
384
408
|
# retries all landing on the same second.
|
|
385
409
|
def retry_delay(count)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cogworker
|
|
4
|
+
# Telling "Redis is unavailable right now" from every other error, and
|
|
5
|
+
# keeping the log readable while it is.
|
|
6
|
+
#
|
|
7
|
+
# Unavailable means a lost connection, or one of the replies a server
|
|
8
|
+
# gives while it can't serve writes for a while — mid-failover (READONLY,
|
|
9
|
+
# MASTERDOWN), still loading its data (LOADING), or busy with a script
|
|
10
|
+
# (BUSY, TRYAGAIN). None of those say anything about the job, entry or key
|
|
11
|
+
# being worked on, so nothing should back off, count a failure, or give up
|
|
12
|
+
# on them; they just mean "try again shortly".
|
|
13
|
+
#
|
|
14
|
+
# `report` logs such errors once per `site` (a short name of the place
|
|
15
|
+
# they come from) as they start, then a one-line summary at most every
|
|
16
|
+
# SUMMARY_INTERVAL, and `recovered(site)` logs once that it's over. A 6
|
|
17
|
+
# minute outage used to produce thousands of identical lines. Any other
|
|
18
|
+
# error is logged in full, every time.
|
|
19
|
+
module RedisErrors
|
|
20
|
+
UNAVAILABLE_REPLIES = %w[READONLY LOADING MASTERDOWN TRYAGAIN BUSY].freeze
|
|
21
|
+
SUMMARY_INTERVAL = 60
|
|
22
|
+
SCRIPT_WRAPPED = /-(?:#{UNAVAILABLE_REPLIES.join('|')})(?=\s|\z)/
|
|
23
|
+
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
@outages = {} # site => { count:, since:, summarized_at:, error: }
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def unavailable?(error)
|
|
29
|
+
return true if error.is_a?(Redis::BaseConnectionError)
|
|
30
|
+
# redis-rb 5's pub/sub path can surface redis-client's own error class.
|
|
31
|
+
return true if defined?(RedisClient::ConnectionError) && error.is_a?(RedisClient::ConnectionError)
|
|
32
|
+
|
|
33
|
+
return false unless error.is_a?(Redis::CommandError)
|
|
34
|
+
|
|
35
|
+
message = error.message.to_s
|
|
36
|
+
# The reply's code is its whole first word — BUSYKEY or BUSYGROUP
|
|
37
|
+
# aren't BUSY. Inside a script, Redis 6.x wraps the code instead:
|
|
38
|
+
# "ERR Error running script (...): ... -READONLY You can't ...".
|
|
39
|
+
UNAVAILABLE_REPLIES.include?(message[/\A\S+/]) ||
|
|
40
|
+
(message.include?('Error running script') && message.match?(SCRIPT_WRAPPED))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Logs `error` at `site`; the block, if given, adds context to the
|
|
44
|
+
# first line (e.g. what was being done).
|
|
45
|
+
def report(site, error, &context)
|
|
46
|
+
detail = "#{error.class}: #{error.message}"
|
|
47
|
+
detail = "#{context.call} — #{detail}" if context
|
|
48
|
+
return Cogworker.logger.error { "#{site}: #{detail}" } unless unavailable?(error)
|
|
49
|
+
|
|
50
|
+
line = @mutex.synchronize { track(site, error, detail) }
|
|
51
|
+
Cogworker.logger.error { line } if line
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Call wherever `site` has just done its job: ends an outage there.
|
|
55
|
+
def recovered(site)
|
|
56
|
+
outage = @mutex.synchronize { @outages.delete(site) }
|
|
57
|
+
return unless outage
|
|
58
|
+
|
|
59
|
+
Cogworker.logger.warn do
|
|
60
|
+
"#{site}: Redis available again after #{outage[:count]} error(s) over #{(now - outage[:since]).round}s"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def reset!
|
|
65
|
+
@mutex.synchronize { @outages.clear }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def track(site, error, detail)
|
|
71
|
+
outage = @outages[site]
|
|
72
|
+
unless outage
|
|
73
|
+
@outages[site] = { count: 1, since: now, summarized_at: now, suppressed: 0 }
|
|
74
|
+
return "#{site}: Redis unavailable — #{detail} (repeats summarized every #{SUMMARY_INTERVAL}s)"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
outage[:count] += 1
|
|
78
|
+
outage[:suppressed] += 1
|
|
79
|
+
return unless now - outage[:summarized_at] >= SUMMARY_INTERVAL
|
|
80
|
+
|
|
81
|
+
suppressed = outage[:suppressed]
|
|
82
|
+
outage[:summarized_at] = now
|
|
83
|
+
outage[:suppressed] = 0
|
|
84
|
+
"#{site}: Redis still unavailable — #{suppressed} more error(s) in the last #{SUMMARY_INTERVAL}s " \
|
|
85
|
+
"(latest: #{error.class}: #{error.message})"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def now
|
|
89
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/cogworker/redis_keys.rb
CHANGED
|
@@ -26,6 +26,10 @@ module Cogworker
|
|
|
26
26
|
# over by a process shutting down (JSON {set, score, payload} each) —
|
|
27
27
|
# see ReliableFetch.park_unsettled / file_unsettled.
|
|
28
28
|
UNSETTLED = 'cogworker:unsettled'
|
|
29
|
+
# queue => Redis time a live process last said it reads it (every
|
|
30
|
+
# heartbeat). Unlike QUEUES (the Web UI's listing, never pruned), this
|
|
31
|
+
# tells "nobody reads this queue any more" apart from "somebody does".
|
|
32
|
+
LIVE_QUEUES = 'cogworker:live_queues'
|
|
29
33
|
QUEUES = 'cogworker:queues'
|
|
30
34
|
SCHEDULE = 'cogworker:schedule'
|
|
31
35
|
RETRY = 'cogworker:retry'
|