cogworker 0.4.1 → 0.5.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/lib/cogworker/basic_fetch.rb +2 -1
- data/lib/cogworker/config.rb +13 -1
- data/lib/cogworker/heartbeat.rb +23 -10
- data/lib/cogworker/job_util.rb +22 -4
- data/lib/cogworker/key_guard.rb +92 -0
- data/lib/cogworker/launcher.rb +3 -2
- data/lib/cogworker/manager.rb +58 -1
- data/lib/cogworker/periodic/ticker.rb +112 -33
- data/lib/cogworker/processor.rb +65 -14
- data/lib/cogworker/redis_keys.rb +9 -0
- data/lib/cogworker/reliable_fetch.rb +356 -54
- data/lib/cogworker/scheduled.rb +92 -16
- data/lib/cogworker/version.rb +1 -1
- metadata +2 -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,113 @@ 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 { "Graduating an entry of #{set} failed (deferred #{DEFER_DELAY}s): #{error.class}: #{error.message}" }
|
|
101
|
+
Cogworker.config.redis { |c| c.zadd(set, Time.now.to_f + DEFER_DELAY, raw, xx: true) }
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
Cogworker.logger.error { "Deferring it failed too: #{e.class}: #{e.message}" }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def guarded(what)
|
|
107
|
+
yield
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
Cogworker.logger.error { "#{what} failed: #{e.class}: #{e.message}" }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The next check is scheduled before this one runs: one that keeps
|
|
113
|
+
# failing (everything inside it is isolated, so that means Redis itself
|
|
114
|
+
# is failing) is retried every ORPHAN_CHECK_INTERVAL — or on the next
|
|
115
|
+
# poll if it lost the connection — never on every poll with a full SCAN.
|
|
116
|
+
# The one-off SCAN only counts as done once it has succeeded.
|
|
83
117
|
def recover_orphans_if_due
|
|
84
118
|
now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
85
119
|
return if @next_orphan_check && now < @next_orphan_check
|
|
86
120
|
|
|
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
121
|
@next_orphan_check = now + ORPHAN_CHECK_INTERVAL
|
|
122
|
+
reliable = @manager.fetch_class == ReliableFetch
|
|
123
|
+
report = {}
|
|
124
|
+
# Lost the connection (anywhere in it): check again on the next poll,
|
|
125
|
+
# not a minute later — Redis coming back is when it matters most.
|
|
126
|
+
begin
|
|
127
|
+
ReliableFetch.recover_orphans(scan: reliable && !@scanned, report: report)
|
|
128
|
+
rescue Redis::BaseConnectionError
|
|
129
|
+
@next_orphan_check = now + POLL_INTERVAL
|
|
130
|
+
raise
|
|
131
|
+
end
|
|
132
|
+
@next_orphan_check = now + POLL_INTERVAL if report[:connection_lost]
|
|
133
|
+
guarded('Reconcile') { reconcile } if reliable
|
|
134
|
+
# Only once that actually looked: a pass that merely survived every
|
|
135
|
+
# error (recovery isolates them), or lost the connection part-way,
|
|
136
|
+
# mustn't let the Ticker start, nor count the one-off SCAN as done.
|
|
137
|
+
return if report[:connection_lost]
|
|
138
|
+
|
|
139
|
+
@scanned = true if report[:scan]
|
|
140
|
+
@recovered_once = true if report[:registry] || report[:scan]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# See KeyGuard. First thing on the first poll, then every
|
|
144
|
+
# KeyGuard::CHECK_INTERVAL — before recovery and polling, so a key that
|
|
145
|
+
# would break them is already out of the way.
|
|
146
|
+
def check_keys_if_due
|
|
147
|
+
now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
148
|
+
return if @next_key_check && now < @next_key_check
|
|
149
|
+
|
|
150
|
+
@next_key_check = now + KeyGuard::CHECK_INTERVAL
|
|
151
|
+
KeyGuard.check(queues: @manager.queues)
|
|
92
152
|
end
|
|
93
153
|
|
|
154
|
+
def reconcile
|
|
155
|
+
@strays = ReliableFetch.reconcile(Cogworker.identity, @strays || [],
|
|
156
|
+
running: @manager.running_jobs, pending: @manager.pending_settlements) do |raw|
|
|
157
|
+
@manager.settled(raw)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Each set, and each entry, on its own: `schedule` unreadable mustn't
|
|
162
|
+
# stop `retry` from being polled, nor one entry whose graduation fails
|
|
163
|
+
# stop the rest of the batch.
|
|
94
164
|
def enqueue_due_jobs
|
|
95
165
|
now = Time.now.to_f
|
|
96
166
|
SETS.each do |set|
|
|
97
|
-
|
|
98
|
-
|
|
167
|
+
guarded("Polling #{set}") do
|
|
168
|
+
candidates = Cogworker.config.redis { |c| c.zrangebyscore(set, '-inf', now, limit: [0, 50]) }
|
|
169
|
+
candidates.each do |raw|
|
|
170
|
+
graduate(set, raw)
|
|
171
|
+
rescue StandardError => e
|
|
172
|
+
defer(set, raw, e)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
99
175
|
end
|
|
100
176
|
end
|
|
101
177
|
|
data/lib/cogworker/version.rb
CHANGED
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.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- vdonec
|
|
@@ -220,6 +220,7 @@ files:
|
|
|
220
220
|
- lib/cogworker/job.rb
|
|
221
221
|
- lib/cogworker/job_record.rb
|
|
222
222
|
- lib/cogworker/job_util.rb
|
|
223
|
+
- lib/cogworker/key_guard.rb
|
|
223
224
|
- lib/cogworker/launcher.rb
|
|
224
225
|
- lib/cogworker/logging.rb
|
|
225
226
|
- lib/cogworker/lua_script.rb
|