cogworker 0.4.1 → 0.5.1
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 +596 -0
- data/lib/cogworker/basic_fetch.rb +4 -3
- 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 +16 -1
- data/lib/cogworker/deferred_releases.rb +46 -0
- data/lib/cogworker/heartbeat.rb +72 -15
- data/lib/cogworker/history/middleware.rb +9 -5
- data/lib/cogworker/job_util.rb +39 -4
- data/lib/cogworker/key_guard.rb +127 -0
- data/lib/cogworker/launcher.rb +6 -2
- data/lib/cogworker/lua_script.rb +3 -3
- data/lib/cogworker/manager.rb +76 -1
- data/lib/cogworker/periodic/claim.lua +5 -2
- data/lib/cogworker/periodic/release_middleware.rb +13 -2
- data/lib/cogworker/periodic/running_lock.rb +12 -0
- data/lib/cogworker/periodic/ticker.rb +148 -40
- data/lib/cogworker/process_set.rb +3 -0
- data/lib/cogworker/processor.rb +94 -19
- data/lib/cogworker/redis_errors.rb +93 -0
- data/lib/cogworker/redis_keys.rb +13 -0
- data/lib/cogworker/reliable_fetch.rb +477 -60
- data/lib/cogworker/scheduled.rb +101 -18
- 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 +7 -1
data/lib/cogworker/scheduled.rb
CHANGED
|
@@ -21,17 +21,21 @@ module Cogworker
|
|
|
21
21
|
# is limited to processes actually using ReliableFetch. The requeue is
|
|
22
22
|
# atomic, so any number of processes doing it at once is safe.
|
|
23
23
|
ORPHAN_CHECK_INTERVAL = 60
|
|
24
|
+
DEFER_DELAY = 60
|
|
24
25
|
|
|
25
26
|
# KEYS[1] = the ZSET, KEYS[2] = cogworker:dead, KEYS[3] = stats:failed;
|
|
26
27
|
# ARGV[1] = the entry, ARGV[2] = its dead-set score, ARGV[3] = the
|
|
27
28
|
# wrapper to store. Same claim-and-move-atomically shape as
|
|
28
29
|
# JobUtil::CLAIM_AND_REQUEUE_SCRIPT.
|
|
30
|
+
# (Write, then remove — see JobUtil::CLAIM_AND_REQUEUE_SCRIPT; the stats
|
|
31
|
+
# counter is best-effort.)
|
|
29
32
|
BURY_SCRIPT = <<~LUA
|
|
30
|
-
if redis.call("
|
|
33
|
+
if not redis.call("ZSCORE", KEYS[1], ARGV[1]) then
|
|
31
34
|
return 0
|
|
32
35
|
end
|
|
33
36
|
redis.call("ZADD", KEYS[2], ARGV[2], ARGV[3])
|
|
34
|
-
redis.call("
|
|
37
|
+
redis.call("ZREM", KEYS[1], ARGV[1])
|
|
38
|
+
redis.pcall("INCR", KEYS[3])
|
|
35
39
|
return 1
|
|
36
40
|
LUA
|
|
37
41
|
|
|
@@ -61,41 +65,120 @@ module Cogworker
|
|
|
61
65
|
@recovered_once == true
|
|
62
66
|
end
|
|
63
67
|
|
|
68
|
+
# What the periodic Ticker waits for before its first tick: the first
|
|
69
|
+
# orphan check that really completed. No timeout: without Redis the
|
|
70
|
+
# Ticker couldn't do anything anyway, and a key that would keep the check
|
|
71
|
+
# failing for good is quarantined (KeyGuard) within a minute — whereas
|
|
72
|
+
# starting early after a long outage could run an `until_executed` slot
|
|
73
|
+
# alongside the recovered run of the previous one.
|
|
74
|
+
def ready_for_ticker?
|
|
75
|
+
recovered_once?
|
|
76
|
+
end
|
|
77
|
+
|
|
64
78
|
private
|
|
65
79
|
|
|
66
80
|
# A failed poll is logged and retried next interval rather than ending
|
|
67
81
|
# the thread — a single Redis blip used to stop every scheduled/retry
|
|
68
82
|
# job in this process from ever reaching its queue again.
|
|
83
|
+
# Recovery and graduating due jobs fail independently: a recovery that
|
|
84
|
+
# keeps failing used to skip `enqueue_due_jobs` on every poll, so no
|
|
85
|
+
# scheduled or retried job reached its queue anywhere in the fleet.
|
|
69
86
|
def run
|
|
70
87
|
until @manager.stopping?
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
rescue StandardError => e
|
|
75
|
-
Cogworker.logger.error { "Scheduled poll failed: #{e.class}: #{e.message}" }
|
|
76
|
-
end
|
|
88
|
+
guarded('Key check') { check_keys_if_due }
|
|
89
|
+
guarded('Orphan check') { recover_orphans_if_due }
|
|
90
|
+
guarded('Scheduled poll') { enqueue_due_jobs unless @manager.quiet? }
|
|
77
91
|
sleep(POLL_INTERVAL)
|
|
78
92
|
end
|
|
79
93
|
end
|
|
80
94
|
|
|
81
|
-
#
|
|
82
|
-
#
|
|
95
|
+
# An entry that can't be graduated right now (its queue key of the wrong
|
|
96
|
+
# type, say) is pushed DEFER_DELAY into the future: left due, it would be
|
|
97
|
+
# one of the same oldest 50 picked again on every poll, and enough of
|
|
98
|
+
# them would starve everything behind them for good.
|
|
99
|
+
def defer(set, raw, error)
|
|
100
|
+
Cogworker.logger.error do
|
|
101
|
+
"Graduating an entry of #{set} failed (deferred #{DEFER_DELAY}s): #{error.class}: #{error.message}"
|
|
102
|
+
end
|
|
103
|
+
Cogworker.config.redis { |c| c.zadd(set, Time.now.to_f + DEFER_DELAY, raw, xx: true) }
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
Cogworker.logger.error { "Deferring it failed too: #{e.class}: #{e.message}" }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# A block returning :skipped did nothing (not due yet): that proves
|
|
109
|
+
# nothing about Redis, so it doesn't end an outage report either.
|
|
110
|
+
def guarded(what)
|
|
111
|
+
result = yield
|
|
112
|
+
RedisErrors.recovered(what) unless result == :skipped
|
|
113
|
+
result
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
RedisErrors.report(what, e)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The next check is scheduled before this one runs: one that keeps
|
|
119
|
+
# failing (everything inside it is isolated, so that means Redis itself
|
|
120
|
+
# is failing) is retried every ORPHAN_CHECK_INTERVAL — or on the next
|
|
121
|
+
# poll if it lost the connection — never on every poll with a full SCAN.
|
|
122
|
+
# The one-off SCAN only counts as done once it has succeeded.
|
|
83
123
|
def recover_orphans_if_due
|
|
84
124
|
now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
85
|
-
return if @next_orphan_check && now < @next_orphan_check
|
|
125
|
+
return :skipped if @next_orphan_check && now < @next_orphan_check
|
|
86
126
|
|
|
87
|
-
reliable = @manager.fetch_class == ReliableFetch
|
|
88
|
-
ReliableFetch.recover_orphans(scan: !@recovered_once && reliable)
|
|
89
|
-
@strays = ReliableFetch.reconcile(Cogworker.identity, @strays || []) if reliable
|
|
90
|
-
@recovered_once = true
|
|
91
127
|
@next_orphan_check = now + ORPHAN_CHECK_INTERVAL
|
|
128
|
+
reliable = @manager.fetch_class == ReliableFetch
|
|
129
|
+
report = {}
|
|
130
|
+
# Lost the connection (anywhere in it): check again on the next poll,
|
|
131
|
+
# not a minute later — Redis coming back is when it matters most.
|
|
132
|
+
begin
|
|
133
|
+
ReliableFetch.recover_orphans(scan: reliable && !@scanned, report: report)
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
@next_orphan_check = now + POLL_INTERVAL if RedisErrors.unavailable?(e)
|
|
136
|
+
raise
|
|
137
|
+
end
|
|
138
|
+
@next_orphan_check = now + POLL_INTERVAL if report[:connection_lost]
|
|
139
|
+
guarded('Reconcile') { reconcile } if reliable
|
|
140
|
+
# Only once that actually looked: a pass that merely survived every
|
|
141
|
+
# error (recovery isolates them), or lost the connection part-way,
|
|
142
|
+
# mustn't let the Ticker start, nor count the one-off SCAN as done.
|
|
143
|
+
return if report[:connection_lost]
|
|
144
|
+
|
|
145
|
+
@scanned = true if report[:scan]
|
|
146
|
+
@recovered_once = true if report[:registry] || report[:scan]
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# See KeyGuard. First thing on the first poll, then every
|
|
151
|
+
# KeyGuard::CHECK_INTERVAL — before recovery and polling, so a key that
|
|
152
|
+
# would break them is already out of the way.
|
|
153
|
+
def check_keys_if_due
|
|
154
|
+
now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
155
|
+
return :skipped if @next_key_check && now < @next_key_check
|
|
156
|
+
|
|
157
|
+
@next_key_check = now + KeyGuard::CHECK_INTERVAL
|
|
158
|
+
KeyGuard.check(queues: @manager.queues)
|
|
92
159
|
end
|
|
93
160
|
|
|
161
|
+
def reconcile
|
|
162
|
+
@strays = ReliableFetch.reconcile(Cogworker.identity, @strays || [],
|
|
163
|
+
running: @manager.running_jobs, pending: @manager.pending_settlements) do |raw|
|
|
164
|
+
@manager.settled(raw)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Each set, and each entry, on its own: `schedule` unreadable mustn't
|
|
169
|
+
# stop `retry` from being polled, nor one entry whose graduation fails
|
|
170
|
+
# stop the rest of the batch.
|
|
94
171
|
def enqueue_due_jobs
|
|
95
172
|
now = Time.now.to_f
|
|
96
173
|
SETS.each do |set|
|
|
97
|
-
|
|
98
|
-
|
|
174
|
+
guarded("Polling #{set}") do
|
|
175
|
+
candidates = Cogworker.config.redis { |c| c.zrangebyscore(set, '-inf', now, limit: [0, 50]) }
|
|
176
|
+
candidates.each do |raw|
|
|
177
|
+
graduate(set, raw)
|
|
178
|
+
rescue StandardError => e
|
|
179
|
+
defer(set, raw, e)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
99
182
|
end
|
|
100
183
|
end
|
|
101
184
|
|
|
@@ -115,7 +198,7 @@ module Cogworker
|
|
|
115
198
|
error = JSON::ParserError.new("not a job (JSON object with a queue) in #{set}")
|
|
116
199
|
job = JobUtil.unparseable_job(raw, queue: nil, error: error)
|
|
117
200
|
buried = LuaScript.run(conn, BURY_SCRIPT, keys: [set, RedisKeys::DEAD, RedisKeys::STATS_FAILED],
|
|
118
|
-
|
|
201
|
+
argv: [raw, job['failed_at'], JSON.generate(job)])
|
|
119
202
|
return unless buried == 1
|
|
120
203
|
|
|
121
204
|
Throughput.record('failed') # counted like Processor#bury_unparseable
|
|
@@ -9,14 +9,20 @@ module Cogworker
|
|
|
9
9
|
@expiration = expiration
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
# Every status write is best-effort (BestEffort) — before `perform`
|
|
13
|
+
# as much as after: one failing used to count as the job failing.
|
|
12
14
|
def call(_worker, job, queue)
|
|
13
|
-
write(job, queue, 'working')
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
BestEffort.call('Status') { write(job, queue, 'working') }
|
|
16
|
+
begin
|
|
17
|
+
yield
|
|
18
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
19
|
+
BestEffort.call('Status') do
|
|
20
|
+
write(job, queue, JobUtil.terminal_failure?(job) ? 'failed' : 'retrying',
|
|
21
|
+
error_class: e.class.name, error_message: JobUtil.error_message(e))
|
|
22
|
+
end
|
|
23
|
+
raise e
|
|
24
|
+
end
|
|
25
|
+
BestEffort.call('Status') { write(job, queue, 'complete') }
|
|
20
26
|
end
|
|
21
27
|
|
|
22
28
|
private
|
data/lib/cogworker/swarm.rb
CHANGED
|
@@ -59,6 +59,8 @@ module Cogworker
|
|
|
59
59
|
def fork_child(slot)
|
|
60
60
|
pid = ::Process.fork do
|
|
61
61
|
Cogworker.reset_identity!
|
|
62
|
+
RedisErrors.reset! # the parent's outage state isn't this child's
|
|
63
|
+
DeferredReleases.reset! # nor are its pending releases
|
|
62
64
|
CLI.new.run(@argv.dup)
|
|
63
65
|
# `exit!`, not plain `exit`: `Launcher#run` only returns once
|
|
64
66
|
# `Manager#stop!` has had its bounded (default 25s) chance to join
|
|
@@ -12,12 +12,16 @@ module Cogworker
|
|
|
12
12
|
# `unique: :until_executed`. Registered unconditionally in every
|
|
13
13
|
# `Config`, same as `Periodic::ReleaseMiddleware`.
|
|
14
14
|
class ReleaseMiddleware
|
|
15
|
+
# Releasing is best-effort (BestEffort): a failed release leaves the
|
|
16
|
+
# lock to its TTL, and only the job's own exception is re-raised.
|
|
15
17
|
def call(_worker, job, _queue)
|
|
16
|
-
|
|
18
|
+
begin
|
|
19
|
+
yield
|
|
20
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
21
|
+
release(job) if UniqueJobs.until_executed?(job) && JobUtil.terminal_failure?(job)
|
|
22
|
+
raise e
|
|
23
|
+
end
|
|
17
24
|
release(job) if UniqueJobs.until_executed?(job)
|
|
18
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
19
|
-
release(job) if UniqueJobs.until_executed?(job) && JobUtil.terminal_failure?(job)
|
|
20
|
-
raise e
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
private
|
|
@@ -25,8 +29,15 @@ module Cogworker
|
|
|
25
29
|
# Owner-checked: if this job's lock already expired and passed to a
|
|
26
30
|
# newer copy, this must not release *that* one's lock (which would let
|
|
27
31
|
# a third copy in while the newer one is still running).
|
|
32
|
+
# Owner-checked; retried later (DeferredReleases) if it fails, rather
|
|
33
|
+
# than leaving duplicates blocked for the lock's whole TTL.
|
|
28
34
|
def release(job)
|
|
29
|
-
|
|
35
|
+
key = RedisKeys.unique_lock(UniqueJobs.digest(job))
|
|
36
|
+
released = BestEffort.call('Unique lock') do
|
|
37
|
+
OwnedKey.delete(key, job['jid'])
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
DeferredReleases.add(key, job['jid']) unless released
|
|
30
41
|
end
|
|
31
42
|
end
|
|
32
43
|
end
|
data/lib/cogworker/version.rb
CHANGED
data/lib/cogworker/web/layout.rb
CHANGED
|
@@ -380,7 +380,9 @@ module Cogworker
|
|
|
380
380
|
# Redis cards), where the default wrapper would just nest one bordered
|
|
381
381
|
# box inside another.
|
|
382
382
|
def table(headers, rows, empty_message: 'Nothing here.', wrapped: true)
|
|
383
|
-
|
|
383
|
+
if rows.empty?
|
|
384
|
+
return %(<p class="text-muted" style="font-size: 13px; font-style: italic;">#{h(empty_message)}</p>)
|
|
385
|
+
end
|
|
384
386
|
|
|
385
387
|
head = headers.map { |c| %(<th>#{h(c)}</th>) }.join
|
|
386
388
|
body = rows.map do |row|
|
|
@@ -288,13 +288,13 @@ module Cogworker
|
|
|
288
288
|
def retry_now_button(bucket, script_name, row, params)
|
|
289
289
|
action = Layout.path(script_name, "jobs/#{bucket}/retry_now")
|
|
290
290
|
form_with_extra(action, 'raw', row[:raw], 'retry now', params, variant: :primary, icon: 'arrow-clockwise',
|
|
291
|
-
|
|
291
|
+
extra: {})
|
|
292
292
|
end
|
|
293
293
|
|
|
294
294
|
def retry_button(bucket, script_name, row, params)
|
|
295
295
|
action = Layout.path(script_name, "jobs/#{bucket}/retry")
|
|
296
296
|
form_with_extra(action, 'raw', row[:raw], 'retry', params, variant: :primary, icon: 'arrow-clockwise',
|
|
297
|
-
|
|
297
|
+
extra: {})
|
|
298
298
|
end
|
|
299
299
|
|
|
300
300
|
# `Layout.form_button` only carries one hidden field. Every action
|
|
@@ -304,9 +304,9 @@ module Cogworker
|
|
|
304
304
|
# since it isn't in the URL a plain form submits to) plus, for an
|
|
305
305
|
# enqueued-job delete, `queue` (`Queue#delete` isn't keyed off the
|
|
306
306
|
# raw payload alone the way the ZSETs are).
|
|
307
|
-
def form_with_extra(action, hidden_name, hidden_value, label, params, variant:, icon: nil
|
|
307
|
+
def form_with_extra(action, hidden_name, hidden_value, label, params, variant:, extra:, icon: nil)
|
|
308
308
|
hidden = extra.merge('status' => params['status'].to_s, 'q' => params['q'].to_s,
|
|
309
|
-
|
|
309
|
+
'selected' => params['selected'].to_s)
|
|
310
310
|
hidden_inputs = hidden.map { |k, v| %(<input type="hidden" name="#{k}" value="#{Layout.h(v)}">) }.join
|
|
311
311
|
classes = "btn #{Layout::BUTTON_VARIANTS.fetch(variant)}"
|
|
312
312
|
<<~HTML
|
|
@@ -613,7 +613,9 @@ module Cogworker
|
|
|
613
613
|
def render_layout_b(script_name, selected_name)
|
|
614
614
|
names = queue_names
|
|
615
615
|
selected_name = names.first if (selected_name.nil? || selected_name.empty?) && !names.empty?
|
|
616
|
-
|
|
616
|
+
if selected_name.nil? || selected_name.empty?
|
|
617
|
+
return %(<p class="text-muted">No queues yet — push a job to create one.</p>)
|
|
618
|
+
end
|
|
617
619
|
|
|
618
620
|
<<~HTML
|
|
619
621
|
<div style="display: grid; grid-template-columns: minmax(240px, 320px) minmax(0, 1fr); gap: 16px; align-items: start;">
|
|
@@ -676,7 +678,7 @@ module Cogworker
|
|
|
676
678
|
end.join
|
|
677
679
|
paused_tag = q.paused? ? " #{Layout.badge('paused', variant: :warning)}" : ''
|
|
678
680
|
header_actions = queue_pause_button(q, script_name, 'b') + retry_all_button(name, script_name,
|
|
679
|
-
|
|
681
|
+
retrying_count)
|
|
680
682
|
<<~HTML
|
|
681
683
|
<section style="display: flex; flex-direction: column; gap: 16px; min-width: 0;">
|
|
682
684
|
<div style="background: var(--color-surface); border-radius: var(--radius-md); box-shadow: var(--shadow-sm); padding: 18px;">
|
data/lib/cogworker/work_set.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
5
|
module Cogworker
|
|
6
|
+
# Every job running right now, across all live processes: yields
|
|
7
|
+
# `[identity, thread id, Cogworker::Work]` from each process's
|
|
8
|
+
# `cogworker:workers:<identity>` Hash (only for processes ProcessSet knows).
|
|
6
9
|
class WorkSet
|
|
7
10
|
include Enumerable
|
|
8
11
|
|
data/lib/cogworker.rb
CHANGED
|
@@ -101,7 +101,7 @@ module Cogworker
|
|
|
101
101
|
# No-op: exists so calling code doesn't need to change, but this gem
|
|
102
102
|
# simply never coerces job args, so there is nothing to toggle — see
|
|
103
103
|
# README "Job arguments".
|
|
104
|
-
def strict_args!(_value = true)
|
|
104
|
+
def strict_args!(_value = true) # rubocop:disable Style/OptionalBooleanParameter -- kept positional for calling-code compatibility
|
|
105
105
|
true
|
|
106
106
|
end
|
|
107
107
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cogworker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- vdonec
|
|
@@ -203,16 +203,20 @@ executables:
|
|
|
203
203
|
extensions: []
|
|
204
204
|
extra_rdoc_files: []
|
|
205
205
|
files:
|
|
206
|
+
- LICENSE.txt
|
|
207
|
+
- README.md
|
|
206
208
|
- exe/cogworker
|
|
207
209
|
- exe/cogworkerswarm
|
|
208
210
|
- lib/cogworker.rb
|
|
209
211
|
- lib/cogworker/attempts.rb
|
|
210
212
|
- lib/cogworker/basic_fetch.rb
|
|
213
|
+
- lib/cogworker/best_effort.rb
|
|
211
214
|
- lib/cogworker/cli.rb
|
|
212
215
|
- lib/cogworker/client.rb
|
|
213
216
|
- lib/cogworker/component.rb
|
|
214
217
|
- lib/cogworker/config.rb
|
|
215
218
|
- lib/cogworker/config_loader.rb
|
|
219
|
+
- lib/cogworker/deferred_releases.rb
|
|
216
220
|
- lib/cogworker/heartbeat.rb
|
|
217
221
|
- lib/cogworker/history.rb
|
|
218
222
|
- lib/cogworker/history/middleware.rb
|
|
@@ -220,6 +224,7 @@ files:
|
|
|
220
224
|
- lib/cogworker/job.rb
|
|
221
225
|
- lib/cogworker/job_record.rb
|
|
222
226
|
- lib/cogworker/job_util.rb
|
|
227
|
+
- lib/cogworker/key_guard.rb
|
|
223
228
|
- lib/cogworker/launcher.rb
|
|
224
229
|
- lib/cogworker/logging.rb
|
|
225
230
|
- lib/cogworker/lua_script.rb
|
|
@@ -238,6 +243,7 @@ files:
|
|
|
238
243
|
- lib/cogworker/prometheus/exporter.rb
|
|
239
244
|
- lib/cogworker/queue.rb
|
|
240
245
|
- lib/cogworker/redis_connection.rb
|
|
246
|
+
- lib/cogworker/redis_errors.rb
|
|
241
247
|
- lib/cogworker/redis_keys.rb
|
|
242
248
|
- lib/cogworker/reliable_fetch.rb
|
|
243
249
|
- lib/cogworker/reliable_fetch/unsupported_error.rb
|