cogworker 0.2.2 → 0.3.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 +37 -2
- data/lib/cogworker/cli.rb +11 -1
- data/lib/cogworker/client.rb +1 -1
- data/lib/cogworker/config.rb +26 -1
- data/lib/cogworker/heartbeat.rb +54 -6
- data/lib/cogworker/job.rb +9 -0
- data/lib/cogworker/job_util.rb +56 -0
- data/lib/cogworker/launcher.rb +20 -1
- data/lib/cogworker/manager.rb +43 -4
- data/lib/cogworker/owned_key.rb +40 -0
- data/lib/cogworker/periodic/claim.lua +9 -0
- data/lib/cogworker/periodic/release_middleware.rb +31 -13
- data/lib/cogworker/periodic/running_lock.rb +44 -0
- data/lib/cogworker/periodic/ticker.rb +39 -10
- data/lib/cogworker/process.rb +2 -2
- data/lib/cogworker/process_set.rb +1 -1
- data/lib/cogworker/processor.rb +116 -14
- data/lib/cogworker/queue.rb +4 -4
- data/lib/cogworker/redis_keys.rb +2 -0
- data/lib/cogworker/reliable_fetch.rb +137 -0
- data/lib/cogworker/scheduled.rb +37 -10
- data/lib/cogworker/stats.rb +1 -1
- data/lib/cogworker/status.rb +5 -1
- data/lib/cogworker/swarm.rb +64 -4
- data/lib/cogworker/testing/empty_queue_error.rb +9 -0
- data/lib/cogworker/testing.rb +27 -3
- data/lib/cogworker/unique_jobs/client_middleware.rb +13 -4
- data/lib/cogworker/unique_jobs/release_middleware.rb +4 -1
- data/lib/cogworker/version.rb +1 -1
- data/lib/cogworker/web/layout.rb +3 -3
- data/lib/cogworker/web/routes/jobs.rb +17 -17
- data/lib/cogworker/web/routes/overview.rb +6 -12
- data/lib/cogworker/web/routes/schedules.rb +2 -2
- data/lib/cogworker/web.rb +21 -8
- data/lib/cogworker.rb +5 -1
- metadata +5 -1
data/lib/cogworker/swarm.rb
CHANGED
|
@@ -20,14 +20,30 @@ module Cogworker
|
|
|
20
20
|
# that never exits (a hung Redis call outliving even Manager#stop!'s
|
|
21
21
|
# own 25s join, or anything else) must not be able to wedge the whole
|
|
22
22
|
# restart cycle, and the supervisor's entire signal-handling loop with
|
|
23
|
-
# it, forever.
|
|
24
|
-
|
|
23
|
+
# it, forever. Kept well above a child's own worst-case clean shutdown
|
|
24
|
+
# (Manager#stop!'s 25s drain + up to BasicFetch::TIMEOUT + 1 waiting out
|
|
25
|
+
# fetching threads + ~1s for Heartbeat#stop!) so a child is never killed
|
|
26
|
+
# mid-`requeue_unfinished` — for `:basic`, that would lose the jobs.
|
|
27
|
+
GRACEFUL_STOP_TIMEOUT = 35
|
|
28
|
+
|
|
29
|
+
# Respawn backoff for a child that exits unexpectedly: none for the
|
|
30
|
+
# first crash after a healthy run, then doubling from
|
|
31
|
+
# RESPAWN_BASE_DELAY up to RESPAWN_MAX_DELAY while the slot keeps dying
|
|
32
|
+
# within HEALTHY_UPTIME of starting. Without it, a child crashing during
|
|
33
|
+
# boot (bad deploy, Redis unreachable, a typo in the init file) made the
|
|
34
|
+
# supervisor fork/crash in a tight loop forever.
|
|
35
|
+
HEALTHY_UPTIME = 30
|
|
36
|
+
RESPAWN_BASE_DELAY = 1
|
|
37
|
+
RESPAWN_MAX_DELAY = 60
|
|
25
38
|
|
|
26
39
|
def initialize(argv, count: ENV.fetch('COGWORKER_COUNT', 1).to_i, phased: ENV['PHASED_RESTART'] == 'true')
|
|
27
40
|
@argv = argv
|
|
28
41
|
@count = [count, 1].max
|
|
29
42
|
@phased = phased
|
|
30
43
|
@children = {} # pid => slot index
|
|
44
|
+
@started_at = {} # slot => monotonic time of its current child's fork
|
|
45
|
+
@crash_streak = Hash.new(0) # slot => consecutive quick crashes
|
|
46
|
+
@pending_respawns = {} # slot => monotonic time it may be forked again
|
|
31
47
|
@signal_queue = ::Queue.new
|
|
32
48
|
@stopping = false
|
|
33
49
|
end
|
|
@@ -56,6 +72,7 @@ module Cogworker
|
|
|
56
72
|
::Process.exit!(true)
|
|
57
73
|
end
|
|
58
74
|
@children[pid] = slot
|
|
75
|
+
@started_at[slot] = monotonic_now
|
|
59
76
|
Cogworker.logger.info { "swarm: started child pid=#{pid} slot=#{slot}" }
|
|
60
77
|
end
|
|
61
78
|
|
|
@@ -70,13 +87,16 @@ module Cogworker
|
|
|
70
87
|
until @stopping && @children.empty?
|
|
71
88
|
drain_signals
|
|
72
89
|
reap_children
|
|
90
|
+
respawn_due_children
|
|
91
|
+
kill_stragglers
|
|
73
92
|
sleep 0.2
|
|
74
93
|
end
|
|
75
94
|
end
|
|
76
95
|
|
|
77
96
|
def drain_signals
|
|
78
97
|
until @signal_queue.empty?
|
|
79
|
-
|
|
98
|
+
signal = @signal_queue.pop
|
|
99
|
+
case signal
|
|
80
100
|
when :quiet then relay(Signals::QUIET)
|
|
81
101
|
when :stop then initiate_stop
|
|
82
102
|
when :restart
|
|
@@ -93,9 +113,25 @@ module Cogworker
|
|
|
93
113
|
|
|
94
114
|
def initiate_stop
|
|
95
115
|
@stopping = true
|
|
116
|
+
@stop_deadline = monotonic_now + GRACEFUL_STOP_TIMEOUT
|
|
117
|
+
@pending_respawns.clear
|
|
96
118
|
relay(Signals::STOP)
|
|
97
119
|
end
|
|
98
120
|
|
|
121
|
+
# Same bound as a phased restart's `wait_for_exit`: a child that hasn't
|
|
122
|
+
# exited GRACEFUL_STOP_TIMEOUT after the stop was relayed is
|
|
123
|
+
# force-killed (once), so one hung child can't keep the supervisor —
|
|
124
|
+
# and whatever is waiting on it — alive forever.
|
|
125
|
+
def kill_stragglers
|
|
126
|
+
return unless @stop_deadline && monotonic_now > @stop_deadline
|
|
127
|
+
|
|
128
|
+
@stop_deadline = nil
|
|
129
|
+
@children.each_key do |pid|
|
|
130
|
+
Cogworker.logger.warn { "swarm: child pid=#{pid} didn't stop within #{GRACEFUL_STOP_TIMEOUT}s, killing" }
|
|
131
|
+
safe_kill(pid, 'KILL')
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
99
135
|
# Non-phased: signal every child at once; the normal reap/respawn path
|
|
100
136
|
# below brings each one back as it exits, so there's a brief capacity
|
|
101
137
|
# dip but no explicit special-casing needed. Phased: replace children
|
|
@@ -155,7 +191,31 @@ module Cogworker
|
|
|
155
191
|
if @stopping
|
|
156
192
|
Cogworker.logger.info { "swarm: child pid=#{pid} slot=#{slot} exited" }
|
|
157
193
|
else
|
|
158
|
-
|
|
194
|
+
schedule_respawn(pid, slot)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def schedule_respawn(pid, slot)
|
|
199
|
+
uptime = monotonic_now - @started_at.fetch(slot, monotonic_now)
|
|
200
|
+
@crash_streak[slot] = uptime < HEALTHY_UPTIME ? @crash_streak[slot] + 1 : 0
|
|
201
|
+
delay = respawn_delay(@crash_streak[slot])
|
|
202
|
+
Cogworker.logger.warn do
|
|
203
|
+
"swarm: child pid=#{pid} slot=#{slot} exited unexpectedly after #{uptime.round(1)}s, " \
|
|
204
|
+
"respawning in #{delay}s"
|
|
205
|
+
end
|
|
206
|
+
@pending_respawns[slot] = monotonic_now + delay
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def respawn_delay(streak)
|
|
210
|
+
return 0 if streak <= 1
|
|
211
|
+
|
|
212
|
+
[RESPAWN_BASE_DELAY * (2**(streak - 2)), RESPAWN_MAX_DELAY].min
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def respawn_due_children
|
|
216
|
+
now = monotonic_now
|
|
217
|
+
@pending_respawns.select { |_slot, at| at <= now }.each_key do |slot|
|
|
218
|
+
@pending_respawns.delete(slot)
|
|
159
219
|
fork_child(slot)
|
|
160
220
|
end
|
|
161
221
|
end
|
data/lib/cogworker/testing.rb
CHANGED
|
@@ -14,8 +14,9 @@ module Cogworker
|
|
|
14
14
|
# but the normalized job hash is appended to `jobs_for(klass_name)`
|
|
15
15
|
# instead of touching Redis — nothing is scheduled or executed. Use
|
|
16
16
|
# `SomeJob.jobs` (added to `Job::ClassMethods`) to assert what got
|
|
17
|
-
# pushed, `SomeJob.
|
|
18
|
-
#
|
|
17
|
+
# pushed, `SomeJob.perform_one`/`SomeJob.drain`/`Testing.drain_all` to
|
|
18
|
+
# then actually run them, `SomeJob.clear`/`Testing.clear_jobs!` to reset
|
|
19
|
+
# between examples.
|
|
19
20
|
# - `:inline` — the job runs synchronously, right here in the calling
|
|
20
21
|
# thread, through the real server middleware chain
|
|
21
22
|
# (`Cogworker.config.server_chain`) — same as `Processor#execute`, minus
|
|
@@ -75,8 +76,31 @@ module Cogworker
|
|
|
75
76
|
registry.clear
|
|
76
77
|
end
|
|
77
78
|
|
|
79
|
+
# Fake mode's "now run them": `perform_one` takes the oldest recorded
|
|
80
|
+
# job for a class off its fake queue and runs it via `perform_inline`
|
|
81
|
+
# (real server middleware chain, errors propagate); `drain` repeats
|
|
82
|
+
# until that class's queue is empty, `drain_all` until every class's
|
|
83
|
+
# is — both also running whatever those jobs push in turn. Also
|
|
84
|
+
# reachable as `SomeJob.perform_one`/`SomeJob.drain`.
|
|
85
|
+
def perform_one(klass_name)
|
|
86
|
+
job = jobs_for(klass_name).shift
|
|
87
|
+
raise EmptyQueueError, "no #{klass_name} jobs recorded" unless job
|
|
88
|
+
|
|
89
|
+
perform_inline(job)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def drain(klass_name)
|
|
93
|
+
perform_one(klass_name) until jobs_for(klass_name).empty?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def drain_all
|
|
97
|
+
while (klass_name = registry.find { |_name, jobs| jobs.any? }&.first)
|
|
98
|
+
drain(klass_name)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
78
102
|
# Runs one job synchronously through the real server middleware chain.
|
|
79
|
-
#
|
|
103
|
+
# Called from `Client.push` while `inline?`, and by `perform_one`.
|
|
80
104
|
def perform_inline(job)
|
|
81
105
|
klass = Object.const_get(job['class'])
|
|
82
106
|
worker = klass.new
|
|
@@ -9,15 +9,14 @@ module Cogworker
|
|
|
9
9
|
# continue down the chain to the real push. A job that loses the claim
|
|
10
10
|
# (another copy is already enqueued/scheduled/running) never reaches
|
|
11
11
|
# `dispatch` at all — it sets `job['unique_skipped']` instead, which
|
|
12
|
-
# `Client.push` reads to return `nil` rather than a jid
|
|
13
|
-
# "nothing was actually pushed" signal
|
|
12
|
+
# `Client.push` reads to return `nil` rather than a jid — the
|
|
13
|
+
# "nothing was actually pushed" signal to the caller.
|
|
14
14
|
class ClientMiddleware
|
|
15
15
|
def call(_worker_class, job, _queue, redis_pool = Cogworker.config.redis_pool)
|
|
16
16
|
return yield unless UniqueJobs.until_executed?(job)
|
|
17
17
|
|
|
18
18
|
acquired = redis_pool.with do |c|
|
|
19
|
-
c.set(RedisKeys.unique_lock(UniqueJobs.digest(job)), job['jid'],
|
|
20
|
-
nx: true, ex: Cogworker.config.unique_lock_ttl)
|
|
19
|
+
c.set(RedisKeys.unique_lock(UniqueJobs.digest(job)), job['jid'], nx: true, ex: lock_ttl(job))
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
if acquired
|
|
@@ -26,6 +25,16 @@ module Cogworker
|
|
|
26
25
|
job['unique_skipped'] = true
|
|
27
26
|
end
|
|
28
27
|
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Counted from when the job is due, not from now: a `perform_in`/
|
|
32
|
+
# `perform_at` further out than `unique_lock_ttl` used to lose its
|
|
33
|
+
# lock before it even ran, letting a duplicate in.
|
|
34
|
+
def lock_ttl(job)
|
|
35
|
+
wait = job['at'] ? [job['at'].to_f - Time.now.to_f, 0].max : 0
|
|
36
|
+
(Cogworker.config.unique_lock_ttl + wait).ceil
|
|
37
|
+
end
|
|
29
38
|
end
|
|
30
39
|
end
|
|
31
40
|
end
|
|
@@ -22,8 +22,11 @@ module Cogworker
|
|
|
22
22
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
|
+
# Owner-checked: if this job's lock already expired and passed to a
|
|
26
|
+
# newer copy, this must not release *that* one's lock (which would let
|
|
27
|
+
# a third copy in while the newer one is still running).
|
|
25
28
|
def release(job)
|
|
26
|
-
|
|
29
|
+
OwnedKey.delete(RedisKeys.unique_lock(UniqueJobs.digest(job)), job['jid'])
|
|
27
30
|
end
|
|
28
31
|
end
|
|
29
32
|
end
|
data/lib/cogworker/version.rb
CHANGED
data/lib/cogworker/web/layout.rb
CHANGED
|
@@ -178,8 +178,8 @@ module Cogworker
|
|
|
178
178
|
end
|
|
179
179
|
|
|
180
180
|
# Cluster-wide "quiet every process" (`Routes::Workers`'s own `POST
|
|
181
|
-
# /workers/pause_all`) — the mock's "Pause intake" button.
|
|
182
|
-
#
|
|
181
|
+
# /workers/pause_all`) — the mock's "Pause intake" button. Quiet isn't
|
|
182
|
+
# a one-way trip that only a fresh process can undo:
|
|
183
183
|
# `Manager#quiet` here is a plain in-memory flag, so `resume_intake_
|
|
184
184
|
# button` below is a genuine, symmetric undo rather than a full
|
|
185
185
|
# process restart.
|
|
@@ -237,7 +237,7 @@ module Cogworker
|
|
|
237
237
|
# each one's visible text to the *browser's local* time, in
|
|
238
238
|
# `Web.time_format`. Registered on `htmx:load`, which htmx fires once
|
|
239
239
|
# for the initial page **and** again after every fragment swap — so
|
|
240
|
-
# newly-swapped-in timestamps (an auto-refreshed
|
|
240
|
+
# newly-swapped-in timestamps (an auto-refreshed Workers/Jobs/Schedules
|
|
241
241
|
# tab) get the same treatment without a full reload.
|
|
242
242
|
#
|
|
243
243
|
# `window.cogworkerFormatTime` is the same formatter exposed globally
|
|
@@ -14,9 +14,9 @@ module Cogworker
|
|
|
14
14
|
# retry timeline, last error, actions) in place of `Routes::Dead`'s
|
|
15
15
|
# flat rows. (*`Routes::Queues`' own queue-level browsing survives
|
|
16
16
|
# separately, as `Routes::Overview` layout B — this tab is the
|
|
17
|
-
# cross-queue, cross-status view; the two overlap a little on purpose
|
|
18
|
-
#
|
|
19
|
-
#
|
|
17
|
+
# cross-queue, cross-status view; the two overlap a little on purpose:
|
|
18
|
+
# an operator wants both "what's in this queue" and "find this one job
|
|
19
|
+
# wherever it is".)
|
|
20
20
|
#
|
|
21
21
|
# `?status=`/`?q=`/`?selected=` are plain query params, read fresh on
|
|
22
22
|
# every request and carried through the self-poll and every action's
|
|
@@ -67,7 +67,7 @@ module Cogworker
|
|
|
67
67
|
Jobs.respond(self, params)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
# Same "graduate back onto its queue" move `
|
|
70
|
+
# Same "graduate back onto its queue" move `/jobs/dead/retry` below/
|
|
71
71
|
# `Cogworker::Scheduled#graduate` make — `zrem` winning (not
|
|
72
72
|
# losing) gates it so two tabs retrying the same entry at once
|
|
73
73
|
# can't both requeue it; the `redis` gem's `#zrem` returns a
|
|
@@ -76,11 +76,7 @@ module Cogworker
|
|
|
76
76
|
app.post('/jobs/retrying/retry_now') do
|
|
77
77
|
raw = params['raw']
|
|
78
78
|
Cogworker.config.redis do |c|
|
|
79
|
-
|
|
80
|
-
job = JSON.parse(raw)
|
|
81
|
-
c.sadd(RedisKeys::QUEUES, job['queue'])
|
|
82
|
-
c.lpush(RedisKeys.queue(job['queue']), raw)
|
|
83
|
-
end
|
|
79
|
+
JobUtil.claim_and_requeue(c, RedisKeys::RETRY, raw)
|
|
84
80
|
end
|
|
85
81
|
Jobs.respond(self, params)
|
|
86
82
|
end
|
|
@@ -102,11 +98,7 @@ module Cogworker
|
|
|
102
98
|
app.post('/jobs/dead/retry') do
|
|
103
99
|
raw = params['raw']
|
|
104
100
|
Cogworker.config.redis do |c|
|
|
105
|
-
|
|
106
|
-
job = JSON.parse(raw)
|
|
107
|
-
c.sadd(RedisKeys::QUEUES, job['queue'])
|
|
108
|
-
c.lpush(RedisKeys.queue(job['queue']), raw)
|
|
109
|
-
end
|
|
101
|
+
JobUtil.claim_and_requeue(c, RedisKeys::DEAD, raw)
|
|
110
102
|
end
|
|
111
103
|
Jobs.respond(self, params)
|
|
112
104
|
end
|
|
@@ -254,15 +246,23 @@ module Cogworker
|
|
|
254
246
|
when :scheduled
|
|
255
247
|
delete_button('scheduled', script_name, row, params)
|
|
256
248
|
when :retrying
|
|
257
|
-
retry_now_button('retrying', script_name, row, params)
|
|
258
|
-
|
|
249
|
+
retry_html = requeueable?(row) ? retry_now_button('retrying', script_name, row, params) : ''
|
|
250
|
+
retry_html + delete_button('retrying', script_name, row, params)
|
|
259
251
|
when :dead
|
|
260
|
-
|
|
252
|
+
retry_html = requeueable?(row) ? retry_button('dead', script_name, row, params) : ''
|
|
253
|
+
retry_html + delete_button('dead', script_name, row, params)
|
|
261
254
|
else
|
|
262
255
|
''
|
|
263
256
|
end
|
|
264
257
|
end
|
|
265
258
|
|
|
259
|
+
# No Retry for an entry that can't go back on a queue (no queue of
|
|
260
|
+
# its own) or that would only fail straight back into Dead (an
|
|
261
|
+
# unparseable payload's wrapper) — Delete is all that makes sense.
|
|
262
|
+
def requeueable?(row)
|
|
263
|
+
row[:klass] != JobUtil::UNPARSEABLE_CLASS && !JobUtil.requeueable(row[:raw].to_s).nil?
|
|
264
|
+
end
|
|
265
|
+
|
|
266
266
|
def delete_button(bucket, script_name, row, params, extra: {})
|
|
267
267
|
action = Layout.path(script_name, "jobs/#{bucket}/delete")
|
|
268
268
|
form_with_extra(action, 'raw', row[:raw], 'delete', params, variant: :danger, icon: 'trash', extra: extra)
|
|
@@ -125,8 +125,8 @@ module Cogworker
|
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
# `raw` is the exact JSON string the job entry was pushed with —
|
|
128
|
-
# same "raw" identity `Routes::
|
|
129
|
-
# their own per-row delete off — so `Queue#delete` can `LREM` it
|
|
128
|
+
# same "raw" identity `Routes::Jobs`' dead/retrying actions already
|
|
129
|
+
# key their own per-row delete off — so `Queue#delete` can `LREM` it
|
|
130
130
|
# back out of the list.
|
|
131
131
|
app.post('/overview/:name/delete') do
|
|
132
132
|
name = url_params('name')
|
|
@@ -167,15 +167,9 @@ module Cogworker
|
|
|
167
167
|
def retry_all(name)
|
|
168
168
|
entries = Cogworker.config.redis { |c| c.zrange(RedisKeys::RETRY, 0, -1) }
|
|
169
169
|
entries.each do |raw|
|
|
170
|
-
|
|
171
|
-
next unless job['queue'] == name
|
|
170
|
+
next unless JobUtil.requeueable(raw)&.fetch('queue') == name
|
|
172
171
|
|
|
173
|
-
Cogworker.config.redis
|
|
174
|
-
if c.zrem(RedisKeys::RETRY, raw)
|
|
175
|
-
c.sadd(RedisKeys::QUEUES, job['queue'])
|
|
176
|
-
c.lpush(RedisKeys.queue(job['queue']), raw)
|
|
177
|
-
end
|
|
178
|
-
end
|
|
172
|
+
Cogworker.config.redis { |c| JobUtil.claim_and_requeue(c, RedisKeys::RETRY, raw) }
|
|
179
173
|
end
|
|
180
174
|
end
|
|
181
175
|
|
|
@@ -447,8 +441,8 @@ module Cogworker
|
|
|
447
441
|
# `processed`/`failed`. Shared between the initial render
|
|
448
442
|
# (`throughput_section`, baked into the page) and `/overview/
|
|
449
443
|
# throughput_data` (what the chart's own poll re-fetches from then
|
|
450
|
-
# on), so the two can never drift apart — same pattern
|
|
451
|
-
#
|
|
444
|
+
# on), so the two can never drift apart — same pattern
|
|
445
|
+
# `runs_per_day_payload` above uses for the Runs-per-day chart.
|
|
452
446
|
def throughput_payload
|
|
453
447
|
series = Cogworker::Throughput.series
|
|
454
448
|
{
|
|
@@ -52,12 +52,12 @@ module Cogworker
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
app.post('/schedules/:pjid/disable') do
|
|
55
|
-
Cogworker.config.redis { |c| c.sadd(RedisKeys::PERIODIC_DISABLED, url_params('pjid')) }
|
|
55
|
+
Cogworker.config.redis { |c| c.sadd?(RedisKeys::PERIODIC_DISABLED, url_params('pjid')) }
|
|
56
56
|
Routes::Schedules.respond(self)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
app.post('/schedules/:pjid/enable') do
|
|
60
|
-
Cogworker.config.redis { |c| c.srem(RedisKeys::PERIODIC_DISABLED, url_params('pjid')) }
|
|
60
|
+
Cogworker.config.redis { |c| c.srem?(RedisKeys::PERIODIC_DISABLED, url_params('pjid')) }
|
|
61
61
|
Routes::Schedules.respond(self)
|
|
62
62
|
end
|
|
63
63
|
end
|
data/lib/cogworker/web.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Cogworker
|
|
|
20
20
|
# Every built-in tab lives under here; touching each constant once
|
|
21
21
|
# forces Zeitwerk to load that file (and run its
|
|
22
22
|
# `Cogworker::Web.register(...)` bottom-of-file side effect) — nothing
|
|
23
|
-
# else ever references `Routes::
|
|
23
|
+
# else ever references `Routes::Overview` etc. by name, so without this
|
|
24
24
|
# they would simply never load.
|
|
25
25
|
BUILT_IN_ROUTE_NAMES = %i[Overview Jobs Schedules Workers History SaveSession].freeze
|
|
26
26
|
DEFAULT_TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
|
|
@@ -94,8 +94,20 @@ module Cogworker
|
|
|
94
94
|
@tabs ||= {}
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# The built-in session cookie's signing secret. The gem itself never
|
|
98
|
+
# reads the session — it's there for an auth middleware added via
|
|
99
|
+
# `.use` — but a random per-process default means each Puma worker
|
|
100
|
+
# signs with its own key, so that middleware's session doesn't survive
|
|
101
|
+
# a request landing on a different worker. Set it explicitly
|
|
102
|
+
# (`Cogworker::Web.session_secret = ...`, at least 64 characters) or
|
|
103
|
+
# via `COGWORKER_SESSION_SECRET` for any multi-process deployment.
|
|
97
104
|
def session_secret
|
|
98
|
-
@session_secret ||= SecureRandom.hex(32)
|
|
105
|
+
@session_secret ||= ENV.fetch('COGWORKER_SESSION_SECRET') { SecureRandom.hex(32) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def session_secret=(secret)
|
|
109
|
+
@session_secret = secret
|
|
110
|
+
@app = nil
|
|
99
111
|
end
|
|
100
112
|
|
|
101
113
|
# How every `Layout.time_tag` timestamp is rendered — a Ruby
|
|
@@ -132,10 +144,11 @@ module Cogworker
|
|
|
132
144
|
end
|
|
133
145
|
|
|
134
146
|
# How often (in seconds) every auto-refreshing tab polls while the
|
|
135
|
-
# global live-update toggle is on —
|
|
136
|
-
#
|
|
137
|
-
# History's
|
|
138
|
-
# so there's a single knob rather than one per
|
|
147
|
+
# global live-update toggle is on — the htmx-polled Overview/Jobs/
|
|
148
|
+
# Schedules/Workers tabs (`Layout.poll_div`'s `hx-trigger="every Ns
|
|
149
|
+
# [...]"`), History's AG Grid poll and Overview's Chart.js polls all
|
|
150
|
+
# share this one value, so there's a single knob rather than one per
|
|
151
|
+
# tab. Set this from your
|
|
139
152
|
# own init file/config.ru, e.g. `Cogworker::Web.live_update_interval = 10`.
|
|
140
153
|
def live_update_interval
|
|
141
154
|
@live_update_interval ||= DEFAULT_LIVE_UPDATE_INTERVAL
|
|
@@ -237,10 +250,10 @@ module Cogworker
|
|
|
237
250
|
builder.use(Rack::Session::Cookie, secret: session_secret, key: 'cogworker.session')
|
|
238
251
|
# No `cache_control:` (no `immutable`/long `max-age`): these files
|
|
239
252
|
# are plain, unfingerprinted paths that *do* change — every time
|
|
240
|
-
#
|
|
253
|
+
# a vendored stylesheet/script gets edited, or on any gem upgrade —
|
|
241
254
|
# and `immutable` previously told browsers to keep serving a stale
|
|
242
255
|
# cached copy under the old URL for up to a year with no
|
|
243
|
-
# revalidation at all (caught by hand: a real edit to
|
|
256
|
+
# revalidation at all (caught by hand: a real edit to a vendored stylesheet
|
|
244
257
|
# didn't show up in an already-open tab until a hard reload).
|
|
245
258
|
# `Rack::Static`/`Rack::Files` already sends `Last-Modified` and
|
|
246
259
|
# honors conditional GETs by default, which is all that's needed
|
data/lib/cogworker.rb
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require 'socket'
|
|
4
4
|
require 'securerandom'
|
|
5
|
+
# `Time#iso8601` (Logging, Web::Layout.time_tag) only moved into core in
|
|
6
|
+
# Ruby 3.4; on 3.1-3.3 it needs the stdlib extension loaded explicitly.
|
|
7
|
+
require 'time'
|
|
5
8
|
require 'zeitwerk'
|
|
6
9
|
|
|
7
10
|
# Redis-backed background job processing: worker DSL, periodic (cron)
|
|
@@ -85,7 +88,8 @@ module Cogworker
|
|
|
85
88
|
end
|
|
86
89
|
|
|
87
90
|
# No-op: exists so calling code doesn't need to change, but this gem
|
|
88
|
-
# simply never coerces job args, so there is nothing to toggle
|
|
91
|
+
# simply never coerces job args, so there is nothing to toggle — see
|
|
92
|
+
# README "Job arguments".
|
|
89
93
|
def strict_args!(_value = true)
|
|
90
94
|
true
|
|
91
95
|
end
|
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- vdonec
|
|
@@ -224,10 +224,12 @@ files:
|
|
|
224
224
|
- lib/cogworker/logging.rb
|
|
225
225
|
- lib/cogworker/manager.rb
|
|
226
226
|
- lib/cogworker/middleware/chain.rb
|
|
227
|
+
- lib/cogworker/owned_key.rb
|
|
227
228
|
- lib/cogworker/periodic/claim.lua
|
|
228
229
|
- lib/cogworker/periodic/entry.rb
|
|
229
230
|
- lib/cogworker/periodic/manager.rb
|
|
230
231
|
- lib/cogworker/periodic/release_middleware.rb
|
|
232
|
+
- lib/cogworker/periodic/running_lock.rb
|
|
231
233
|
- lib/cogworker/periodic/ticker.rb
|
|
232
234
|
- lib/cogworker/process.rb
|
|
233
235
|
- lib/cogworker/process_set.rb
|
|
@@ -236,6 +238,7 @@ files:
|
|
|
236
238
|
- lib/cogworker/queue.rb
|
|
237
239
|
- lib/cogworker/redis_connection.rb
|
|
238
240
|
- lib/cogworker/redis_keys.rb
|
|
241
|
+
- lib/cogworker/reliable_fetch.rb
|
|
239
242
|
- lib/cogworker/scheduled.rb
|
|
240
243
|
- lib/cogworker/signals.rb
|
|
241
244
|
- lib/cogworker/stats.rb
|
|
@@ -246,6 +249,7 @@ files:
|
|
|
246
249
|
- lib/cogworker/status/worker.rb
|
|
247
250
|
- lib/cogworker/swarm.rb
|
|
248
251
|
- lib/cogworker/testing.rb
|
|
252
|
+
- lib/cogworker/testing/empty_queue_error.rb
|
|
249
253
|
- lib/cogworker/throughput.rb
|
|
250
254
|
- lib/cogworker/unique_jobs.rb
|
|
251
255
|
- lib/cogworker/unique_jobs/client_middleware.rb
|