riverqueue 0.12.0 → 0.14.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/CHANGELOG.md +80 -4
- data/README.md +91 -0
- data/docs/README.md +213 -36
- data/docs/conformance.md +72 -0
- data/docs/migrations.md +20 -23
- data/docs/testing.md +1 -1
- data/lib/client.rb +103 -15
- data/lib/client_runtime.rb +434 -87
- data/lib/config.rb +61 -9
- data/lib/driver/job_row_decoder.rb +2 -2
- data/lib/driver/notification_listener.rb +61 -0
- data/lib/driver/postgres_capabilities.rb +1 -1
- data/lib/driver/runtime.rb +419 -146
- data/lib/driver.rb +1 -0
- data/lib/event.rb +8 -1
- data/lib/insert_opts.rb +8 -3
- data/lib/job.rb +5 -5
- data/lib/job_persisted_logging/plugin.rb +3 -2
- data/lib/migrator.rb +1 -1
- data/lib/periodic_cron.rb +42 -6
- data/lib/periodic_job.rb +23 -4
- data/lib/resumable.rb +24 -1
- data/lib/worker.rb +32 -2
- data/migration/README.md +4 -2
- data/migration/manifest.json +6 -7
- data/migration/postgresql/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.down.sql +1 -1
- data/migration/postgresql/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.up.sql +1 -1
- data/migration/postgresql/main/008_job_id_autoincrement.down.sql +1 -1
- data/migration/postgresql/main/008_job_id_autoincrement.up.sql +1 -1
- data/migration/sqlite/main/008_job_id_autoincrement.up.sql +1 -1
- data/sig/client.rbs +9 -3
- data/sig/driver.rbs +46 -9
- data/sig/insert_opts.rbs +2 -2
- data/sig/job.rbs +2 -0
- data/sig/runtime.rbs +76 -8
- metadata +9 -6
data/lib/driver.rb
CHANGED
data/lib/event.rb
CHANGED
|
@@ -12,7 +12,14 @@ module River
|
|
|
12
12
|
# Event emitted by a Client and delivered through a Subscription.
|
|
13
13
|
Event = Data.define(:kind, :job, :queue, :stats)
|
|
14
14
|
|
|
15
|
-
# Timing statistics attached to job lifecycle events.
|
|
15
|
+
# Timing statistics attached to job lifecycle events, in seconds.
|
|
16
|
+
# run_duration measures worker execution including work hooks;
|
|
17
|
+
# complete_duration measures finalization after execution finishes.
|
|
18
|
+
# queue_wait_duration uses the schedule at the start of execution, before
|
|
19
|
+
# retries or snoozes change it.
|
|
20
|
+
# Execution and completion durations use a monotonic clock. Externally
|
|
21
|
+
# executed jobs have zero run_duration because this runtime does not observe
|
|
22
|
+
# their execution; their queue wait ends at the row's attempted_at timestamp.
|
|
16
23
|
JobStatistics = Data.define(:complete_duration, :queue_wait_duration, :run_duration)
|
|
17
24
|
|
|
18
25
|
# Bounded stream of selected Client lifecycle events.
|
data/lib/insert_opts.rb
CHANGED
|
@@ -7,6 +7,8 @@ module River
|
|
|
7
7
|
class InsertOpts
|
|
8
8
|
# The maximum number of total attempts (including both the original run and
|
|
9
9
|
# all retries) before a job is abandoned and set as discarded.
|
|
10
|
+
# Must be between 1 and MAX_ATTEMPTS_LIMIT (32,767).
|
|
11
|
+
# Defaults to MAX_ATTEMPTS_DEFAULT.
|
|
10
12
|
attr_accessor :max_attempts
|
|
11
13
|
|
|
12
14
|
# Arbitrary metadata to merge into the persisted job. River and River Pro
|
|
@@ -23,6 +25,7 @@ module River
|
|
|
23
25
|
attr_accessor :priority
|
|
24
26
|
|
|
25
27
|
# The name of the job queue in which to insert the job, as a symbol or string.
|
|
28
|
+
# Must contain 1 to 127 ASCII letters, digits, underscores, hyphens, colons, or periods.
|
|
26
29
|
#
|
|
27
30
|
# Defaults to QUEUE_DEFAULT.
|
|
28
31
|
attr_accessor :queue
|
|
@@ -38,7 +41,8 @@ module River
|
|
|
38
41
|
attr_accessor :scheduled_at
|
|
39
42
|
|
|
40
43
|
# Initial state. Primarily intended for extensions such as workflows and
|
|
41
|
-
# sequences, which insert blocked jobs as `:pending`. Accepts
|
|
44
|
+
# sequences, which insert blocked jobs as `:pending`. Accepts only
|
|
45
|
+
# `available`, `pending`, or `scheduled`, as symbols or strings.
|
|
42
46
|
attr_accessor :state
|
|
43
47
|
|
|
44
48
|
# An arbitrary list of keywords to add to the job. They have no functional
|
|
@@ -90,7 +94,7 @@ module River
|
|
|
90
94
|
# be inserted as a new job.
|
|
91
95
|
class UniqueOpts
|
|
92
96
|
# Empty options don't enable uniqueness. Zero periods and empty state lists
|
|
93
|
-
# have the same meaning as their unset equivalents
|
|
97
|
+
# have the same meaning as their unset equivalents.
|
|
94
98
|
def empty?
|
|
95
99
|
!by_args && !by_queue && !exclude_kind && (!by_period || by_period == 0) && (!by_state || by_state.empty?)
|
|
96
100
|
end
|
|
@@ -109,7 +113,8 @@ module River
|
|
|
109
113
|
# in UTC. Equivalent times in different zones produce the same unique key.
|
|
110
114
|
#
|
|
111
115
|
# The period should be specified in seconds. So a job that's unique every 15
|
|
112
|
-
# minute period would have a value of 900.
|
|
116
|
+
# minute period would have a value of 900. Nonzero periods must be at least
|
|
117
|
+
# one second; zero disables period-based uniqueness.
|
|
113
118
|
#
|
|
114
119
|
# Default is no unique period, meaning that as long as any other unique
|
|
115
120
|
# property is enabled, uniqueness will be enforced across all jobs of the
|
data/lib/job.rb
CHANGED
|
@@ -59,7 +59,7 @@ module River
|
|
|
59
59
|
|
|
60
60
|
# The set of worker IDs that have worked this job. A worker ID differs
|
|
61
61
|
# between different programs, but is shared by all executors within any
|
|
62
|
-
# given one. (i.e. Different
|
|
62
|
+
# given one. (i.e. Different processes have different IDs, but IDs are
|
|
63
63
|
# shared within any given process.) A process generates a new ID based on
|
|
64
64
|
# host and current time when it starts up.
|
|
65
65
|
attr_accessor :attempted_by
|
|
@@ -173,7 +173,7 @@ module River
|
|
|
173
173
|
# that occurred.
|
|
174
174
|
class AttemptError
|
|
175
175
|
# The time at which the error occurred. Unreadable historical timestamps
|
|
176
|
-
# become Time.utc(1),
|
|
176
|
+
# become Time.utc(1), the zero time 0001-01-01T00:00:00Z.
|
|
177
177
|
attr_accessor :at
|
|
178
178
|
|
|
179
179
|
# The attempt number on which the error occurred (maps to #attempt on a job
|
|
@@ -184,8 +184,8 @@ module River
|
|
|
184
184
|
# value in case of a panic.
|
|
185
185
|
attr_accessor :error
|
|
186
186
|
|
|
187
|
-
# Contains a stack trace from a job that panicked.
|
|
188
|
-
#
|
|
187
|
+
# Contains a stack trace from a job that raised or panicked. Ruby records
|
|
188
|
+
# the exception backtrace; other River clients record their own format.
|
|
189
189
|
attr_accessor :trace
|
|
190
190
|
|
|
191
191
|
def initialize(
|
|
@@ -202,7 +202,7 @@ module River
|
|
|
202
202
|
|
|
203
203
|
# Returns the database-compatible representation of this attempt error.
|
|
204
204
|
def to_h
|
|
205
|
-
{at: at.
|
|
205
|
+
{at: at.getutc.iso8601(6), attempt: attempt, error: error, trace: trace}
|
|
206
206
|
end
|
|
207
207
|
end
|
|
208
208
|
end
|
|
@@ -5,7 +5,8 @@ require "logger"
|
|
|
5
5
|
module River
|
|
6
6
|
module JobPersistedLogging
|
|
7
7
|
# Captures job.logger output and persists it with the attempt's state change,
|
|
8
|
-
# including failed attempts. Uses
|
|
8
|
+
# including failed attempts. Uses the shared `river:log` metadata format
|
|
9
|
+
# that River UI displays.
|
|
9
10
|
# Register through Config#plugins, preferably before other work middleware.
|
|
10
11
|
class Plugin
|
|
11
12
|
# Limits are positive byte counts. History is capped at 64 MiB, dropping
|
|
@@ -84,7 +85,7 @@ module River
|
|
|
84
85
|
@mutex.synchronize do
|
|
85
86
|
@closed = true
|
|
86
87
|
# Drop invalid/incomplete UTF-8 sequences (including a character cut
|
|
87
|
-
# at the byte limit) and NULs, which
|
|
88
|
+
# at the byte limit) and NULs, which Postgres JSONB cannot represent.
|
|
88
89
|
[@data.dup.force_encoding(Encoding::UTF_8).scrub("").delete("\0"), @truncated]
|
|
89
90
|
end
|
|
90
91
|
end
|
data/lib/migrator.rb
CHANGED
|
@@ -9,7 +9,7 @@ module River
|
|
|
9
9
|
|
|
10
10
|
attr_reader :migrations
|
|
11
11
|
|
|
12
|
-
# Creates a migrator for an installed River driver.
|
|
12
|
+
# Creates a migrator for an installed River driver. Postgres schemas must
|
|
13
13
|
# already exist and use simple SQL identifiers; SQLite uses its main schema.
|
|
14
14
|
# The optional migrations_path is a root containing backend/line/*.sql.
|
|
15
15
|
def initialize(driver, line: "main", migrations_path: File.expand_path("../migration", __dir__.to_s), schema: nil)
|
data/lib/periodic_cron.rb
CHANGED
|
@@ -4,21 +4,57 @@ module River
|
|
|
4
4
|
# A cron schedule for PeriodicJob. Add the optional +fugit+ gem to use it.
|
|
5
5
|
class PeriodicCron
|
|
6
6
|
# Parses a cron expression once. +timezone+ defaults to UTC; use an IANA
|
|
7
|
-
# name
|
|
8
|
-
#
|
|
7
|
+
# name or a fixed offset for local schedules. A CRON_TZ= or TZ= prefix takes
|
|
8
|
+
# precedence. Supports @every durations such as `1h30m` or `500ms`, rounded
|
|
9
|
+
# down to whole seconds with a one-second minimum. Fugit is loaded only on
|
|
10
|
+
# construction.
|
|
9
11
|
def initialize(expression, timezone: "UTC")
|
|
10
12
|
require "fugit"
|
|
11
13
|
|
|
12
14
|
raise ArgumentError, "cron expression must be a String" unless expression.is_a?(String)
|
|
13
15
|
raise ArgumentError, "timezone must be a nonempty name without whitespace" unless timezone.is_a?(String) && /\A\S+\z/.match?(timezone)
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
expression = expression.strip
|
|
18
|
+
if (prefix = /\A(?:CRON_TZ|TZ)=(\S+)\s+(.+)\z/.match(expression))
|
|
19
|
+
timezone = prefix[1].to_s
|
|
20
|
+
expression = prefix[2].to_s
|
|
21
|
+
end
|
|
22
|
+
# Validate the zone even for interval schedules, which don't use it.
|
|
23
|
+
cron_class = Object.const_get(:Fugit).const_get(:Cron)
|
|
24
|
+
cron_class.do_parse("* * * * * #{timezone}")
|
|
25
|
+
@interval = nil
|
|
26
|
+
if expression.start_with?("@every ")
|
|
27
|
+
@interval = parse_interval(expression.delete_prefix("@every "))
|
|
28
|
+
@cron = nil
|
|
29
|
+
else
|
|
30
|
+
@cron = cron_class.do_parse("#{expression.tr("?", "*")} #{timezone}")
|
|
31
|
+
end
|
|
16
32
|
end
|
|
17
33
|
|
|
18
|
-
# Returns a UTC Time strictly after +time+.
|
|
19
|
-
#
|
|
34
|
+
# Returns a UTC Time strictly after +time+. Interval schedules align to
|
|
35
|
+
# whole seconds. Calendar and daylight-saving rules use Fugit.
|
|
20
36
|
def next(time)
|
|
21
|
-
@
|
|
37
|
+
interval = @interval
|
|
38
|
+
return Time.at(time.to_i + interval).utc if interval
|
|
39
|
+
|
|
40
|
+
# Keep the reference in UTC so Fugit does not skip a repeated local wall
|
|
41
|
+
# time when daylight saving ends. The schedule owns the calendar zone.
|
|
42
|
+
@cron.next_time(time.getutc).to_t.getutc
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private def parse_interval(duration)
|
|
46
|
+
# Go's time.ParseDuration syntax, deliberately excluding Fugit's extra
|
|
47
|
+
# units (days/months) and natural-language interval expressions.
|
|
48
|
+
sign = duration.start_with?("-") ? -1 : 1
|
|
49
|
+
duration = duration.sub(/\A[+-]/, "")
|
|
50
|
+
parts = duration.scan(/((?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+))(ns|us|µs|μs|ms|s|m|h)/) #: Array[[String, String]] # rubocop:disable Layout/LeadingCommentSpace
|
|
51
|
+
valid = duration == "0" || (!parts.empty? && parts.map(&:join).join == duration)
|
|
52
|
+
raise ArgumentError, "invalid @every duration" unless valid
|
|
53
|
+
units = {"ns" => 1, "us" => 1_000, "µs" => 1_000, "μs" => 1_000, "ms" => 1_000_000, "s" => 1_000_000_000, "m" => 60_000_000_000, "h" => 3_600_000_000_000}
|
|
54
|
+
nanos = parts.sum { |number, unit| (number.to_r * units.fetch(unit)).to_i } * sign
|
|
55
|
+
raise ArgumentError, "@every duration must fit in signed 64-bit nanoseconds" unless (-(1 << 63)...(1 << 63)).cover?(nanos)
|
|
56
|
+
|
|
57
|
+
[nanos.div(1_000_000_000), 1].max
|
|
22
58
|
end
|
|
23
59
|
end
|
|
24
60
|
end
|
data/lib/periodic_job.rb
CHANGED
|
@@ -56,7 +56,8 @@ module River
|
|
|
56
56
|
|
|
57
57
|
# Thread-safe collection used to change a client's periodic jobs at runtime.
|
|
58
58
|
class PeriodicJobBundle
|
|
59
|
-
def initialize(jobs, wake:)
|
|
59
|
+
def initialize(jobs, wake:, enabled: true)
|
|
60
|
+
@enabled = enabled
|
|
60
61
|
@jobs = {}
|
|
61
62
|
@mutex = Mutex.new
|
|
62
63
|
@next_handle = 0
|
|
@@ -72,8 +73,13 @@ module River
|
|
|
72
73
|
# Adds periodic jobs atomically and returns their handles in input order.
|
|
73
74
|
# Invalid schedules or duplicate IDs leave the registry unchanged.
|
|
74
75
|
def add_many(jobs)
|
|
76
|
+
raise ArgumentError, "periodic jobs require leader election" if !@enabled && !jobs.empty?
|
|
77
|
+
|
|
75
78
|
now = Time.now.utc
|
|
76
|
-
entries = jobs.map
|
|
79
|
+
entries = jobs.map do |job|
|
|
80
|
+
run_on_start = job.run_on_start
|
|
81
|
+
{job: job, next_at: run_on_start ? now : job.next_at(now), run_on_start: run_on_start}
|
|
82
|
+
end
|
|
77
83
|
handles = @mutex.synchronize do
|
|
78
84
|
ids = @jobs.values.to_h { |entry| [entry[:job].id, true] }
|
|
79
85
|
jobs.each do |job|
|
|
@@ -97,7 +103,9 @@ module River
|
|
|
97
103
|
self
|
|
98
104
|
end
|
|
99
105
|
|
|
100
|
-
|
|
106
|
+
# Internal scheduler entry point that retains each claimed occurrence's
|
|
107
|
+
# time even after advancing its registration.
|
|
108
|
+
def __due_with_times(now)
|
|
101
109
|
# Application schedules may reenter the registry, so evaluate outside its
|
|
102
110
|
# lock and only advance registrations that haven't been removed or claimed.
|
|
103
111
|
entries = @mutex.synchronize { @jobs.map { |handle, entry| [handle, entry, entry[:next_at]] } }
|
|
@@ -107,8 +115,10 @@ module River
|
|
|
107
115
|
following = entry[:job].next_at(now)
|
|
108
116
|
@mutex.synchronize do
|
|
109
117
|
next unless @jobs[handle].equal?(entry) && entry[:next_at] == next_at
|
|
118
|
+
scheduled_at = entry[:run_on_start] ? now : next_at
|
|
119
|
+
entry[:run_on_start] = false
|
|
110
120
|
entry[:next_at] = following
|
|
111
|
-
entry[:job]
|
|
121
|
+
[entry[:job], scheduled_at]
|
|
112
122
|
end
|
|
113
123
|
rescue => error
|
|
114
124
|
raise unless block_given?
|
|
@@ -117,6 +127,15 @@ module River
|
|
|
117
127
|
end
|
|
118
128
|
end
|
|
119
129
|
|
|
130
|
+
def due(now, &block)
|
|
131
|
+
__due_with_times(now, &block).map(&:first)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns true when the bundle has no registered periodic jobs.
|
|
135
|
+
def empty?
|
|
136
|
+
@mutex.synchronize { @jobs.empty? }
|
|
137
|
+
end
|
|
138
|
+
|
|
120
139
|
# Removes the job associated with +handle+, returning the PeriodicJob or
|
|
121
140
|
# nil when no such handle exists.
|
|
122
141
|
def remove(handle)
|
data/lib/resumable.rb
CHANGED
|
@@ -11,6 +11,7 @@ module River
|
|
|
11
11
|
attr_accessor :completed_step
|
|
12
12
|
attr_reader :cursors
|
|
13
13
|
attr_accessor :cursors_dirty
|
|
14
|
+
attr_reader :pending_cursor_steps
|
|
14
15
|
attr_accessor :resume_matched
|
|
15
16
|
attr_reader :resume_step
|
|
16
17
|
attr_accessor :step_name
|
|
@@ -24,6 +25,8 @@ module River
|
|
|
24
25
|
@step_name = nil
|
|
25
26
|
|
|
26
27
|
@resume_matched = @resume_step.to_s.empty?
|
|
28
|
+
# Step markers may accompany stale cursors from older clients.
|
|
29
|
+
@pending_cursor_steps = @resume_matched ? @cursors.keys.sort : []
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
def register(name)
|
|
@@ -37,6 +40,9 @@ module River
|
|
|
37
40
|
# Internal runtime boundary: attach progress only to attempts that did not
|
|
38
41
|
# complete, matching River Go's persisted metadata format.
|
|
39
42
|
def __capture_resumable_metadata!
|
|
43
|
+
# Keep missing cursor steps reachable when the worker restores them.
|
|
44
|
+
return unless @resumable_state.pending_cursor_steps.empty?
|
|
45
|
+
|
|
40
46
|
if @resumable_state.cursors_dirty
|
|
41
47
|
@metadata_updates[RESUMABLE_CURSOR_METADATA_KEY] = @resumable_state.cursors.empty? ? nil : @resumable_state.cursors.dup
|
|
42
48
|
end
|
|
@@ -49,6 +55,9 @@ module River
|
|
|
49
55
|
# Internal runtime boundary: validate that the recorded resume point still
|
|
50
56
|
# exists in the worker after it has returned.
|
|
51
57
|
def __finish_resumable_work!
|
|
58
|
+
if (pending = @resumable_state.pending_cursor_steps.first)
|
|
59
|
+
raise Error, "resumable cursor step #{pending.inspect} not found in worker"
|
|
60
|
+
end
|
|
52
61
|
if @resumable_state.resume_step && !@resumable_state.resume_matched
|
|
53
62
|
raise Error, "resumable step #{@resumable_state.resume_step.inspect} not found in worker"
|
|
54
63
|
end
|
|
@@ -56,12 +65,22 @@ module River
|
|
|
56
65
|
|
|
57
66
|
# Immediately checkpoints the current step and cursor progress. Wrap the
|
|
58
67
|
# call in Driver#transaction alongside application writes when an atomic
|
|
59
|
-
# checkpoint is needed.
|
|
68
|
+
# checkpoint is needed. When resuming cursor-only progress, reach its saved
|
|
69
|
+
# cursor step before checkpointing another step.
|
|
70
|
+
#
|
|
71
|
+
# Commit the transaction before the step block returns
|
|
72
|
+
# successfully, and let transaction and commit errors propagate out of it.
|
|
73
|
+
# A successful block marks the step complete even if an enclosing transaction
|
|
74
|
+
# later rolls back.
|
|
60
75
|
def resumable_checkpoint(cursor: RESUMABLE_CURSOR_UNSET)
|
|
61
76
|
step_name = @resumable_state.step_name
|
|
62
77
|
raise Error, "resumable step can only be persisted inside a resumable step" unless step_name
|
|
63
78
|
raise Error, "job must be running" unless row.state == JOB_STATE_RUNNING
|
|
64
79
|
|
|
80
|
+
if (pending = @resumable_state.pending_cursor_steps.first)
|
|
81
|
+
raise Error, "resumable checkpoint cannot precede saved cursor step #{pending.inspect}"
|
|
82
|
+
end
|
|
83
|
+
|
|
65
84
|
cursors = @resumable_state.cursors.dup
|
|
66
85
|
cursors[step_name] = JSON.parse(JSON.generate(cursor)) unless cursor.equal?(RESUMABLE_CURSOR_UNSET)
|
|
67
86
|
updates = {
|
|
@@ -117,6 +136,9 @@ module River
|
|
|
117
136
|
private def run_resumable_step(name, cursor:, default:)
|
|
118
137
|
raise Error, "resumable steps cannot be nested" if @resumable_state.step_name
|
|
119
138
|
raise ArgumentError, "resumable step name must be non-empty" if name.empty?
|
|
139
|
+
if !cursor && @resumable_state.pending_cursor_steps.include?(name)
|
|
140
|
+
raise Error, "resumable cursor step #{name.inspect} requires a cursor step"
|
|
141
|
+
end
|
|
120
142
|
@resumable_state.register(name)
|
|
121
143
|
|
|
122
144
|
unless @resumable_state.resume_matched
|
|
@@ -132,6 +154,7 @@ module River
|
|
|
132
154
|
@resumable_state.step_name = name
|
|
133
155
|
begin
|
|
134
156
|
value = (cursor && @resumable_state.cursors.key?(name)) ? @resumable_state.cursors[name] : default
|
|
157
|
+
@resumable_state.pending_cursor_steps.delete(name) if cursor
|
|
135
158
|
result = cursor ? yield(value) : yield
|
|
136
159
|
@resumable_state.completed_step = name
|
|
137
160
|
if cursor && @resumable_state.cursors.key?(name)
|
data/lib/worker.rb
CHANGED
|
@@ -12,8 +12,18 @@ module River
|
|
|
12
12
|
#
|
|
13
13
|
# With one argument, the kind is read from +worker.kind+ or
|
|
14
14
|
# +worker.class.kind+. Pass a kind and worker separately to override it.
|
|
15
|
-
#
|
|
16
|
-
|
|
15
|
+
# Alternatively, pass a kind and a block that receives a River::Job on each
|
|
16
|
+
# attempt. Block workers use the client's retry and timeout policies.
|
|
17
|
+
# Supply a worker or a block, not both. Kinds and aliases accept symbols or
|
|
18
|
+
# strings.
|
|
19
|
+
def add(kind_or_worker, worker = nil, aliases: [], &block)
|
|
20
|
+
if block
|
|
21
|
+
raise ArgumentError, "use a worker or a block, not both" unless worker.nil?
|
|
22
|
+
raise ArgumentError, "block workers require a string or symbol kind" unless kind_or_worker.is_a?(String) || kind_or_worker.is_a?(Symbol)
|
|
23
|
+
|
|
24
|
+
worker = BlockWorker.new(block)
|
|
25
|
+
end
|
|
26
|
+
|
|
17
27
|
if worker
|
|
18
28
|
kind = kind_or_worker.to_s
|
|
19
29
|
else
|
|
@@ -54,11 +64,29 @@ module River
|
|
|
54
64
|
def kinds
|
|
55
65
|
@workers.keys.freeze
|
|
56
66
|
end
|
|
67
|
+
|
|
68
|
+
# Adapts a registered block to the runtime's worker interface.
|
|
69
|
+
class BlockWorker
|
|
70
|
+
def initialize(work_func)
|
|
71
|
+
@work_func = work_func
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def work(job)
|
|
75
|
+
@work_func.call(job)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
private_constant :BlockWorker
|
|
57
79
|
end
|
|
58
80
|
|
|
59
81
|
# A job being worked, with access to its persisted row and attempt-local
|
|
60
82
|
# metadata changes.
|
|
61
83
|
class Job
|
|
84
|
+
# Internal runtime hint; persisted state determines whether completion committed.
|
|
85
|
+
attr_accessor :__completion_attempted
|
|
86
|
+
|
|
87
|
+
# Internal snapshot retained only after transactional completion commits.
|
|
88
|
+
attr_accessor :__completion_snapshot
|
|
89
|
+
|
|
62
90
|
# Client working this job.
|
|
63
91
|
attr_reader :client
|
|
64
92
|
|
|
@@ -67,6 +95,8 @@ module River
|
|
|
67
95
|
|
|
68
96
|
def initialize(client, row)
|
|
69
97
|
@client = client
|
|
98
|
+
@__completion_attempted = false
|
|
99
|
+
@__completion_snapshot = nil
|
|
70
100
|
@logger = nil
|
|
71
101
|
@metadata_updates = {}
|
|
72
102
|
@row = row
|
data/migration/README.md
CHANGED
|
@@ -5,7 +5,9 @@ https://github.com/riverqueue/river, distributed under the upstream Mozilla
|
|
|
5
5
|
Public License 2.0 (see LICENSE). The Ruby implementation remains licensed
|
|
6
6
|
under the repository's MPL-2.0 license.
|
|
7
7
|
|
|
8
|
-
The bundle
|
|
9
|
-
manifest.json records the
|
|
8
|
+
The bundle includes main migrations through version 008.
|
|
9
|
+
manifest.json records the SHA-256 of every SQL file in the repository's
|
|
10
|
+
riverdriver/ directory.
|
|
11
|
+
Verification compares against this checkout, not a pinned upstream revision.
|
|
10
12
|
Update or verify these files with scripts/sync_migrations.rb; do not edit SQL
|
|
11
13
|
here independently of upstream. Private Pro migrations are not included here.
|
data/migration/manifest.json
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
"postgresql/main/005_migration_unique_client.up.sql": "b760f487152c7d92102869d46b8a64dc1e2094d5675e690ffbe52a747eee8431",
|
|
13
13
|
"postgresql/main/006_bulk_unique.down.sql": "726483f6e5aa7dd02cdd974cd7bf716973a8d0a97ba6dbc5dc5304aaf54c7ad7",
|
|
14
14
|
"postgresql/main/006_bulk_unique.up.sql": "3b133f7ce4662d3dc8bd4a57628e0e116a300b2635a79315557aa1369849f0fb",
|
|
15
|
-
"postgresql/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.down.sql": "
|
|
16
|
-
"postgresql/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.up.sql": "
|
|
17
|
-
"postgresql/main/008_job_id_autoincrement.down.sql": "
|
|
18
|
-
"postgresql/main/008_job_id_autoincrement.up.sql": "
|
|
15
|
+
"postgresql/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.down.sql": "2771765becc5baa0a8af4406cb4d58ea6d3fb0ebdbf72a0c23ccc72da623caeb",
|
|
16
|
+
"postgresql/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.up.sql": "af01339fcb1687de1f768a570b6ad1e51d23300082aed60440f6a662a0e39d85",
|
|
17
|
+
"postgresql/main/008_job_id_autoincrement.down.sql": "678b3b02deb2f798c3fda3a73c974fc1fe48c6b88688b94f0f7700132b30626f",
|
|
18
|
+
"postgresql/main/008_job_id_autoincrement.up.sql": "678b3b02deb2f798c3fda3a73c974fc1fe48c6b88688b94f0f7700132b30626f",
|
|
19
19
|
"sqlite/main/001_create_river_migration.down.sql": "34c87dc594bf7520bc3ae69f6f0da8d2d9a472616ab38b37e63d4e3838da06d2",
|
|
20
20
|
"sqlite/main/001_create_river_migration.up.sql": "d15597cb0bb884fb0727d2a29ad8313842708b55fd561a5fe62e37aad5f34298",
|
|
21
21
|
"sqlite/main/002_initial_schema.down.sql": "900508ba08d0ca3c8451eb2854cd9ab837166ef55736393524253b6228438470",
|
|
@@ -31,8 +31,7 @@
|
|
|
31
31
|
"sqlite/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.down.sql": "55bffeb528b40dffc0cbec2f22d1463aef37a8b3d7977f83729cc505bb77c745",
|
|
32
32
|
"sqlite/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.up.sql": "441a05e1d9aa4f151877ccf725b0b0a86f27013442297d4b621c05676270d0b0",
|
|
33
33
|
"sqlite/main/008_job_id_autoincrement.down.sql": "04871283fe5d4cab4ac70da28d8509aa7ba765994d528c2dcc7ebe0ee596710c",
|
|
34
|
-
"sqlite/main/008_job_id_autoincrement.up.sql": "
|
|
34
|
+
"sqlite/main/008_job_id_autoincrement.up.sql": "3ce6d941f4c434adbd87e69089fa5e99b6047e5b51e611c9845e1d0d3c080e0f"
|
|
35
35
|
},
|
|
36
|
-
"
|
|
37
|
-
"revision": "c2c380490d14df8efc28b86fbe7be1cf93930405"
|
|
36
|
+
"source": "riverdriver/"
|
|
38
37
|
}
|
data/migration/postgresql/main/007_notification_outbox_sqlite_jsonb_and_sql_cleanup.down.sql
CHANGED
|
@@ -47,7 +47,7 @@ ALTER TABLE /* TEMPLATE: schema */river_queue
|
|
|
47
47
|
--
|
|
48
48
|
-- SQLite JSONB conversion rollback.
|
|
49
49
|
--
|
|
50
|
-
-- No-op.
|
|
50
|
+
-- No-op. Postgres already stores River JSON columns as jsonb.
|
|
51
51
|
|
|
52
52
|
--
|
|
53
53
|
-- Notification outbox rollback.
|
|
@@ -16,7 +16,7 @@ CREATE INDEX river_notification_topic_id_idx ON /* TEMPLATE: schema */river_noti
|
|
|
16
16
|
--
|
|
17
17
|
-- SQLite JSONB conversion.
|
|
18
18
|
--
|
|
19
|
-
-- No-op.
|
|
19
|
+
-- No-op. Postgres already stores River JSON columns as jsonb.
|
|
20
20
|
|
|
21
21
|
--
|
|
22
22
|
-- SQL cleanup.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- Rebuild river_job so automatically generated IDs are never reused after the
|
|
2
|
-
-- job holding the largest ID is deleted. Unlike
|
|
2
|
+
-- job holding the largest ID is deleted. Unlike Postgres sequences, SQLite's
|
|
3
3
|
-- default ROWID allocator may otherwise reuse that deleted ID.
|
|
4
4
|
|
|
5
5
|
-- Rebuilding river_job would discard schema installed by River Pro. Check
|
data/sig/client.rbs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module River
|
|
2
2
|
MAX_ATTEMPTS_DEFAULT: Integer
|
|
3
|
+
MAX_ATTEMPTS_LIMIT: Integer
|
|
3
4
|
PRIORITY_DEFAULT: Integer
|
|
4
5
|
QUEUE_DEFAULT: String
|
|
5
6
|
|
|
@@ -14,15 +15,17 @@ module River
|
|
|
14
15
|
|
|
15
16
|
def initialize: (_Driver driver, ?config: Config?) -> void
|
|
16
17
|
def __finish_claimed_job: (JobRow, ?Exception?) -> untyped
|
|
18
|
+
def __insert_with_default_scheduled_at: (jobArgs, InsertOpts, Time?) -> JobInsertResult
|
|
17
19
|
def __perform_job: (Integer, ?allow_scheduled: bool) -> untyped
|
|
18
20
|
def __interrupt_workers: () -> untyped
|
|
19
21
|
def __runtime_healthy?: () -> bool
|
|
20
22
|
def id: () -> String
|
|
21
23
|
def insert: (jobArgs, ?insert_opts: InsertOpts?, ?max_attempts: Integer?,
|
|
22
24
|
?metadata: Hash[String | Symbol, untyped]?, ?priority: Integer?, ?queue: (String | Symbol)?,
|
|
23
|
-
?scheduled_at: Time?, ?state:
|
|
25
|
+
?scheduled_at: Time?, ?state: jobStateInitialInput?, ?tags: Array[String]?, ?unique_opts: UniqueOpts?) -> JobInsertResult
|
|
24
26
|
def insert_many: (Array[jobArgs | InsertManyParams]) -> Array[JobInsertResult]
|
|
25
27
|
def job_cancel: (Integer) -> JobRow
|
|
28
|
+
def job_complete_tx: (Job) -> JobRow
|
|
26
29
|
def job_delete: (Integer) -> JobRow
|
|
27
30
|
def job_delete_many: (?JobListParams?, ?after: untyped, ?after_id: untyped,
|
|
28
31
|
?ids: untyped, ?kinds: untyped, ?limit: untyped, ?metadata: untyped, ?priorities: untyped,
|
|
@@ -45,6 +48,7 @@ module River
|
|
|
45
48
|
def queue_remove: (String | Symbol) -> self
|
|
46
49
|
def queue_resume: (String | Symbol) -> bool
|
|
47
50
|
def queue_update: (String | Symbol, metadata: Hash[untyped, untyped]) -> Queue
|
|
51
|
+
def request_resign: () -> nil
|
|
48
52
|
def start: () -> self
|
|
49
53
|
def started?: () -> bool
|
|
50
54
|
def stop: (?wait: bool) -> self
|
|
@@ -54,15 +58,17 @@ module River
|
|
|
54
58
|
|
|
55
59
|
DEFAULT_UNIQUE_STATES: Array[jobStateAll]
|
|
56
60
|
EMPTY_INSERT_OPTS: InsertOpts
|
|
61
|
+
INITIAL_STATES: Array[jobStateInitial]
|
|
57
62
|
REQUIRED_UNIQUE_STATES: Array[jobStateAll]
|
|
58
63
|
TAG_RE: Regexp
|
|
59
64
|
|
|
60
65
|
private def insert_and_check_unique_job: (Driver::JobInsertParams) -> JobInsertResult
|
|
61
|
-
private def make_insert_params: (jobArgs, InsertOpts) -> Driver::JobInsertParams
|
|
66
|
+
private def make_insert_params: (jobArgs, InsertOpts, ?Time?) -> Driver::JobInsertParams
|
|
62
67
|
private def make_unique_key_and_bitmask: (Driver::JobInsertParams, UniqueOpts) -> [String, String]
|
|
63
68
|
private def run_insert_plugins: (Array[Driver::JobInsertParams]) { () -> Array[JobInsertResult] } -> Array[JobInsertResult]
|
|
64
69
|
private def truncate_time: (Time, Integer | Rational) -> Time
|
|
65
70
|
private def uint64_to_int64: (Integer) -> Integer
|
|
71
|
+
private def validate_insert_state: (String | Symbol) -> jobStateInitial
|
|
66
72
|
private def validate_tags: (Array[String]) -> Array[String]
|
|
67
73
|
private def validate_unique_states: (Array[jobStateInput]) -> Array[jobStateAll]
|
|
68
74
|
end
|
|
@@ -76,7 +82,7 @@ module River
|
|
|
76
82
|
|
|
77
83
|
def initialize: (jobArgs job, ?insert_opts: InsertOpts?, ?max_attempts: Integer?,
|
|
78
84
|
?metadata: Hash[String | Symbol, untyped]?, ?priority: Integer?, ?queue: (String | Symbol)?,
|
|
79
|
-
?scheduled_at: Time?, ?state:
|
|
85
|
+
?scheduled_at: Time?, ?state: jobStateInitialInput?, ?tags: Array[String]?, ?unique_opts: UniqueOpts?) -> void
|
|
80
86
|
def is_a?: (Class) -> bool
|
|
81
87
|
end
|
|
82
88
|
|