cogworker 0.2.2 → 0.4.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 +60 -2
- data/lib/cogworker/cli.rb +11 -1
- data/lib/cogworker/client.rb +1 -1
- data/lib/cogworker/config.rb +54 -1
- data/lib/cogworker/heartbeat.rb +79 -7
- data/lib/cogworker/job.rb +9 -0
- data/lib/cogworker/job_util.rb +61 -0
- data/lib/cogworker/launcher.rb +38 -1
- data/lib/cogworker/logging.rb +4 -0
- data/lib/cogworker/manager.rb +66 -4
- data/lib/cogworker/owned_key.rb +40 -0
- data/lib/cogworker/periodic/claim.lua +16 -4
- data/lib/cogworker/periodic/release_middleware.rb +31 -13
- data/lib/cogworker/periodic/running_lock.rb +69 -0
- data/lib/cogworker/periodic/ticker.rb +78 -13
- data/lib/cogworker/process.rb +2 -2
- data/lib/cogworker/process_set.rb +1 -1
- data/lib/cogworker/processor.rb +144 -14
- data/lib/cogworker/queue.rb +4 -4
- data/lib/cogworker/redis_keys.rb +9 -0
- data/lib/cogworker/reliable_fetch/unsupported_error.rb +11 -0
- data/lib/cogworker/reliable_fetch.rb +225 -0
- data/lib/cogworker/scheduled.rb +58 -10
- data/lib/cogworker/stats.rb +1 -1
- data/lib/cogworker/status.rb +5 -1
- data/lib/cogworker/swarm.rb +65 -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/optional_session.rb +25 -0
- data/lib/cogworker/web/routes/jobs.rb +72 -37
- data/lib/cogworker/web/routes/overview.rb +6 -12
- data/lib/cogworker/web/routes/schedules.rb +2 -2
- data/lib/cogworker/web.rb +36 -10
- data/lib/cogworker.rb +16 -1
- metadata +7 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'cgi'
|
|
4
|
+
require 'digest/sha1'
|
|
4
5
|
require 'json'
|
|
5
6
|
|
|
6
7
|
module Cogworker
|
|
@@ -14,9 +15,9 @@ module Cogworker
|
|
|
14
15
|
# retry timeline, last error, actions) in place of `Routes::Dead`'s
|
|
15
16
|
# flat rows. (*`Routes::Queues`' own queue-level browsing survives
|
|
16
17
|
# 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
|
-
#
|
|
18
|
+
# cross-queue, cross-status view; the two overlap a little on purpose:
|
|
19
|
+
# an operator wants both "what's in this queue" and "find this one job
|
|
20
|
+
# wherever it is".)
|
|
20
21
|
#
|
|
21
22
|
# `?status=`/`?q=`/`?selected=` are plain query params, read fresh on
|
|
22
23
|
# every request and carried through the self-poll and every action's
|
|
@@ -63,37 +64,30 @@ module Cogworker
|
|
|
63
64
|
app.post('/jobs/retrying/delete') do
|
|
64
65
|
raw = params['raw']
|
|
65
66
|
Cogworker.config.redis { |c| c.zrem(RedisKeys::RETRY, raw) }
|
|
66
|
-
Cogworker::Attempts.clear(
|
|
67
|
+
Cogworker::Attempts.clear(Jobs.parse_entry(raw)['jid'])
|
|
67
68
|
Jobs.respond(self, params)
|
|
68
69
|
end
|
|
69
70
|
|
|
70
|
-
# Same "graduate back onto its queue" move `
|
|
71
|
-
# `Cogworker::Scheduled#graduate` make
|
|
72
|
-
#
|
|
73
|
-
# can't both requeue it
|
|
74
|
-
#
|
|
75
|
-
# `== 1` (`true == 1` is `false` in Ruby).
|
|
71
|
+
# Same "graduate back onto its queue" move `/jobs/dead/retry` below/
|
|
72
|
+
# `Cogworker::Scheduled#graduate` make (`JobUtil.claim_and_requeue`:
|
|
73
|
+
# validate, then claim + push atomically) — two tabs retrying the
|
|
74
|
+
# same entry at once can't both requeue it, and an entry that
|
|
75
|
+
# can't be requeued is left in place with a notice.
|
|
76
76
|
app.post('/jobs/retrying/retry_now') do
|
|
77
77
|
raw = params['raw']
|
|
78
|
-
Cogworker.config.redis
|
|
79
|
-
|
|
80
|
-
job = JSON.parse(raw)
|
|
81
|
-
c.sadd(RedisKeys::QUEUES, job['queue'])
|
|
82
|
-
c.lpush(RedisKeys.queue(job['queue']), raw)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
Jobs.respond(self, params)
|
|
78
|
+
result = Cogworker.config.redis { |c| JobUtil.claim_and_requeue(c, RedisKeys::RETRY, raw) }
|
|
79
|
+
Jobs.respond(self, params, notice: result == :invalid ? 'not_requeueable' : nil)
|
|
86
80
|
end
|
|
87
81
|
|
|
88
82
|
app.post('/jobs/dead/delete') do
|
|
89
83
|
raw = params['raw']
|
|
90
84
|
Cogworker.config.redis { |c| c.zrem(RedisKeys::DEAD, raw) }
|
|
91
|
-
Cogworker::Attempts.clear(
|
|
85
|
+
Cogworker::Attempts.clear(Jobs.parse_entry(raw)['jid'])
|
|
92
86
|
Jobs.respond(self, params)
|
|
93
87
|
end
|
|
94
88
|
|
|
95
89
|
app.post('/jobs/dead/delete_all') do
|
|
96
|
-
jids = Cogworker.config.redis { |c| c.zrange(RedisKeys::DEAD, 0, -1) }.map { |raw|
|
|
90
|
+
jids = Cogworker.config.redis { |c| c.zrange(RedisKeys::DEAD, 0, -1) }.map { |raw| Jobs.parse_entry(raw)['jid'] }
|
|
97
91
|
Cogworker.config.redis { |c| c.del(RedisKeys::DEAD) }
|
|
98
92
|
jids.each { |jid| Cogworker::Attempts.clear(jid) }
|
|
99
93
|
Jobs.respond(self, params)
|
|
@@ -101,14 +95,8 @@ module Cogworker
|
|
|
101
95
|
|
|
102
96
|
app.post('/jobs/dead/retry') do
|
|
103
97
|
raw = params['raw']
|
|
104
|
-
Cogworker.config.redis
|
|
105
|
-
|
|
106
|
-
job = JSON.parse(raw)
|
|
107
|
-
c.sadd(RedisKeys::QUEUES, job['queue'])
|
|
108
|
-
c.lpush(RedisKeys.queue(job['queue']), raw)
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
Jobs.respond(self, params)
|
|
98
|
+
result = Cogworker.config.redis { |c| JobUtil.claim_and_requeue(c, RedisKeys::DEAD, raw) }
|
|
99
|
+
Jobs.respond(self, params, notice: result == :invalid ? 'not_requeueable' : nil)
|
|
112
100
|
end
|
|
113
101
|
|
|
114
102
|
# Moves this one entry's score on its own ZSET — "run sooner" or
|
|
@@ -152,11 +140,16 @@ module Cogworker
|
|
|
152
140
|
# #jobs-content in place, keeping the same filter/search/selection;
|
|
153
141
|
# a plain form submission (no JS) falls back to a normal redirect
|
|
154
142
|
# to that same URL.
|
|
155
|
-
|
|
143
|
+
# `notice:` is a key into NOTICES (never free text — it's echoed into
|
|
144
|
+
# the page). It shows once: the redirect carries it as `?notice=`,
|
|
145
|
+
# but `query_string` (and so every poll URL) leaves it out.
|
|
146
|
+
def respond(action, params, notice: nil)
|
|
156
147
|
if action.hx_request?
|
|
157
|
-
render_content(action.request.script_name, params)
|
|
148
|
+
render_content(action.request.script_name, params.merge('notice' => notice))
|
|
158
149
|
else
|
|
159
|
-
|
|
150
|
+
location = "jobs#{query_string(params)}"
|
|
151
|
+
location += "#{location.include?('?') ? '&' : '?'}notice=#{CGI.escape(notice)}" if notice
|
|
152
|
+
action.redirect(Layout.path(action.request.script_name, location))
|
|
160
153
|
end
|
|
161
154
|
end
|
|
162
155
|
|
|
@@ -171,6 +164,19 @@ module Cogworker
|
|
|
171
164
|
parts.empty? ? '' : "?#{parts.join('&')}"
|
|
172
165
|
end
|
|
173
166
|
|
|
167
|
+
NOTICES = {
|
|
168
|
+
'not_requeueable' => "This entry can't be put back on a queue — it's unreadable or has no queue " \
|
|
169
|
+
'of its own. Delete it instead.'
|
|
170
|
+
}.freeze
|
|
171
|
+
|
|
172
|
+
def notice_banner(params)
|
|
173
|
+
text = NOTICES[params['notice'].to_s]
|
|
174
|
+
return '' unless text
|
|
175
|
+
|
|
176
|
+
style = 'padding: 10px 14px; border-left: 3px solid var(--color-danger);'
|
|
177
|
+
%(<div class="card" role="status" style="#{style}">#{Layout.h(text)}</div>)
|
|
178
|
+
end
|
|
179
|
+
|
|
174
180
|
def render_content(script_name, params)
|
|
175
181
|
status = STATUSES.include?(params['status']) ? params['status'] : 'All'
|
|
176
182
|
query = params['q'].to_s
|
|
@@ -183,6 +189,7 @@ module Cogworker
|
|
|
183
189
|
|
|
184
190
|
<<~HTML
|
|
185
191
|
<div style="display: flex; flex-direction: column; gap: 16px;">
|
|
192
|
+
#{notice_banner(params)}
|
|
186
193
|
#{page_header(script_name, status, query, filtered.size, rows.size)}
|
|
187
194
|
<div style="display: grid; grid-template-columns: minmax(0, 1fr) #{selected ? 'minmax(320px, 400px)' : '0px'}; gap: 16px; align-items: start;">
|
|
188
195
|
#{jobs_table(filtered, script_name, params)}
|
|
@@ -254,15 +261,25 @@ module Cogworker
|
|
|
254
261
|
when :scheduled
|
|
255
262
|
delete_button('scheduled', script_name, row, params)
|
|
256
263
|
when :retrying
|
|
257
|
-
retry_now_button('retrying', script_name, row, params)
|
|
258
|
-
|
|
264
|
+
retry_html = requeueable?(row) ? retry_now_button('retrying', script_name, row, params) : ''
|
|
265
|
+
retry_html + delete_button('retrying', script_name, row, params)
|
|
259
266
|
when :dead
|
|
260
|
-
|
|
267
|
+
retry_html = requeueable?(row) ? retry_button('dead', script_name, row, params) : ''
|
|
268
|
+
retry_html + delete_button('dead', script_name, row, params)
|
|
261
269
|
else
|
|
262
270
|
''
|
|
263
271
|
end
|
|
264
272
|
end
|
|
265
273
|
|
|
274
|
+
# No Retry for an entry that can't go back on a queue (no queue of
|
|
275
|
+
# its own) or that would only fail straight back into Dead (an
|
|
276
|
+
# unparseable payload's wrapper) — Delete is all that makes sense.
|
|
277
|
+
# From the row's already-parsed fields — this runs for every row on
|
|
278
|
+
# every poll, so no re-parsing `raw` here.
|
|
279
|
+
def requeueable?(row)
|
|
280
|
+
row[:klass] != JobUtil::UNPARSEABLE_CLASS && row[:queue].is_a?(String) && !row[:queue].empty?
|
|
281
|
+
end
|
|
282
|
+
|
|
266
283
|
def delete_button(bucket, script_name, row, params, extra: {})
|
|
267
284
|
action = Layout.path(script_name, "jobs/#{bucket}/delete")
|
|
268
285
|
form_with_extra(action, 'raw', row[:raw], 'delete', params, variant: :danger, icon: 'trash', extra: extra)
|
|
@@ -430,10 +447,28 @@ module Cogworker
|
|
|
430
447
|
end
|
|
431
448
|
end
|
|
432
449
|
|
|
450
|
+
# A ZSET entry that isn't a job Hash (corrupt, or written by
|
|
451
|
+
# something else) shows up as an `(unparseable)` row — Delete is
|
|
452
|
+
# offered, Retry isn't (`requeueable?`) — instead of a JSON::ParserError
|
|
453
|
+
# taking the whole tab down.
|
|
454
|
+
def parse_entry(raw)
|
|
455
|
+
job = JSON.parse(raw)
|
|
456
|
+
return job if job.is_a?(Hash)
|
|
457
|
+
|
|
458
|
+
unparseable_entry(raw)
|
|
459
|
+
rescue JSON::ParserError
|
|
460
|
+
unparseable_entry(raw)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def unparseable_entry(raw)
|
|
464
|
+
{ 'class' => JobUtil::UNPARSEABLE_CLASS, 'jid' => "unparseable-#{Digest::SHA1.hexdigest(raw.to_s)[0, 12]}",
|
|
465
|
+
'args' => [], 'error_message' => raw.to_s[0, 200] }
|
|
466
|
+
end
|
|
467
|
+
|
|
433
468
|
def scheduled_rows
|
|
434
469
|
entries = Cogworker.config.redis { |c| c.zrange(RedisKeys::SCHEDULE, 0, -1, withscores: true) }
|
|
435
470
|
entries.map do |raw, score|
|
|
436
|
-
job =
|
|
471
|
+
job = parse_entry(raw)
|
|
437
472
|
{ jid: job['jid'], klass: job['class'], queue: job['queue'], status: 'Scheduled', attempt: '—',
|
|
438
473
|
next_action: score, at: job['created_at'], args: job['args'], error_class: nil, error_message: nil,
|
|
439
474
|
raw: raw, source: :scheduled }
|
|
@@ -443,7 +478,7 @@ module Cogworker
|
|
|
443
478
|
def retrying_rows
|
|
444
479
|
entries = Cogworker.config.redis { |c| c.zrange(RedisKeys::RETRY, 0, -1, withscores: true) }
|
|
445
480
|
entries.map do |raw, score|
|
|
446
|
-
job =
|
|
481
|
+
job = parse_entry(raw)
|
|
447
482
|
{ jid: job['jid'], klass: job['class'], queue: job['queue'], status: 'Retrying',
|
|
448
483
|
attempt: "#{job['retry_count'].to_i} of #{JobUtil.max_retries(job)}", next_action: score,
|
|
449
484
|
at: job['failed_at'] || job['created_at'], args: job['args'], error_class: job['error_class'],
|
|
@@ -454,7 +489,7 @@ module Cogworker
|
|
|
454
489
|
def dead_rows
|
|
455
490
|
entries = Cogworker.config.redis { |c| c.zrevrange(RedisKeys::DEAD, 0, -1, withscores: true) }
|
|
456
491
|
entries.map do |raw, score|
|
|
457
|
-
job =
|
|
492
|
+
job = parse_entry(raw)
|
|
458
493
|
{ jid: job['jid'], klass: job['class'], queue: job['queue'], status: 'Dead',
|
|
459
494
|
attempt: "#{job['retry_count'].to_i} of #{JobUtil.max_retries(job)}", next_action: nil, at: score,
|
|
460
495
|
args: job['args'], error_class: job['error_class'], error_message: job['error_message'], raw: raw,
|
|
@@ -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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'rack'
|
|
4
|
-
require 'rack/session/cookie'
|
|
5
4
|
require 'rack/static'
|
|
6
5
|
require 'securerandom'
|
|
7
6
|
|
|
@@ -20,7 +19,7 @@ module Cogworker
|
|
|
20
19
|
# Every built-in tab lives under here; touching each constant once
|
|
21
20
|
# forces Zeitwerk to load that file (and run its
|
|
22
21
|
# `Cogworker::Web.register(...)` bottom-of-file side effect) — nothing
|
|
23
|
-
# else ever references `Routes::
|
|
22
|
+
# else ever references `Routes::Overview` etc. by name, so without this
|
|
24
23
|
# they would simply never load.
|
|
25
24
|
BUILT_IN_ROUTE_NAMES = %i[Overview Jobs Schedules Workers History SaveSession].freeze
|
|
26
25
|
DEFAULT_TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
|
|
@@ -94,8 +93,32 @@ module Cogworker
|
|
|
94
93
|
@tabs ||= {}
|
|
95
94
|
end
|
|
96
95
|
|
|
96
|
+
# The built-in session cookie's signing secret. The gem itself never
|
|
97
|
+
# reads the session — it's there for an auth middleware added via
|
|
98
|
+
# `.use` — but a random per-process default means each Puma worker
|
|
99
|
+
# signs with its own key, so that middleware's session doesn't survive
|
|
100
|
+
# a request landing on a different worker. Set it explicitly
|
|
101
|
+
# (`Cogworker::Web.session_secret = ...`, at least 64 characters) or
|
|
102
|
+
# via `COGWORKER_SESSION_SECRET` for any multi-process deployment.
|
|
97
103
|
def session_secret
|
|
98
|
-
@session_secret ||= SecureRandom.hex(32)
|
|
104
|
+
@session_secret ||= ENV.fetch('COGWORKER_SESSION_SECRET') { SecureRandom.hex(32) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# `false` drops the built-in session cookie entirely (it's already
|
|
108
|
+
# skipped automatically whenever the host app provides a session —
|
|
109
|
+
# see OptionalSession). Default true.
|
|
110
|
+
def builtin_session
|
|
111
|
+
@builtin_session.nil? || @builtin_session
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def builtin_session=(enabled)
|
|
115
|
+
@builtin_session = enabled
|
|
116
|
+
@app = nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def session_secret=(secret)
|
|
120
|
+
@session_secret = secret
|
|
121
|
+
@app = nil
|
|
99
122
|
end
|
|
100
123
|
|
|
101
124
|
# How every `Layout.time_tag` timestamp is rendered — a Ruby
|
|
@@ -132,10 +155,11 @@ module Cogworker
|
|
|
132
155
|
end
|
|
133
156
|
|
|
134
157
|
# 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
|
|
158
|
+
# global live-update toggle is on — the htmx-polled Overview/Jobs/
|
|
159
|
+
# Schedules/Workers tabs (`Layout.poll_div`'s `hx-trigger="every Ns
|
|
160
|
+
# [...]"`), History's AG Grid poll and Overview's Chart.js polls all
|
|
161
|
+
# share this one value, so there's a single knob rather than one per
|
|
162
|
+
# tab. Set this from your
|
|
139
163
|
# own init file/config.ru, e.g. `Cogworker::Web.live_update_interval = 10`.
|
|
140
164
|
def live_update_interval
|
|
141
165
|
@live_update_interval ||= DEFAULT_LIVE_UPDATE_INTERVAL
|
|
@@ -234,13 +258,15 @@ module Cogworker
|
|
|
234
258
|
|
|
235
259
|
def build_app
|
|
236
260
|
builder = Rack::Builder.new
|
|
237
|
-
|
|
261
|
+
if builtin_session
|
|
262
|
+
builder.use(Cogworker::Web::OptionalSession, secret: session_secret, key: 'cogworker.session')
|
|
263
|
+
end
|
|
238
264
|
# No `cache_control:` (no `immutable`/long `max-age`): these files
|
|
239
265
|
# are plain, unfingerprinted paths that *do* change — every time
|
|
240
|
-
#
|
|
266
|
+
# a vendored stylesheet/script gets edited, or on any gem upgrade —
|
|
241
267
|
# and `immutable` previously told browsers to keep serving a stale
|
|
242
268
|
# cached copy under the old URL for up to a year with no
|
|
243
|
-
# revalidation at all (caught by hand: a real edit to
|
|
269
|
+
# revalidation at all (caught by hand: a real edit to a vendored stylesheet
|
|
244
270
|
# didn't show up in an already-open tab until a hard reload).
|
|
245
271
|
# `Rack::Static`/`Rack::Files` already sends `Last-Modified` and
|
|
246
272
|
# 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)
|
|
@@ -80,12 +83,24 @@ module Cogworker
|
|
|
80
83
|
!!@server_process
|
|
81
84
|
end
|
|
82
85
|
|
|
86
|
+
# Called right before every `Process.exit!` (which, unlike `exit`,
|
|
87
|
+
# flushes nothing): a custom `Cogworker.logger` writing to a buffered
|
|
88
|
+
# stdout/stderr would otherwise lose its last lines.
|
|
89
|
+
def flush_output!
|
|
90
|
+
[$stdout, $stderr].each do |io|
|
|
91
|
+
io.flush
|
|
92
|
+
rescue StandardError
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
83
97
|
def server_process!
|
|
84
98
|
@server_process = true
|
|
85
99
|
end
|
|
86
100
|
|
|
87
101
|
# 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
|
|
102
|
+
# simply never coerces job args, so there is nothing to toggle — see
|
|
103
|
+
# README "Job arguments".
|
|
89
104
|
def strict_args!(_value = true)
|
|
90
105
|
true
|
|
91
106
|
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.4.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,8 @@ 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
|
|
242
|
+
- lib/cogworker/reliable_fetch/unsupported_error.rb
|
|
239
243
|
- lib/cogworker/scheduled.rb
|
|
240
244
|
- lib/cogworker/signals.rb
|
|
241
245
|
- lib/cogworker/stats.rb
|
|
@@ -246,6 +250,7 @@ files:
|
|
|
246
250
|
- lib/cogworker/status/worker.rb
|
|
247
251
|
- lib/cogworker/swarm.rb
|
|
248
252
|
- lib/cogworker/testing.rb
|
|
253
|
+
- lib/cogworker/testing/empty_queue_error.rb
|
|
249
254
|
- lib/cogworker/throughput.rb
|
|
250
255
|
- lib/cogworker/unique_jobs.rb
|
|
251
256
|
- lib/cogworker/unique_jobs/client_middleware.rb
|
|
@@ -265,6 +270,7 @@ files:
|
|
|
265
270
|
- lib/cogworker/web/assets/phosphor/style.css
|
|
266
271
|
- lib/cogworker/web/history_query.rb
|
|
267
272
|
- lib/cogworker/web/layout.rb
|
|
273
|
+
- lib/cogworker/web/optional_session.rb
|
|
268
274
|
- lib/cogworker/web/router.rb
|
|
269
275
|
- lib/cogworker/web/routes/history.rb
|
|
270
276
|
- lib/cogworker/web/routes/jobs.rb
|