stampeded 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/README.md +13 -3
- data/lib/stampeded/envelope.rb +12 -7
- data/lib/stampeded/lifecycle.rb +2 -1
- data/lib/stampeded/queue_policy.rb +29 -1
- data/lib/stampeded/schedule.rb +12 -17
- data/lib/stampeded/version.rb +1 -1
- data/lib/stampeded/worker.rb +19 -6
- data/schema/0006_schedule_unknown_timezone.sql +58 -0
- data/schema/0007_queue_pause.sql +83 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db2468be1b5b64e89bd00f2bebccbafce4faf21c875065d91d5f43a394bdbd9c
|
|
4
|
+
data.tar.gz: 5f5bc056343dff4ac2c83eec8eda56334e9d1423cc22acdae97aaba046f9672d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5d126e89b18e370b2b9cee271d025eb2f68a7967aaaea185423854d99f314c88266a06dbb7b54ee3b6b3659319ff7a169c553bd3f61f08e144b182b9b06cb01
|
|
7
|
+
data.tar.gz: e77790d2458e5be336950f1590958afc7ebc52bc03be7b9c0db0a40d173ddc91b0b8c4ac092cfb8606a6d5ecd79e54aa73a42f7fee4755aabeaa3ea895789c0a
|
data/README.md
CHANGED
|
@@ -135,7 +135,8 @@ Stampeded::Schedule.recurring(
|
|
|
135
135
|
|
|
136
136
|
The daemon fires it; a new schedule first runs at its next occurrence. An
|
|
137
137
|
expression that names its hours runs once on a DST fall-back day; one using `*`
|
|
138
|
-
or a step runs on real elapsed time.
|
|
138
|
+
or a step runs on real elapsed time. The queue is `queue:`, or the job's
|
|
139
|
+
`queue_as` evaluated with `args`. `Stampeded::Schedule.pause`, `.remove` and
|
|
139
140
|
`.all` manage them.
|
|
140
141
|
|
|
141
142
|
## Workers
|
|
@@ -164,8 +165,9 @@ config.except_queues = %w[payments_dlq] # with :all
|
|
|
164
165
|
- `SIGTERM` lets in-flight jobs finish before exiting. Jobs still running after
|
|
165
166
|
`shutdown_timeout` are reclaimed by the daemon.
|
|
166
167
|
- The worker runs on `libstampeded_ffi`, the same core every runtime's worker
|
|
167
|
-
uses; see [The binaries](#the-binaries).
|
|
168
|
-
`STAMPEDED_LOG=
|
|
168
|
+
uses; see [The binaries](#the-binaries). Failures to claim or report go to
|
|
169
|
+
stderr always; `STAMPEDED_LOG=1` also logs each job, and
|
|
170
|
+
`STAMPEDED_LOG=debug` (or `info`, `warn`, ...) sets the core's level.
|
|
169
171
|
|
|
170
172
|
## Queue policies
|
|
171
173
|
|
|
@@ -178,6 +180,14 @@ The daemon turns these into a verdict per queue every second; the worker claims
|
|
|
178
180
|
rushed queues first and skips held ones. `max_running` also limits what one
|
|
179
181
|
worker process holds from that queue, with or without the daemon.
|
|
180
182
|
|
|
183
|
+
```ruby
|
|
184
|
+
Stampeded::QueuePolicy.pause("mail") # no worker in any language claims from it
|
|
185
|
+
Stampeded::QueuePolicy.resume("mail")
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Jobs already running finish, and `perform_later` and schedules keep queueing
|
|
189
|
+
behind the pause. `set` keeps a pause; `remove` lifts it.
|
|
190
|
+
|
|
181
191
|
## Without Rails
|
|
182
192
|
|
|
183
193
|
```ruby
|
data/lib/stampeded/envelope.rb
CHANGED
|
@@ -21,17 +21,22 @@ module Stampeded
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def from_active_job(job)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
build(job.class, job.arguments, job.serialize.except("arguments", "provider_job_id"))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# [task, args, meta] for `arguments` as `perform` takes them: plain keywords
|
|
28
|
+
# become portable `args`, anything else rides in ActiveJob's own format.
|
|
29
|
+
def build(job_class, arguments, context)
|
|
30
|
+
task = job_class.try(:stampeded_task)
|
|
31
|
+
kwargs = standard_arguments(arguments)
|
|
27
32
|
|
|
28
33
|
if kwargs
|
|
29
|
-
[task ||
|
|
34
|
+
[task || job_class.name, kwargs, { "activejob" => context }]
|
|
30
35
|
elsif task
|
|
31
|
-
raise PortableArgumentsError.new(
|
|
36
|
+
raise PortableArgumentsError.new(job_class.name, task)
|
|
32
37
|
else
|
|
33
|
-
context["arguments"] = ActiveJob::Arguments.serialize(
|
|
34
|
-
[
|
|
38
|
+
context["arguments"] = ActiveJob::Arguments.serialize(arguments)
|
|
39
|
+
[job_class.name, {}, { "activejob" => context }]
|
|
35
40
|
end
|
|
36
41
|
end
|
|
37
42
|
|
data/lib/stampeded/lifecycle.rb
CHANGED
|
@@ -18,7 +18,8 @@ module Stampeded
|
|
|
18
18
|
|
|
19
19
|
def events_from(state) = events.select { legal?(state, it) }
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# No worker moves it again; only an operator's resurrect does.
|
|
22
|
+
def terminal?(state) = events_from(state).all?(:resurrect)
|
|
22
23
|
|
|
23
24
|
def claimable?(state) = legal?(state, :claim)
|
|
24
25
|
|
|
@@ -21,6 +21,34 @@ module Stampeded
|
|
|
21
21
|
queue.to_s
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# No worker, in any runtime, claims from a paused queue; running jobs finish
|
|
25
|
+
# and enqueueing carries on. `set` keeps a pause; `remove` lifts it.
|
|
26
|
+
def pause(queue)
|
|
27
|
+
raise Error, "a queue policy needs a queue name" if queue.to_s.strip.empty?
|
|
28
|
+
|
|
29
|
+
Connection.translating_errors do
|
|
30
|
+
Connection.with do |c|
|
|
31
|
+
c.exec_params(<<~SQL, [queue.to_s])
|
|
32
|
+
INSERT INTO stampeded_queue_policies (queue, paused) VALUES ($1, true)
|
|
33
|
+
ON CONFLICT (queue) DO UPDATE SET paused = true, updated_at = now()
|
|
34
|
+
SQL
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
queue.to_s
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def resume(queue)
|
|
41
|
+
Connection.translating_errors do
|
|
42
|
+
Connection.with do |c|
|
|
43
|
+
c.exec_params(<<~SQL, [queue.to_s])
|
|
44
|
+
UPDATE stampeded_queue_policies SET paused = false, updated_at = now()
|
|
45
|
+
WHERE queue = $1 AND paused
|
|
46
|
+
SQL
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
queue.to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
24
52
|
def remove(queue)
|
|
25
53
|
Connection.with { |c| c.exec_params("DELETE FROM stampeded_queue_policies WHERE queue = $1", [queue.to_s]) }
|
|
26
54
|
end
|
|
@@ -29,7 +57,7 @@ module Stampeded
|
|
|
29
57
|
Connection.translating_errors do
|
|
30
58
|
Connection.with do |c|
|
|
31
59
|
c.exec(<<~SQL).to_a
|
|
32
|
-
SELECT queue, max_running, max_wait_secs, verdict, reason, decided_at
|
|
60
|
+
SELECT queue, max_running, max_wait_secs, paused, verdict, reason, decided_at
|
|
33
61
|
FROM stampeded_queue_policies ORDER BY queue
|
|
34
62
|
SQL
|
|
35
63
|
end
|
data/lib/stampeded/schedule.rb
CHANGED
|
@@ -13,9 +13,11 @@ module Stampeded
|
|
|
13
13
|
suggested = check_cron!(cron, timezone)
|
|
14
14
|
dst_policy ||= suggested
|
|
15
15
|
job_class = job.is_a?(Class) ? job : job.class
|
|
16
|
-
|
|
16
|
+
# As `perform` takes them: a Hash is keywords, anything else positional.
|
|
17
|
+
arguments = args.is_a?(Hash) ? [Hash.ruby2_keywords_hash(args)] : Array(args)
|
|
18
|
+
queue ||= resolve_queue_name(job_class, arguments) || Stampeded.config.default_queue
|
|
17
19
|
|
|
18
|
-
task, standard_args, meta = envelope(job_class,
|
|
20
|
+
task, standard_args, meta = envelope(job_class, arguments, queue, priority)
|
|
19
21
|
|
|
20
22
|
Connection.with do |c|
|
|
21
23
|
c.exec_params(<<~SQL, [key, queue, task, JSON.generate(standard_args), JSON.generate(meta), cron, timezone, dst_policy, priority])
|
|
@@ -68,18 +70,19 @@ module Stampeded
|
|
|
68
70
|
MSG
|
|
69
71
|
end
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
# A `queue_as` block sees the schedule's arguments, which are the same every run.
|
|
74
|
+
def resolve_queue_name(job_class, arguments)
|
|
72
75
|
return nil unless job_class.respond_to?(:queue_name)
|
|
73
76
|
|
|
74
77
|
name = job_class.queue_name
|
|
75
|
-
name = job_class.new.queue_name if name.respond_to?(:call)
|
|
78
|
+
name = job_class.new(*arguments).queue_name if name.respond_to?(:call)
|
|
76
79
|
name&.to_s
|
|
77
|
-
rescue StandardError
|
|
78
|
-
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
raise Error, "#{job_class.name}'s queue_as raised #{e.class}: #{e.message}; " \
|
|
82
|
+
"pass queue: to name the schedule's queue"
|
|
79
83
|
end
|
|
80
84
|
|
|
81
|
-
def envelope(job_class,
|
|
82
|
-
task = job_class.try(:stampeded_task)
|
|
85
|
+
def envelope(job_class, arguments, queue, priority)
|
|
83
86
|
context = {
|
|
84
87
|
"job_class" => job_class.name,
|
|
85
88
|
"queue_name" => queue,
|
|
@@ -88,15 +91,7 @@ module Stampeded
|
|
|
88
91
|
"timezone" => defined?(Time.zone) && Time.zone ? Time.zone.name : "UTC"
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
if kwargs && Envelope.plain?(kwargs)
|
|
93
|
-
[task || job_class.name, JSON.parse(JSON.generate(kwargs)), { "activejob" => context }]
|
|
94
|
-
elsif task
|
|
95
|
-
raise PortableArgumentsError.new(job_class.name, task)
|
|
96
|
-
else
|
|
97
|
-
context["arguments"] = ActiveJob::Arguments.serialize(Array(args))
|
|
98
|
-
[job_class.name, {}, { "activejob" => context }]
|
|
99
|
-
end
|
|
94
|
+
Envelope.build(job_class, arguments, context)
|
|
100
95
|
end
|
|
101
96
|
end
|
|
102
97
|
end
|
data/lib/stampeded/version.rb
CHANGED
data/lib/stampeded/worker.rb
CHANGED
|
@@ -17,6 +17,8 @@ module Stampeded
|
|
|
17
17
|
@batch_size = positive!(batch_size || Stampeded.config.batch_size, "batch_size")
|
|
18
18
|
@poll_interval = poll_interval || Stampeded.config.poll_interval
|
|
19
19
|
@handlers = {}
|
|
20
|
+
@warned = {}
|
|
21
|
+
@warned_lock = Mutex.new
|
|
20
22
|
@stopping = false
|
|
21
23
|
@wake_reader, @wake_writer = IO.pipe
|
|
22
24
|
end
|
|
@@ -57,8 +59,8 @@ module Stampeded
|
|
|
57
59
|
|
|
58
60
|
stragglers = @threads.count(&:alive?)
|
|
59
61
|
if stragglers.positive?
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
problem "#{stragglers} job(s) did not finish in #{Stampeded.config.shutdown_timeout}s; " \
|
|
63
|
+
"the reaper will reclaim them"
|
|
62
64
|
end
|
|
63
65
|
log "stopped"
|
|
64
66
|
ensure
|
|
@@ -66,7 +68,7 @@ module Stampeded
|
|
|
66
68
|
begin
|
|
67
69
|
puller.stop(0) if puller && !stopped
|
|
68
70
|
rescue Error => e
|
|
69
|
-
|
|
71
|
+
problem "could not stop: #{e.message}"
|
|
70
72
|
end
|
|
71
73
|
@threads&.each { |t| t.join(@poll_interval * 2) }
|
|
72
74
|
puller&.close if @threads.nil? || @threads.none?(&:alive?)
|
|
@@ -105,7 +107,7 @@ module Stampeded
|
|
|
105
107
|
begin
|
|
106
108
|
puller.next(@poll_interval)
|
|
107
109
|
rescue Error => e
|
|
108
|
-
|
|
110
|
+
problem "claim failed: #{e.message}"
|
|
109
111
|
sleep @poll_interval
|
|
110
112
|
next
|
|
111
113
|
end
|
|
@@ -146,7 +148,7 @@ module Stampeded
|
|
|
146
148
|
else lease.fail!(message, retry_in_ms: error.is_a?(RetryIn) ? error.delay_ms : nil)
|
|
147
149
|
end
|
|
148
150
|
rescue Error => e
|
|
149
|
-
|
|
151
|
+
problem "could not report #{lease.row["id"]}: #{e.message}"
|
|
150
152
|
end
|
|
151
153
|
|
|
152
154
|
def with_job_timeout(job, &block)
|
|
@@ -155,7 +157,9 @@ module Stampeded
|
|
|
155
157
|
|
|
156
158
|
expire = (job.row["expire_seconds"] || 3600).to_i
|
|
157
159
|
if limit >= expire
|
|
158
|
-
|
|
160
|
+
once([job.task, expire]) do
|
|
161
|
+
problem "job_timeout (#{limit}s) is not shorter than #{job.task}'s expire_seconds (#{expire}s): it may run twice"
|
|
162
|
+
end
|
|
159
163
|
end
|
|
160
164
|
|
|
161
165
|
Timeout.timeout(limit, Timeout::Error, "job exceeded job_timeout of #{limit}s", &block)
|
|
@@ -215,10 +219,19 @@ module Stampeded
|
|
|
215
219
|
log? ? "info" : "warn"
|
|
216
220
|
end
|
|
217
221
|
|
|
222
|
+
# Progress: each job, start and stop. Only when asked for, or on a terminal.
|
|
218
223
|
def log(message)
|
|
219
224
|
return unless log?
|
|
220
225
|
|
|
221
226
|
warn "[stampeded] #{message}"
|
|
222
227
|
end
|
|
228
|
+
|
|
229
|
+
# Something an operator must see, which a service manager's stdout is not.
|
|
230
|
+
def problem(message) = warn("[stampeded] #{message}")
|
|
231
|
+
|
|
232
|
+
def once(key)
|
|
233
|
+
first = @warned_lock.synchronize { @warned[key] ? false : (@warned[key] = true) }
|
|
234
|
+
yield if first
|
|
235
|
+
end
|
|
223
236
|
end
|
|
224
237
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- Schema 6: a wall-clock occurrence of a schedule whose zone Postgres does not
|
|
2
|
+
-- know is keyed in UTC, as the scheduler fires it. Before, the key raised, and
|
|
3
|
+
-- the error rolled back the whole tick: one mistyped zone stopped every schedule.
|
|
4
|
+
CREATE OR REPLACE FUNCTION stampeded_fire_schedule(p_key text, p_next_run_at timestamptz)
|
|
5
|
+
RETURNS uuid
|
|
6
|
+
LANGUAGE plpgsql AS $$
|
|
7
|
+
DECLARE
|
|
8
|
+
s stampeded_schedules%ROWTYPE;
|
|
9
|
+
v_token text;
|
|
10
|
+
v_job uuid;
|
|
11
|
+
-- Separate from v_job: the ledger's id is bigserial, the job's is uuid.
|
|
12
|
+
-- Reusing one variable for both is a runtime cast error, not a compile one.
|
|
13
|
+
v_occ bigint;
|
|
14
|
+
BEGIN
|
|
15
|
+
SELECT * INTO s FROM stampeded_schedules WHERE key = p_key;
|
|
16
|
+
IF NOT FOUND THEN
|
|
17
|
+
RETURN NULL;
|
|
18
|
+
END IF;
|
|
19
|
+
|
|
20
|
+
IF s.dst_policy = 'wall_clock' THEN
|
|
21
|
+
BEGIN
|
|
22
|
+
v_token := to_char(s.next_run_at AT TIME ZONE s.timezone, 'YYYY-MM-DD HH24:MI');
|
|
23
|
+
EXCEPTION WHEN invalid_parameter_value THEN
|
|
24
|
+
v_token := to_char(s.next_run_at AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI');
|
|
25
|
+
END;
|
|
26
|
+
ELSE
|
|
27
|
+
v_token := extract(epoch FROM s.next_run_at)::bigint::text;
|
|
28
|
+
END IF;
|
|
29
|
+
|
|
30
|
+
INSERT INTO stampeded_cron_occurrences (cron_key, occurrence_token)
|
|
31
|
+
VALUES (s.key, v_token)
|
|
32
|
+
ON CONFLICT (cron_key, occurrence_token) DO NOTHING
|
|
33
|
+
RETURNING id INTO v_occ;
|
|
34
|
+
|
|
35
|
+
IF v_occ IS NULL THEN
|
|
36
|
+
UPDATE stampeded_schedules
|
|
37
|
+
SET next_run_at = p_next_run_at, updated_at = now()
|
|
38
|
+
WHERE key = s.key;
|
|
39
|
+
RETURN NULL;
|
|
40
|
+
END IF;
|
|
41
|
+
|
|
42
|
+
INSERT INTO stampeded_jobs (queue, task, args, meta, tenant_id, priority)
|
|
43
|
+
VALUES (s.queue, s.task, s.args, s.meta, s.tenant_id, s.priority)
|
|
44
|
+
RETURNING id INTO v_job;
|
|
45
|
+
|
|
46
|
+
UPDATE stampeded_cron_occurrences
|
|
47
|
+
SET job_id = v_job
|
|
48
|
+
WHERE cron_key = s.key AND occurrence_token = v_token;
|
|
49
|
+
|
|
50
|
+
UPDATE stampeded_schedules
|
|
51
|
+
SET next_run_at = p_next_run_at, last_run_at = now(), updated_at = now()
|
|
52
|
+
WHERE key = s.key;
|
|
53
|
+
|
|
54
|
+
RETURN v_job;
|
|
55
|
+
END;
|
|
56
|
+
$$;
|
|
57
|
+
|
|
58
|
+
UPDATE stampeded_schema SET version = 6, changed_at = now();
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
-- Schema 7: a queue can be paused. A paused queue's jobs stay where they are
|
|
2
|
+
-- and enqueueing still works; stampeded_claim hands out nothing from it, so
|
|
3
|
+
-- the pause holds for every runtime and without a daemon. Jobs already
|
|
4
|
+
-- running finish.
|
|
5
|
+
ALTER TABLE stampeded_queue_policies
|
|
6
|
+
ADD COLUMN IF NOT EXISTS paused boolean NOT NULL DEFAULT false;
|
|
7
|
+
|
|
8
|
+
-- Whether p_queue is paused; false, not an error, without the policies table,
|
|
9
|
+
-- which workers already treat as optional. plpgsql so the table is looked up
|
|
10
|
+
-- only once it is known to exist.
|
|
11
|
+
CREATE OR REPLACE FUNCTION stampeded_queue_paused(p_queue text)
|
|
12
|
+
RETURNS boolean
|
|
13
|
+
LANGUAGE plpgsql STABLE AS $$
|
|
14
|
+
BEGIN
|
|
15
|
+
IF to_regclass('stampeded_queue_policies') IS NULL THEN
|
|
16
|
+
RETURN false;
|
|
17
|
+
END IF;
|
|
18
|
+
RETURN EXISTS (SELECT 1 FROM stampeded_queue_policies
|
|
19
|
+
WHERE queue = p_queue AND paused);
|
|
20
|
+
END;
|
|
21
|
+
$$;
|
|
22
|
+
|
|
23
|
+
CREATE OR REPLACE FUNCTION stampeded_claim(
|
|
24
|
+
p_queue text,
|
|
25
|
+
p_worker_id uuid,
|
|
26
|
+
p_limit integer DEFAULT 1,
|
|
27
|
+
|
|
28
|
+
p_now timestamptz DEFAULT NULL
|
|
29
|
+
) RETURNS SETOF stampeded_jobs
|
|
30
|
+
LANGUAGE sql AS $$
|
|
31
|
+
WITH candidate AS MATERIALIZED (
|
|
32
|
+
SELECT id
|
|
33
|
+
FROM stampeded_jobs
|
|
34
|
+
WHERE queue = p_queue
|
|
35
|
+
-- 'retryable' is claimable once its backoff has elapsed. That can be
|
|
36
|
+
-- handled by a claim predicate, a separate promotion pass, or both.
|
|
37
|
+
-- The predicate is chosen here for having no moving parts -- a
|
|
38
|
+
-- promotion pass is another process that can fall behind or die.
|
|
39
|
+
--
|
|
40
|
+
-- Omitting 'retryable' here is not a missing feature, it is a silent
|
|
41
|
+
-- poison-job bug: the reaper parks a crashed job in 'retryable', a
|
|
42
|
+
-- claim that only reads 'available' never picks it up again, the
|
|
43
|
+
-- attempt budget never walks down, and max_attempts is never
|
|
44
|
+
-- reached. Found by test, not by reading.
|
|
45
|
+
AND state IN ('available','retryable')
|
|
46
|
+
AND scheduled_at <= coalesce(p_now, now())
|
|
47
|
+
AND attempt < max_attempts
|
|
48
|
+
-- Reads no job column, so it is a one-time filter: a paused queue
|
|
49
|
+
-- skips the index scan rather than filtering every row it reads.
|
|
50
|
+
AND NOT stampeded_queue_paused(p_queue)
|
|
51
|
+
-- Must match stampeded_jobs_claim_idx EXACTLY. `id` is not a
|
|
52
|
+
-- tiebreaker here: adding one reintroduces a sort unless it is also
|
|
53
|
+
-- added to the index, and in the index it costs 8.5x the size by
|
|
54
|
+
-- defeating btree deduplication. See the claim index note in NOTES.md.
|
|
55
|
+
ORDER BY priority, scheduled_at
|
|
56
|
+
LIMIT p_limit
|
|
57
|
+
FOR UPDATE SKIP LOCKED
|
|
58
|
+
)
|
|
59
|
+
UPDATE stampeded_jobs j
|
|
60
|
+
SET state = 'executing',
|
|
61
|
+
attempt = j.attempt + 1,
|
|
62
|
+
-- Seed the job heartbeat at claim so stampeded_expire has a baseline
|
|
63
|
+
-- for a job that never calls stampeded_touch. Left NULL it would fall
|
|
64
|
+
-- back to claimed_at, which is the same thing -- but only by accident,
|
|
65
|
+
-- and the expiry predicate should not depend on that coincidence.
|
|
66
|
+
heartbeat_at = coalesce(p_now, now()),
|
|
67
|
+
claimed_by = p_worker_id,
|
|
68
|
+
claimed_at = coalesce(p_now, now()),
|
|
69
|
+
-- Bounded ring buffer. Unbounded attempt history is a slow leak on
|
|
70
|
+
-- heavily-retried jobs; keep the last 10.
|
|
71
|
+
attempted_by = array_append(
|
|
72
|
+
CASE WHEN coalesce(array_length(j.attempted_by, 1), 0) >= 10
|
|
73
|
+
THEN j.attempted_by[coalesce(array_length(j.attempted_by,1),0) - 8 :]
|
|
74
|
+
ELSE j.attempted_by
|
|
75
|
+
END,
|
|
76
|
+
p_worker_id::text)
|
|
77
|
+
FROM candidate c
|
|
78
|
+
WHERE j.queue = p_queue
|
|
79
|
+
AND j.id = c.id
|
|
80
|
+
RETURNING j.*;
|
|
81
|
+
$$;
|
|
82
|
+
|
|
83
|
+
UPDATE stampeded_schema SET version = 7, changed_at = now();
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stampeded
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -86,6 +86,8 @@ files:
|
|
|
86
86
|
- schema/0003_dedup_key_separator.sql
|
|
87
87
|
- schema/0004_dedup_key_raw_scope.sql
|
|
88
88
|
- schema/0005_schema_version_guard.sql
|
|
89
|
+
- schema/0006_schedule_unknown_timezone.sql
|
|
90
|
+
- schema/0007_queue_pause.sql
|
|
89
91
|
homepage: https://github.com/seuros/stampeded
|
|
90
92
|
licenses:
|
|
91
93
|
- BSD-3-Clause
|