pgbus 0.21.0 → 0.21.2
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 +23 -0
- data/lib/generators/pgbus/add_failed_events_source_queue_generator.rb +44 -0
- data/lib/generators/pgbus/templates/add_failed_events_source_queue.rb.erb +27 -0
- data/lib/generators/pgbus/templates/add_failed_events_unique_index.rb.erb +5 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +6 -3
- data/lib/pgbus/active_job/executor.rb +29 -9
- data/lib/pgbus/batch/sweep.rb +10 -5
- data/lib/pgbus/client.rb +109 -42
- data/lib/pgbus/configuration.rb +11 -0
- data/lib/pgbus/failed_event_recorder.rb +135 -42
- data/lib/pgbus/generators/migration_detector.rb +16 -9
- data/lib/pgbus/process/consumer.rb +12 -5
- data/lib/pgbus/process/heartbeat.rb +12 -1
- data/lib/pgbus/process/readiness_snapshot.rb +14 -6
- data/lib/pgbus/process/supervisor.rb +178 -48
- data/lib/pgbus/process/worker.rb +4 -3
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source/job_list.rb +16 -3
- data/lib/pgbus/web/data_source.rb +67 -36
- data/lib/pgbus/web/health_app.rb +1 -1
- data/sig/pgbus/client.rbs +7 -5
- data/sig/pgbus/configuration.rbs +2 -0
- metadata +3 -1
|
@@ -4,45 +4,49 @@ require "time"
|
|
|
4
4
|
|
|
5
5
|
module Pgbus
|
|
6
6
|
# Records job failures to pgbus_failed_events for dashboard visibility.
|
|
7
|
-
# Uses upsert (INSERT ON CONFLICT UPDATE) keyed on
|
|
8
|
-
#
|
|
7
|
+
# Uses upsert (INSERT ON CONFLICT UPDATE) keyed on the message, so each
|
|
8
|
+
# message has at most one failed_event row tracking its latest error.
|
|
9
|
+
#
|
|
10
|
+
# A message is (queue_name, msg_id, source_queue): queue_name is the logical
|
|
11
|
+
# queue, source_queue the physical table the message was read from. Under
|
|
12
|
+
# priority routing each `_pN` sub-queue numbers its messages from its own
|
|
13
|
+
# sequence, so the logical name and msg_id alone can name two jobs (issue
|
|
14
|
+
# #583). A row written before the source_queue column existed has NULL there
|
|
15
|
+
# and is read as belonging to any table of its queue. Until the column is
|
|
16
|
+
# migrated, every query keeps its (queue_name, msg_id) shape.
|
|
9
17
|
class FailedEventRecorder
|
|
18
|
+
# How long a "column missing" answer stands before the schema is probed
|
|
19
|
+
# again, so a process started before the migration picks the column up.
|
|
20
|
+
SOURCE_QUEUE_RECHECK_SECONDS = 60
|
|
21
|
+
|
|
22
|
+
@source_queue_column_mutex = Mutex.new
|
|
23
|
+
|
|
10
24
|
class << self
|
|
11
|
-
def record!(queue_name:, msg_id:, payload:, headers:, error:, retry_count:)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"FailedEvent Record",
|
|
25
|
-
|
|
26
|
-
queue_name,
|
|
27
|
-
msg_id.to_i,
|
|
28
|
-
payload.is_a?(String) ? payload : JSON.generate(payload),
|
|
29
|
-
headers.is_a?(String) ? headers : headers&.then { |h| JSON.generate(h) },
|
|
30
|
-
error.class.name,
|
|
31
|
-
error.message.to_s.truncate(10_000),
|
|
32
|
-
error.backtrace&.first(30)&.join("\n"),
|
|
33
|
-
retry_count
|
|
34
|
-
]
|
|
35
|
-
)
|
|
25
|
+
def record!(queue_name:, msg_id:, source_queue:, payload:, headers:, error:, retry_count:)
|
|
26
|
+
details = [
|
|
27
|
+
payload.is_a?(String) ? payload : JSON.generate(payload),
|
|
28
|
+
headers.is_a?(String) ? headers : headers&.then { |h| JSON.generate(h) },
|
|
29
|
+
error.class.name,
|
|
30
|
+
error.message.to_s.truncate(10_000),
|
|
31
|
+
error.backtrace&.first(30)&.join("\n"),
|
|
32
|
+
retry_count
|
|
33
|
+
]
|
|
34
|
+
legacy = !source_queue_column?
|
|
35
|
+
if legacy
|
|
36
|
+
connection.exec_query(LEGACY_RECORD_SQL, "FailedEvent Record", [queue_name, msg_id.to_i, *details])
|
|
37
|
+
else
|
|
38
|
+
connection.exec_query(RECORD_SQL, "FailedEvent Record", [queue_name, source_queue, msg_id.to_i, *details])
|
|
39
|
+
end
|
|
36
40
|
rescue StandardError => e
|
|
41
|
+
# A process started before the migration would keep sending the old
|
|
42
|
+
# ON CONFLICT target, whose index the migration drops: probe again.
|
|
43
|
+
reset_source_queue_column_check! if legacy
|
|
37
44
|
ErrorReporter.report(e, { action: "record_failed_event", queue: queue_name, msg_id: msg_id })
|
|
38
45
|
end
|
|
39
46
|
|
|
40
|
-
def exists?(queue_name:, msg_id:)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"FailedEvent Exists",
|
|
44
|
-
[queue_name, msg_id.to_i]
|
|
45
|
-
)
|
|
47
|
+
def exists?(queue_name:, msg_id:, source_queue:)
|
|
48
|
+
sql, binds = message_scope(queue_name, msg_id, source_queue)
|
|
49
|
+
result = connection.select_value("SELECT 1 FROM pgbus_failed_events WHERE #{sql} LIMIT 1", "FailedEvent Exists", binds)
|
|
46
50
|
!result.nil?
|
|
47
51
|
rescue StandardError => e
|
|
48
52
|
Pgbus.logger.debug { "[Pgbus] FailedEvent exists? check failed: #{e.class}: #{e.message}" }
|
|
@@ -53,12 +57,15 @@ module Pgbus
|
|
|
53
57
|
# dead-letters the message and clears the row (issue #495). failed_at
|
|
54
58
|
# comes back as an ISO 8601 UTC string whatever the adapter decodes it
|
|
55
59
|
# to. A failed read returns nil: it must never stop a dead-letter.
|
|
56
|
-
|
|
60
|
+
# The message's own row wins over one written before source_queue.
|
|
61
|
+
def last_error(queue_name:, msg_id:, source_queue:)
|
|
62
|
+
sql, binds = message_scope(queue_name, msg_id, source_queue)
|
|
63
|
+
order = binds.size > 2 ? " ORDER BY source_queue IS NULL, failed_at DESC" : ""
|
|
57
64
|
row = connection.select_one(
|
|
58
65
|
"SELECT error_class, error_message, backtrace, retry_count, failed_at " \
|
|
59
|
-
"FROM pgbus_failed_events WHERE
|
|
66
|
+
"FROM pgbus_failed_events WHERE #{sql}#{order} LIMIT 1",
|
|
60
67
|
"FailedEvent Last Error",
|
|
61
|
-
|
|
68
|
+
binds
|
|
62
69
|
)
|
|
63
70
|
return unless row
|
|
64
71
|
|
|
@@ -91,12 +98,11 @@ module Pgbus
|
|
|
91
98
|
end
|
|
92
99
|
end
|
|
93
100
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
101
|
+
# Deletes the message's own row, and a row written before source_queue
|
|
102
|
+
# existed: a sibling sub-queue's row with the same msg_id stays.
|
|
103
|
+
def clear!(queue_name:, msg_id:, source_queue:)
|
|
104
|
+
sql, binds = message_scope(queue_name, msg_id, source_queue)
|
|
105
|
+
connection.exec_delete("DELETE FROM pgbus_failed_events WHERE #{sql}", "FailedEvent Clear", binds)
|
|
100
106
|
rescue StandardError => e
|
|
101
107
|
# ERROR-level: a failed clear leaves a stale row in the dashboard
|
|
102
108
|
# AFTER the job actually succeeded — confusing and load-bearing
|
|
@@ -107,8 +113,69 @@ module Pgbus
|
|
|
107
113
|
end
|
|
108
114
|
end
|
|
109
115
|
|
|
116
|
+
# Whether pgbus_failed_events has the source_queue column (issue #583).
|
|
117
|
+
# Probed like ProcessedEvent.completion_column?: a gem upgraded before
|
|
118
|
+
# its migration gets false, a one-time warning, and the (queue_name,
|
|
119
|
+
# msg_id) queries. A present column is remembered for good; a missing
|
|
120
|
+
# one for SOURCE_QUEUE_RECHECK_SECONDS. A probe that raises is not
|
|
121
|
+
# memoized.
|
|
122
|
+
def source_queue_column?
|
|
123
|
+
# Double-checked: the unlocked read only skips the lock while the
|
|
124
|
+
# answer stands; it is written under @source_queue_column_mutex. The
|
|
125
|
+
# state is true, nil (never probed) or the monotonic time a missing
|
|
126
|
+
# column's answer runs out.
|
|
127
|
+
known = source_queue_column_answer(@source_queue_column) # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
128
|
+
return known unless known.nil?
|
|
129
|
+
|
|
130
|
+
@source_queue_column_mutex.synchronize do
|
|
131
|
+
known = source_queue_column_answer(@source_queue_column)
|
|
132
|
+
next known unless known.nil?
|
|
133
|
+
|
|
134
|
+
supported = detect_source_queue_column(warn: @source_queue_column.nil?)
|
|
135
|
+
@source_queue_column = supported || (monotonic_now + SOURCE_QUEUE_RECHECK_SECONDS)
|
|
136
|
+
supported
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Clears the memoized probe: specs exercise both schema shapes, and a
|
|
141
|
+
# failed legacy write re-probes after the migration lands.
|
|
142
|
+
def reset_source_queue_column_check!
|
|
143
|
+
@source_queue_column_mutex.synchronize { @source_queue_column = nil }
|
|
144
|
+
end
|
|
145
|
+
|
|
110
146
|
private
|
|
111
147
|
|
|
148
|
+
# WHERE fragment and binds naming one message's rows.
|
|
149
|
+
def message_scope(queue_name, msg_id, source_queue)
|
|
150
|
+
return ["queue_name = $1 AND msg_id = $2", [queue_name, msg_id.to_i]] unless source_queue_column?
|
|
151
|
+
|
|
152
|
+
["queue_name = $1 AND msg_id = $2 AND (source_queue = $3 OR source_queue IS NULL)",
|
|
153
|
+
[queue_name, msg_id.to_i, source_queue]]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# true, false while a missing column's answer stands, nil to probe.
|
|
157
|
+
def source_queue_column_answer(state)
|
|
158
|
+
return true if state == true
|
|
159
|
+
|
|
160
|
+
false if state && monotonic_now < state
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def monotonic_now
|
|
164
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def detect_source_queue_column(warn:)
|
|
168
|
+
supported = connection.column_exists?(:pgbus_failed_events, :source_queue)
|
|
169
|
+
if warn && !supported
|
|
170
|
+
Pgbus.logger.warn do
|
|
171
|
+
"[Pgbus] pgbus_failed_events is missing the source_queue column; under priority routing two " \
|
|
172
|
+
"sub-queues' failures with the same msg_id share one row. " \
|
|
173
|
+
"Run `rails generate pgbus:add_failed_events_source_queue` (or pgbus:update) and migrate."
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
supported
|
|
177
|
+
end
|
|
178
|
+
|
|
112
179
|
def iso_utc(value)
|
|
113
180
|
time = value.is_a?(String) ? Time.parse(value) : value
|
|
114
181
|
time&.utc&.iso8601(6)
|
|
@@ -124,5 +191,31 @@ module Pgbus
|
|
|
124
191
|
end
|
|
125
192
|
end
|
|
126
193
|
end
|
|
194
|
+
|
|
195
|
+
UPSERT_ERROR_COLUMNS = <<~SQL.gsub(/\s+/, " ").strip.freeze
|
|
196
|
+
DO UPDATE SET
|
|
197
|
+
error_class = EXCLUDED.error_class,
|
|
198
|
+
error_message = EXCLUDED.error_message,
|
|
199
|
+
backtrace = EXCLUDED.backtrace,
|
|
200
|
+
retry_count = EXCLUDED.retry_count,
|
|
201
|
+
failed_at = EXCLUDED.failed_at
|
|
202
|
+
SQL
|
|
203
|
+
private_constant :UPSERT_ERROR_COLUMNS
|
|
204
|
+
|
|
205
|
+
RECORD_SQL = <<~SQL.gsub(/\s+/, " ").strip.freeze
|
|
206
|
+
INSERT INTO pgbus_failed_events
|
|
207
|
+
(queue_name, source_queue, msg_id, payload, headers, error_class, error_message, backtrace, retry_count, failed_at)
|
|
208
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP)
|
|
209
|
+
ON CONFLICT (queue_name, msg_id, source_queue) #{UPSERT_ERROR_COLUMNS}
|
|
210
|
+
SQL
|
|
211
|
+
private_constant :RECORD_SQL
|
|
212
|
+
|
|
213
|
+
LEGACY_RECORD_SQL = <<~SQL.gsub(/\s+/, " ").strip.freeze
|
|
214
|
+
INSERT INTO pgbus_failed_events
|
|
215
|
+
(queue_name, msg_id, payload, headers, error_class, error_message, backtrace, retry_count, failed_at)
|
|
216
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP)
|
|
217
|
+
ON CONFLICT (queue_name, msg_id) #{UPSERT_ERROR_COLUMNS}
|
|
218
|
+
SQL
|
|
219
|
+
private_constant :LEGACY_RECORD_SQL
|
|
127
220
|
end
|
|
128
221
|
end
|
|
@@ -97,6 +97,7 @@ module Pgbus
|
|
|
97
97
|
add_workflows: "pgbus:add_workflows",
|
|
98
98
|
add_workflow_signals: "pgbus:add_workflow_signals",
|
|
99
99
|
add_failed_events_index: "pgbus:add_failed_events_index",
|
|
100
|
+
add_failed_events_source_queue: "pgbus:add_failed_events_source_queue",
|
|
100
101
|
add_processed_event_completion: "pgbus:add_processed_event_completion",
|
|
101
102
|
add_batch_executions: "pgbus:add_batch_executions",
|
|
102
103
|
add_batch_callback_jobs: "pgbus:add_batch_callback_jobs",
|
|
@@ -122,6 +123,7 @@ module Pgbus
|
|
|
122
123
|
add_workflows: "workflow tables (durable multi-step workflows: runs, step attempts, audit trail)",
|
|
123
124
|
add_workflow_signals: "waiting workflow steps (fan_out, wait_for: signals, wait timeouts)",
|
|
124
125
|
add_failed_events_index: "unique index on pgbus_failed_events (queue_name, msg_id)",
|
|
126
|
+
add_failed_events_source_queue: "source_queue on pgbus_failed_events (one row per priority sub-queue's failure)",
|
|
125
127
|
add_processed_event_completion: "completed_at on pgbus_processed_events (two-phase idempotency claim)",
|
|
126
128
|
add_batch_executions: "batch execution-row tracking (self-healing completion, on_failure rename)",
|
|
127
129
|
add_batch_callback_jobs: "batch callback-job columns (configured ActiveJob instances as batch callbacks)",
|
|
@@ -151,7 +153,7 @@ module Pgbus
|
|
|
151
153
|
*recurring_migrations,
|
|
152
154
|
*deploy_tasks_migrations,
|
|
153
155
|
*workflows_migrations,
|
|
154
|
-
*
|
|
156
|
+
*failed_events_migrations,
|
|
155
157
|
*processed_event_completion_migrations,
|
|
156
158
|
*batch_executions_migrations,
|
|
157
159
|
*batch_callback_jobs_migrations,
|
|
@@ -266,16 +268,21 @@ module Pgbus
|
|
|
266
268
|
migrations
|
|
267
269
|
end
|
|
268
270
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
# FailedEventRecorder's upsert needs the unique index that matches its
|
|
272
|
+
# ON CONFLICT target, or it fails and the failure goes unrecorded. The
|
|
273
|
+
# source_queue migration (issue #583) adds the column, creates the
|
|
274
|
+
# (queue_name, msg_id, source_queue) index and drops the old
|
|
275
|
+
# (queue_name, msg_id) one, so it is the only migration queued here:
|
|
276
|
+
# add_failed_events_index would put back the index that collapses two
|
|
277
|
+
# priority sub-queues' failures into one row. That index still present
|
|
278
|
+
# (or put back by running the old generator by name) queues it again.
|
|
279
|
+
def failed_events_migrations
|
|
275
280
|
return [] unless table_exists?("pgbus_failed_events")
|
|
276
|
-
return [] if
|
|
281
|
+
return [] if column_names("pgbus_failed_events").include?("source_queue") &&
|
|
282
|
+
index_exists?("pgbus_failed_events", "idx_pgbus_failed_events_queue_msg_source") &&
|
|
283
|
+
!index_exists?("pgbus_failed_events", "idx_pgbus_failed_events_queue_msg")
|
|
277
284
|
|
|
278
|
-
[:
|
|
285
|
+
[:add_failed_events_source_queue]
|
|
279
286
|
end
|
|
280
287
|
|
|
281
288
|
# completed_at backs the two-phase idempotency claim (issue #385).
|
|
@@ -267,7 +267,10 @@ module Pgbus
|
|
|
267
267
|
Pgbus.client.archive_message(queue_name, message.msg_id.to_i)
|
|
268
268
|
# A redelivery that succeeds leaves no stale failure behind. First
|
|
269
269
|
# deliveries never had one: no query on the hot path (#484's gate).
|
|
270
|
-
|
|
270
|
+
if message.read_ct.to_i > 1
|
|
271
|
+
FailedEventRecorder.clear!(queue_name: queue_name, msg_id: message.msg_id.to_i,
|
|
272
|
+
source_queue: config.queue_name(queue_name))
|
|
273
|
+
end
|
|
271
274
|
@circuit_breaker.record_success(queue_name)
|
|
272
275
|
record_stat(message, queue_name, "success", execution_start)
|
|
273
276
|
rescue StandardError => e
|
|
@@ -293,7 +296,8 @@ module Pgbus
|
|
|
293
296
|
# failed-event row, then announce it. A move that raises keeps the row,
|
|
294
297
|
# so the next attempt still carries the reason, and announces nothing.
|
|
295
298
|
def dead_letter(message, queue_name)
|
|
296
|
-
last_error = FailedEventRecorder.last_error(queue_name: queue_name, msg_id: message.msg_id.to_i
|
|
299
|
+
last_error = FailedEventRecorder.last_error(queue_name: queue_name, msg_id: message.msg_id.to_i,
|
|
300
|
+
source_queue: config.queue_name(queue_name))
|
|
297
301
|
headers = DeadLetterHeader.build(
|
|
298
302
|
existing: message.headers, reason: DeadLetterHeader::REASON_MAX_RETRIES, source: "consumer",
|
|
299
303
|
source_queue: config.queue_name(queue_name), source_msg_id: message.msg_id.to_i,
|
|
@@ -301,7 +305,8 @@ module Pgbus
|
|
|
301
305
|
max_retries: config.max_retries, error: last_error
|
|
302
306
|
)
|
|
303
307
|
Pgbus.client.move_to_dead_letter(queue_name, message, headers: headers)
|
|
304
|
-
FailedEventRecorder.clear!(queue_name: queue_name, msg_id: message.msg_id.to_i
|
|
308
|
+
FailedEventRecorder.clear!(queue_name: queue_name, msg_id: message.msg_id.to_i,
|
|
309
|
+
source_queue: config.queue_name(queue_name))
|
|
305
310
|
instrument_dead_lettered(message, queue_name, last_error)
|
|
306
311
|
end
|
|
307
312
|
|
|
@@ -361,10 +366,12 @@ module Pgbus
|
|
|
361
366
|
# failure, under the LOGICAL queue name, so the dashboard reads the event
|
|
362
367
|
# as retrying with its error. No ErrorReporter call: Handler#process!
|
|
363
368
|
# already instruments pgbus.event_failed, which feeds the reporters.
|
|
369
|
+
# A handler queue has no priority sub-queues: its one table is the source.
|
|
364
370
|
def record_failed_event(message, queue_name, error)
|
|
365
371
|
FailedEventRecorder.record!(
|
|
366
|
-
queue_name: queue_name, msg_id: message.msg_id.to_i,
|
|
367
|
-
headers: message.headers, error: error,
|
|
372
|
+
queue_name: queue_name, msg_id: message.msg_id.to_i, source_queue: config.queue_name(queue_name),
|
|
373
|
+
payload: message.message, headers: message.headers, error: error,
|
|
374
|
+
retry_count: [message.read_ct.to_i - 1, 0].max
|
|
368
375
|
)
|
|
369
376
|
end
|
|
370
377
|
|
|
@@ -32,6 +32,7 @@ module Pgbus
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def start
|
|
35
|
+
@started = true
|
|
35
36
|
register_process
|
|
36
37
|
@timer = Concurrent::TimerTask.new(execution_interval: INTERVAL) { beat }
|
|
37
38
|
@timer.execute
|
|
@@ -44,7 +45,8 @@ module Pgbus
|
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def beat
|
|
47
|
-
return
|
|
48
|
+
return if @stopped
|
|
49
|
+
return retry_registration unless @process_id
|
|
48
50
|
|
|
49
51
|
@on_beat&.call
|
|
50
52
|
updates = { last_heartbeat_at: Time.current }
|
|
@@ -57,6 +59,15 @@ module Pgbus
|
|
|
57
59
|
|
|
58
60
|
private
|
|
59
61
|
|
|
62
|
+
# The registration at start failed (a DB blip at boot). Retry it here so
|
|
63
|
+
# the process does not stay invisible: a pipeless child checks in with
|
|
64
|
+
# the supervisor's boot deadline only through this row (issue #587).
|
|
65
|
+
def retry_registration
|
|
66
|
+
return unless @started
|
|
67
|
+
|
|
68
|
+
@mutex.synchronize { register_process unless @stopped }
|
|
69
|
+
end
|
|
70
|
+
|
|
60
71
|
# Build the metadata hash to persist on this beat. Returns nil when no
|
|
61
72
|
# supplier is configured so heartbeats without dynamic metadata
|
|
62
73
|
# (supervisor/dispatcher/scheduler) leave the column untouched, exactly
|
|
@@ -7,12 +7,20 @@ module Pgbus
|
|
|
7
7
|
# server's accept thread — the immutability is what makes the cross-thread
|
|
8
8
|
# handoff safe without a lock (issue #386).
|
|
9
9
|
#
|
|
10
|
-
# `expected` is the child count forked by boot_processes; `
|
|
11
|
-
# current fork-table size
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
|
|
10
|
+
# `expected` is the child count forked by boot_processes; `forked` is the
|
|
11
|
+
# current fork-table size; `live` counts the forks that have checked in
|
|
12
|
+
# (a process row on this host, or a liveness byte). A child sitting in
|
|
13
|
+
# crash-restart backoff, or one that forked and hangs before checking in
|
|
14
|
+
# (issue #587), keeps `live < expected`, which is exactly the signal a
|
|
15
|
+
# rolling deploy's health gate needs to fail on: the replacement
|
|
16
|
+
# container never goes ready, and the orchestrator keeps the old
|
|
17
|
+
# container running. `forked` defaults to `live` for callers that do not
|
|
18
|
+
# track check-ins.
|
|
19
|
+
ReadinessSnapshot = Data.define(:booted, :shutting_down, :expected, :live, :forked) do
|
|
20
|
+
def initialize(booted:, shutting_down:, expected:, live:, forked: live)
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
|
|
16
24
|
def ready?
|
|
17
25
|
booted && !shutting_down && live >= expected
|
|
18
26
|
end
|