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/config.rb
CHANGED
|
@@ -40,22 +40,52 @@ module River
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Lazily generates a client ID once per process. The process ID check
|
|
44
|
+
# regenerates it in forked children without needing a fork hook.
|
|
45
|
+
class DefaultClientID # :nodoc:
|
|
46
|
+
HOSTNAME_MAX_LENGTH = 80
|
|
47
|
+
|
|
48
|
+
def initialize
|
|
49
|
+
@mutex = Mutex.new
|
|
50
|
+
@state = nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def value
|
|
54
|
+
pid = Process.pid
|
|
55
|
+
@state&.[](pid) || @mutex.synchronize { @state&.[](pid) || generate(pid) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The PID and ID are swapped in as one frozen hash so a lock-free reader
|
|
59
|
+
# never pairs one process's PID with another's ID.
|
|
60
|
+
private def generate(pid)
|
|
61
|
+
id = "#{Socket.gethostname[0, HOSTNAME_MAX_LENGTH]}-#{pid}-#{SecureRandom.hex(6)}".freeze
|
|
62
|
+
@state = {pid => id}.freeze
|
|
63
|
+
id
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
private_constant :DefaultClientID
|
|
67
|
+
|
|
43
68
|
# Complete configuration for a River client and its worker runtime.
|
|
44
69
|
class Config
|
|
45
70
|
# Configured values used by Client and its runtime.
|
|
46
71
|
attr_reader :cancelled_job_retention_period, :completed_job_retention_period,
|
|
47
72
|
:discarded_job_retention_period, :error_handler, :fetch_cooldown,
|
|
48
|
-
:fetch_only_known_kinds, :fetch_poll_interval, :
|
|
73
|
+
:fetch_only_known_kinds, :fetch_poll_interval, :job_timeout,
|
|
49
74
|
:leader_election_disabled, :logger, :maintenance_services,
|
|
50
|
-
:periodic_jobs, :plugins, :queues, :retry_policy, :workers
|
|
75
|
+
:periodic_jobs, :plugins, :poll_only, :queues, :retry_policy, :workers
|
|
51
76
|
|
|
52
77
|
# Creates a client configuration.
|
|
53
78
|
#
|
|
54
79
|
# Configure queues and workers here to enable job processing; a client with
|
|
55
80
|
# neither may still be used for insertion and administrative operations.
|
|
56
|
-
# +
|
|
57
|
-
#
|
|
58
|
-
#
|
|
81
|
+
# An explicit +id+ is used exactly as given and must be unique among all
|
|
82
|
+
# running clients. Without one, each process using this config generates
|
|
83
|
+
# its own ID; see #id. +fetch_only_known_kinds+ leaves unregistered kinds
|
|
84
|
+
# untouched in shared queues. +leader_election_disabled+ prevents this
|
|
85
|
+
# client's maintenance; another eligible client must run scheduling,
|
|
86
|
+
# rescue, and cleanup.
|
|
87
|
+
# +poll_only+ disables notification receiving, including leadership
|
|
88
|
+
# resignation requests. Job and queue state are still polled.
|
|
59
89
|
def initialize(
|
|
60
90
|
queues: {},
|
|
61
91
|
workers: Workers.new,
|
|
@@ -70,6 +100,7 @@ module River
|
|
|
70
100
|
plugins: [],
|
|
71
101
|
maintenance_services: [],
|
|
72
102
|
periodic_jobs: [],
|
|
103
|
+
poll_only: false,
|
|
73
104
|
cancelled_job_retention_period: 86_400,
|
|
74
105
|
completed_job_retention_period: 86_400,
|
|
75
106
|
discarded_job_retention_period: 604_800,
|
|
@@ -82,13 +113,15 @@ module River
|
|
|
82
113
|
@fetch_cooldown = Float(fetch_cooldown)
|
|
83
114
|
@fetch_only_known_kinds = fetch_only_known_kinds
|
|
84
115
|
@fetch_poll_interval = Float(fetch_poll_interval)
|
|
85
|
-
@
|
|
116
|
+
@default_id = DefaultClientID.new
|
|
117
|
+
@id = id
|
|
86
118
|
@job_timeout = job_timeout.nil? ? nil : Float(job_timeout)
|
|
87
119
|
@leader_election_disabled = leader_election_disabled
|
|
88
120
|
@logger = logger || Logger.new($stdout, level: Logger::WARN)
|
|
89
121
|
@maintenance_services = maintenance_services.dup.freeze
|
|
90
122
|
@periodic_jobs = periodic_jobs.dup.freeze
|
|
91
123
|
@plugins = plugins.dup.freeze
|
|
124
|
+
@poll_only = poll_only
|
|
92
125
|
@queues = normalize_queues(queues).freeze
|
|
93
126
|
@retry_policy = retry_policy
|
|
94
127
|
@workers = workers
|
|
@@ -96,10 +129,22 @@ module River
|
|
|
96
129
|
validate
|
|
97
130
|
end
|
|
98
131
|
|
|
132
|
+
# Returns the client ID recorded in +attempted_by+ and used for leadership.
|
|
133
|
+
#
|
|
134
|
+
# An explicitly configured ID is returned unchanged. Otherwise, the ID is
|
|
135
|
+
# generated on first use in each process and stays stable for the life of
|
|
136
|
+
# that process, so clients built from a config before a fork (for example,
|
|
137
|
+
# by a preloading Puma, Unicorn, or Pitchfork server) get distinct IDs in
|
|
138
|
+
# each forked child instead of sharing the parent's.
|
|
139
|
+
def id
|
|
140
|
+
@id || @default_id.value
|
|
141
|
+
end
|
|
142
|
+
|
|
99
143
|
# Returns a new Config with the supplied values replacing this config's
|
|
100
|
-
# corresponding settings.
|
|
144
|
+
# corresponding settings. Without an +id+ override, the copy shares this
|
|
145
|
+
# config's ID, including its per-process generated default.
|
|
101
146
|
def with(**overrides)
|
|
102
|
-
Config.new(id: id,
|
|
147
|
+
copy = Config.new(id: @id,
|
|
103
148
|
cancelled_job_retention_period: cancelled_job_retention_period,
|
|
104
149
|
completed_job_retention_period: completed_job_retention_period,
|
|
105
150
|
discarded_job_retention_period: discarded_job_retention_period,
|
|
@@ -113,9 +158,16 @@ module River
|
|
|
113
158
|
maintenance_services: maintenance_services,
|
|
114
159
|
periodic_jobs: periodic_jobs,
|
|
115
160
|
plugins: plugins,
|
|
161
|
+
poll_only: poll_only,
|
|
116
162
|
queues: queues,
|
|
117
163
|
retry_policy: retry_policy,
|
|
118
164
|
workers: workers, **overrides)
|
|
165
|
+
copy.adopt_default_id(@default_id) unless overrides.key?(:id)
|
|
166
|
+
copy
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
protected def adopt_default_id(default_id)
|
|
170
|
+
@default_id = default_id
|
|
119
171
|
end
|
|
120
172
|
|
|
121
173
|
private def normalize_queues(queues)
|
|
@@ -136,7 +188,7 @@ module River
|
|
|
136
188
|
|
|
137
189
|
private def validate
|
|
138
190
|
raise ArgumentError, "fetch intervals and job_timeout must be finite" unless [fetch_cooldown, fetch_poll_interval, job_timeout].compact.all?(&:finite?)
|
|
139
|
-
raise ArgumentError, "id must be between 1 and 127 characters"
|
|
191
|
+
raise ArgumentError, "id must be between 1 and 127 characters" if (explicit_id = @id) && !(1...128).cover?(explicit_id.length)
|
|
140
192
|
raise ArgumentError, "fetch_cooldown must be at least 0.001 seconds" if fetch_cooldown < 0.001
|
|
141
193
|
raise ArgumentError, "fetch_poll_interval cannot be less than fetch_cooldown" if fetch_poll_interval < fetch_cooldown
|
|
142
194
|
raise ArgumentError, "job_timeout must be greater than zero or nil" if job_timeout && job_timeout <= 0
|
|
@@ -23,7 +23,7 @@ module River::Driver
|
|
|
23
23
|
def self.error_time(value)
|
|
24
24
|
return ZERO_TIME unless value.is_a?(String)
|
|
25
25
|
|
|
26
|
-
# Keep Ruby's existing acceptance of
|
|
26
|
+
# Keep Ruby's existing acceptance of Postgres-style timestamps too.
|
|
27
27
|
Time.parse(value).utc
|
|
28
28
|
rescue ArgumentError
|
|
29
29
|
ZERO_TIME
|
|
@@ -63,7 +63,7 @@ module River::Driver
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def decoded(field, value, type: nil, strings: false, default: nil)
|
|
66
|
-
# Sequel's
|
|
66
|
+
# Sequel's Postgres JSON and array wrappers delegate to Ruby values.
|
|
67
67
|
value = value.__getobj__ if value.respond_to?(:__getobj__)
|
|
68
68
|
return default if value.nil?
|
|
69
69
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module River::Driver
|
|
4
|
+
# Internal notification transports. Each client owns its listener; SQLite
|
|
5
|
+
# cursors never consume or delete another client's notifications.
|
|
6
|
+
module NotificationListener
|
|
7
|
+
TOPICS = %w[river_control river_insert river_leadership].freeze
|
|
8
|
+
|
|
9
|
+
class Postgres
|
|
10
|
+
def initialize(connection, schema)
|
|
11
|
+
@connection = connection
|
|
12
|
+
@channels = TOPICS.to_h { |topic| ["#{schema}.#{topic}", topic] }
|
|
13
|
+
@channels.each_key { |channel| @connection.exec("LISTEN #{@connection.escape_identifier(channel)}") }
|
|
14
|
+
rescue
|
|
15
|
+
close
|
|
16
|
+
raise
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def close
|
|
20
|
+
@connection.close unless @connection.finished?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def poll(timeout)
|
|
24
|
+
notifications = [] #: Array[untyped]
|
|
25
|
+
@connection.wait_for_notify(timeout) do |channel, _pid, payload|
|
|
26
|
+
notifications << [@channels[channel], payload]
|
|
27
|
+
end
|
|
28
|
+
# Bound each batch so a busy sender cannot prevent shutdown.
|
|
29
|
+
while notifications.length < 1_000 && (notification = @connection.notifies)
|
|
30
|
+
notifications << [@channels[notification[:relname]], notification[:extra]]
|
|
31
|
+
end
|
|
32
|
+
notifications
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class SQLite
|
|
37
|
+
def initialize(query)
|
|
38
|
+
@query = query
|
|
39
|
+
# Historical requests must not resign a newly started client.
|
|
40
|
+
@last_id = @query.call("SELECT coalesce(max(id), 0) AS id FROM river_notification").first.transform_keys(&:to_sym).fetch(:id).to_i
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def close
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def poll(timeout)
|
|
47
|
+
rows = @query.call("SELECT id, topic, payload FROM river_notification WHERE id > #{@last_id} ORDER BY id LIMIT 1000")
|
|
48
|
+
if rows.empty?
|
|
49
|
+
sleep(timeout)
|
|
50
|
+
return []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
rows.map do |row|
|
|
54
|
+
row = row.transform_keys(&:to_sym)
|
|
55
|
+
@last_id = row.fetch(:id).to_i
|
|
56
|
+
[row.fetch(:topic), row.fetch(:payload)]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module River::Driver
|
|
4
|
-
#
|
|
4
|
+
# Postgres-compatible servers differ in conflict detection and notification
|
|
5
5
|
# support. Keep these rules aligned with Go's PostgresCapabilities.
|
|
6
6
|
class PostgresCapabilities
|
|
7
7
|
attr_reader :supports_listen_notify, :unique_insert_mode, :unique_insert_sql
|