pgbus 0.9.8 → 0.9.9
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 +329 -0
- data/README.md +336 -25
- data/lib/generators/pgbus/{add_job_locks_generator.rb → add_uniqueness_keys_generator.rb} +5 -5
- data/lib/pgbus/active_job/adapter.rb +1 -1
- data/lib/pgbus/config_loader.rb +29 -2
- data/lib/pgbus/configuration.rb +202 -73
- data/lib/pgbus/doctor.rb +28 -3
- data/lib/pgbus/execution_pools/async_pool.rb +2 -2
- data/lib/pgbus/generators/config_converter.rb +7 -5
- data/lib/pgbus/generators/migration_detector.rb +20 -16
- data/lib/pgbus/instrumentation.rb +2 -1
- data/lib/pgbus/integrations/appsignal/subscriber.rb +17 -2
- data/lib/pgbus/metrics/subscriber.rb +26 -2
- data/lib/pgbus/metrics.rb +3 -2
- data/lib/pgbus/pgmq_schema/pgmq_v1.11.1.sql +2126 -0
- data/lib/pgbus/pgmq_schema.rb +7 -2
- data/lib/pgbus/process/consumer.rb +141 -12
- data/lib/pgbus/process/supervisor.rb +33 -10
- data/lib/pgbus/process/worker.rb +6 -16
- data/lib/pgbus/recurring/schedule.rb +1 -2
- data/lib/pgbus/serializer.rb +4 -4
- data/lib/pgbus/streams/broadcastable_override.rb +0 -8
- data/lib/pgbus/streams/signed_name.rb +2 -2
- data/lib/pgbus/uniqueness.rb +11 -12
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +20 -4
- data/lib/pgbus/web/payload_filter.rb +3 -3
- data/lib/pgbus/web/streamer/instance.rb +6 -4
- data/lib/pgbus/web/streamer/listener.rb +21 -36
- data/lib/pgbus.rb +46 -6
- metadata +4 -6
- data/app/models/pgbus/job_lock.rb +0 -98
- data/lib/generators/pgbus/templates/add_job_locks.rb.erb +0 -21
- data/lib/rubocop/cop/pgbus/no_ruby_timeout.rb +0 -42
- data/lib/rubocop/pgbus.rb +0 -5
|
@@ -19,10 +19,11 @@ module Pgbus
|
|
|
19
19
|
#
|
|
20
20
|
# Health check: `wait_for_notify(timeout)` returns nil on timeout. When
|
|
21
21
|
# it does, the listener runs `SELECT 1` as a TCP keepalive. If that
|
|
22
|
-
# raises, the
|
|
23
|
-
#
|
|
24
|
-
# (silently dropped LISTEN connections from NAT / PG
|
|
25
|
-
# blips)
|
|
22
|
+
# raises, the listener rebuilds a FRESH connection via connection_factory
|
|
23
|
+
# and re-LISTENs every channel in `@listening_to`. This is the fix for
|
|
24
|
+
# design doc §11 #1 (silently dropped LISTEN connections from NAT / PG
|
|
25
|
+
# restart / network blips) — a fresh connect also re-resolves DNS and
|
|
26
|
+
# converges on the promoted primary after a failover.
|
|
26
27
|
#
|
|
27
28
|
# NOTIFY channel naming (from pgmq_v1.11.0.sql:1634):
|
|
28
29
|
# PG_NOTIFY('pgmq.' || TG_TABLE_NAME || '.' || TG_OP, NULL)
|
|
@@ -42,12 +43,15 @@ module Pgbus
|
|
|
42
43
|
|
|
43
44
|
attr_reader :listening_to
|
|
44
45
|
|
|
45
|
-
# @param connection_factory [#call
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
46
|
+
# @param connection_factory [#call] builds a FRESH PG connection on each
|
|
47
|
+
# reconnect attempt (the Instance passes -> { build_raw_pg_connection }).
|
|
48
|
+
# Required — the reconnect loop always rebuilds rather than resetting a
|
|
49
|
+
# possibly-dead socket, so every caller (production and tests) must pass
|
|
50
|
+
# one.
|
|
49
51
|
def initialize(pg_connection:, dispatch_queue:, health_check_ms:,
|
|
50
|
-
logger: Pgbus.logger
|
|
52
|
+
connection_factory:, logger: Pgbus.logger)
|
|
53
|
+
raise ArgumentError, "connection_factory is required" unless connection_factory.respond_to?(:call)
|
|
54
|
+
|
|
51
55
|
@conn = pg_connection
|
|
52
56
|
@dispatch_queue = dispatch_queue
|
|
53
57
|
@health_check_ms = health_check_ms
|
|
@@ -210,11 +214,8 @@ module Pgbus
|
|
|
210
214
|
# NotifyListener#reconnect!: a single failed attempt never returns with
|
|
211
215
|
# a dead connection while running, so ensure_listening acks can't
|
|
212
216
|
# succeed vacuously against a broken LISTEN socket and strand SSE
|
|
213
|
-
# clients.
|
|
214
|
-
# pg_connection:), fall back to the legacy single-shot conn.reset.
|
|
217
|
+
# clients.
|
|
215
218
|
def reconnect!
|
|
216
|
-
return reconnect_via_reset unless @connection_factory
|
|
217
|
-
|
|
218
219
|
# Snapshot the canonical subscription set BEFORE tearing down the old
|
|
219
220
|
# connection. We rebuild @listening_to from this and only publish it
|
|
220
221
|
# once every channel has been re-LISTENed on the new conn, so a
|
|
@@ -234,10 +235,13 @@ module Pgbus
|
|
|
234
235
|
# primary; re-LISTENing on a replica registers channels that never wake.
|
|
235
236
|
Pgbus::Process::PrimaryValidator.validate_primary!(new_conn)
|
|
236
237
|
channels.each { |channel| new_conn.exec(%(LISTEN "#{channel}")) }
|
|
237
|
-
rescue PG::Error, Pgbus::Process::ReplicaConnectionError => e
|
|
238
|
-
# The factory may
|
|
239
|
-
#
|
|
240
|
-
#
|
|
238
|
+
rescue PG::Error, Pgbus::Process::ReplicaConnectionError, Pgbus::ConfigurationError => e
|
|
239
|
+
# The factory may raise a ConfigurationError (a bad
|
|
240
|
+
# streams_connection_options, e.g. a pooled AR connection) or return
|
|
241
|
+
# a live conn before a later LISTEN raised (or validate_primary!
|
|
242
|
+
# rejected a replica). Close any partial conn so repeated failures
|
|
243
|
+
# don't leak PG connections, then back off and retry rather than
|
|
244
|
+
# letting the exception kill the listener thread silently.
|
|
241
245
|
close_quietly(new_conn)
|
|
242
246
|
@logger.error { "[Pgbus::Streamer::Listener] reconnect failed: #{e.class}: #{e.message}" }
|
|
243
247
|
sleep RECONNECT_BACKOFF_SECONDS
|
|
@@ -250,25 +254,6 @@ module Pgbus
|
|
|
250
254
|
end
|
|
251
255
|
end
|
|
252
256
|
|
|
253
|
-
# Legacy fallback for tests that inject only pg_connection: (no factory).
|
|
254
|
-
# A single conn.reset attempt; on failure it logs and backs off, leaving
|
|
255
|
-
# recovery to the next run_loop cycle. Kept for backwards compatibility
|
|
256
|
-
# with test wiring; production always injects a connection_factory.
|
|
257
|
-
def reconnect_via_reset
|
|
258
|
-
@conn.reset
|
|
259
|
-
Pgbus::Process::PrimaryValidator.validate_primary!(@conn)
|
|
260
|
-
to_relisten = @listening_to.to_a
|
|
261
|
-
new_listening = Set.new
|
|
262
|
-
to_relisten.each do |channel|
|
|
263
|
-
@conn.exec(%(LISTEN "#{channel}"))
|
|
264
|
-
new_listening.add(channel)
|
|
265
|
-
end
|
|
266
|
-
@listening_to = new_listening
|
|
267
|
-
rescue PG::Error, Pgbus::Process::ReplicaConnectionError => e
|
|
268
|
-
@logger.error { "[Pgbus::Streamer::Listener] reconnect failed: #{e.class}: #{e.message}" }
|
|
269
|
-
sleep RECONNECT_BACKOFF_SECONDS
|
|
270
|
-
end
|
|
271
|
-
|
|
272
257
|
# Close a PG connection we're discarding (the old conn at the top of a
|
|
273
258
|
# reconnect cycle, or a half-built one whose LISTEN raised). Best-effort.
|
|
274
259
|
def close_quietly(conn)
|
data/lib/pgbus.rb
CHANGED
|
@@ -10,6 +10,24 @@ module Pgbus
|
|
|
10
10
|
# this to be configurable.
|
|
11
11
|
DEAD_LETTER_SUFFIX = "_dlq"
|
|
12
12
|
|
|
13
|
+
# Error-hierarchy policy (the 1.0 contract, issue #282):
|
|
14
|
+
#
|
|
15
|
+
# * OPERATIONAL errors — a pgbus subsystem failed to do its job at runtime
|
|
16
|
+
# (bad configuration, a pool that's shutting down, a batch enqueue that
|
|
17
|
+
# came back short, a rejected GlobalID, a missing PGMQ version) — descend
|
|
18
|
+
# from Pgbus::Error. `rescue Pgbus::Error` is meant to catch every one of
|
|
19
|
+
# them; that is the whole point of the hierarchy.
|
|
20
|
+
#
|
|
21
|
+
# * ARGUMENT-SHAPE errors — the caller handed a method a malformed argument
|
|
22
|
+
# (a stream name too long for the queue-name budget, an unparseable
|
|
23
|
+
# cursor, a capsule DSL line that doesn't parse) — stay ArgumentError
|
|
24
|
+
# subclasses. That's what ArgumentError means, and rescuing it is the
|
|
25
|
+
# caller's responsibility, not an operational concern. These live in
|
|
26
|
+
# their own files: Streams::StreamNameTooLong, Streams::Cursor::InvalidCursor,
|
|
27
|
+
# Configuration::CapsuleDSL::ParseError.
|
|
28
|
+
#
|
|
29
|
+
# When adding a new error class, decide which half it belongs to and parent
|
|
30
|
+
# it accordingly — do not default to StandardError.
|
|
13
31
|
class Error < StandardError; end
|
|
14
32
|
class ConfigurationError < Error; end
|
|
15
33
|
class SerializationError < Error; end
|
|
@@ -20,6 +38,16 @@ module Pgbus
|
|
|
20
38
|
class SchemaNotReady < Error; end
|
|
21
39
|
class ReadTimeoutError < Error; end
|
|
22
40
|
|
|
41
|
+
# Raised by the ActiveJob adapter when a batch enqueue (perform_all_later)
|
|
42
|
+
# comes back with a msg_id count that doesn't match the number of jobs sent —
|
|
43
|
+
# a data-integrity signal that some jobs may not have been persisted.
|
|
44
|
+
class EnqueueError < Error; end
|
|
45
|
+
|
|
46
|
+
# Raised by the execution pools when work can't be accepted: the pool is
|
|
47
|
+
# shutting down, or it's momentarily at capacity. Consumer-reachable during
|
|
48
|
+
# job submission, so it descends from Pgbus::Error rather than RuntimeError.
|
|
49
|
+
class ExecutionPoolError < Error; end
|
|
50
|
+
|
|
23
51
|
# Raised by Client read paths when the in-memory connection-health circuit
|
|
24
52
|
# breaker (Client::ConnectionHealth) is open: the database has failed enough
|
|
25
53
|
# consecutive connection attempts that reads now fail fast without a pool
|
|
@@ -85,12 +113,6 @@ module Pgbus
|
|
|
85
113
|
# root and tries to autoload Puma::Plugin, which collides with the real
|
|
86
114
|
# Puma::Plugin class defined by the puma gem itself.
|
|
87
115
|
loader.ignore("#{__dir__}/puma")
|
|
88
|
-
# lib/rubocop holds pgbus's custom RuboCop cops (Pgbus/NoRubyTimeout).
|
|
89
|
-
# They're loaded by RuboCop via .rubocop.yml's `require:`, not by the
|
|
90
|
-
# gem at runtime. Without this ignore, Zeitwerk scans lib/rubocop under
|
|
91
|
-
# the pgbus root and tries to autoload a `Rubocop` constant, colliding
|
|
92
|
-
# with the rubocop gem's real `RuboCop`.
|
|
93
|
-
loader.ignore("#{__dir__}/rubocop")
|
|
94
116
|
loader
|
|
95
117
|
end
|
|
96
118
|
end
|
|
@@ -187,6 +209,24 @@ module Pgbus
|
|
|
187
209
|
Streams::Key.stream_key!(key)
|
|
188
210
|
end
|
|
189
211
|
|
|
212
|
+
# Publish an event to the bus — the top-level shortcut for
|
|
213
|
+
# `Pgbus::EventBus::Publisher.publish`, symmetric with `Pgbus.stream`.
|
|
214
|
+
#
|
|
215
|
+
# Pgbus.publish("orders.created", { order_id: 42 })
|
|
216
|
+
# Pgbus.publish("orders.created", order, headers: { "x-trace" => id }, delay: 30)
|
|
217
|
+
def publish(routing_key, payload, headers: nil, delay: 0)
|
|
218
|
+
EventBus::Publisher.publish(routing_key, payload, headers: headers, delay: delay)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Publish an event with a delay. Mirrors
|
|
222
|
+
# `Pgbus::EventBus::Publisher.publish_later`; `delay:` is required (a
|
|
223
|
+
# publish_later with no delay is just publish).
|
|
224
|
+
#
|
|
225
|
+
# Pgbus.publish_later("orders.reminder", { order_id: 42 }, delay: 1.hour)
|
|
226
|
+
def publish_later(routing_key, payload, delay:, headers: nil)
|
|
227
|
+
EventBus::Publisher.publish_later(routing_key, payload, delay: delay, headers: headers)
|
|
228
|
+
end
|
|
229
|
+
|
|
190
230
|
def reset!
|
|
191
231
|
@client&.close
|
|
192
232
|
@client = nil
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgbus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -150,7 +150,6 @@ files:
|
|
|
150
150
|
- app/models/pgbus/application_record.rb
|
|
151
151
|
- app/models/pgbus/batch_entry.rb
|
|
152
152
|
- app/models/pgbus/blocked_execution.rb
|
|
153
|
-
- app/models/pgbus/job_lock.rb
|
|
154
153
|
- app/models/pgbus/job_stat.rb
|
|
155
154
|
- app/models/pgbus/outbox_entry.rb
|
|
156
155
|
- app/models/pgbus/process_entry.rb
|
|
@@ -209,7 +208,6 @@ files:
|
|
|
209
208
|
- exe/pgbus
|
|
210
209
|
- lib/active_job/queue_adapters/pgbus_adapter.rb
|
|
211
210
|
- lib/generators/pgbus/add_failed_events_index_generator.rb
|
|
212
|
-
- lib/generators/pgbus/add_job_locks_generator.rb
|
|
213
211
|
- lib/generators/pgbus/add_job_stats_generator.rb
|
|
214
212
|
- lib/generators/pgbus/add_job_stats_latency_generator.rb
|
|
215
213
|
- lib/generators/pgbus/add_job_stats_queue_index_generator.rb
|
|
@@ -218,11 +216,11 @@ files:
|
|
|
218
216
|
- lib/generators/pgbus/add_queue_states_generator.rb
|
|
219
217
|
- lib/generators/pgbus/add_recurring_generator.rb
|
|
220
218
|
- lib/generators/pgbus/add_stream_stats_generator.rb
|
|
219
|
+
- lib/generators/pgbus/add_uniqueness_keys_generator.rb
|
|
221
220
|
- lib/generators/pgbus/install_generator.rb
|
|
222
221
|
- lib/generators/pgbus/migrate_job_locks_generator.rb
|
|
223
222
|
- lib/generators/pgbus/migration_path.rb
|
|
224
223
|
- lib/generators/pgbus/templates/add_failed_events_unique_index.rb.erb
|
|
225
|
-
- lib/generators/pgbus/templates/add_job_locks.rb.erb
|
|
226
224
|
- lib/generators/pgbus/templates/add_job_stats.rb.erb
|
|
227
225
|
- lib/generators/pgbus/templates/add_job_stats_latency.rb.erb
|
|
228
226
|
- lib/generators/pgbus/templates/add_job_stats_queue_index.rb.erb
|
|
@@ -318,6 +316,7 @@ files:
|
|
|
318
316
|
- lib/pgbus/outbox/poller.rb
|
|
319
317
|
- lib/pgbus/pgmq_schema.rb
|
|
320
318
|
- lib/pgbus/pgmq_schema/pgmq_v1.11.0.sql
|
|
319
|
+
- lib/pgbus/pgmq_schema/pgmq_v1.11.1.sql
|
|
321
320
|
- lib/pgbus/process/consumer.rb
|
|
322
321
|
- lib/pgbus/process/consumer_priority.rb
|
|
323
322
|
- lib/pgbus/process/dispatcher.rb
|
|
@@ -383,8 +382,6 @@ files:
|
|
|
383
382
|
- lib/pgbus/web/streamer/stream_counter.rb
|
|
384
383
|
- lib/pgbus/web/streamer/stream_event_dispatcher.rb
|
|
385
384
|
- lib/puma/plugin/pgbus_streams.rb
|
|
386
|
-
- lib/rubocop/cop/pgbus/no_ruby_timeout.rb
|
|
387
|
-
- lib/rubocop/pgbus.rb
|
|
388
385
|
- lib/tasks/pgbus_autovacuum.rake
|
|
389
386
|
- lib/tasks/pgbus_doctor.rake
|
|
390
387
|
- lib/tasks/pgbus_pgmq.rake
|
|
@@ -397,6 +394,7 @@ metadata:
|
|
|
397
394
|
homepage_uri: https://github.com/mhenrixon/pgbus
|
|
398
395
|
source_code_uri: https://github.com/mhenrixon/pgbus/tree/main
|
|
399
396
|
changelog_uri: https://github.com/mhenrixon/pgbus/blob/main/CHANGELOG.md
|
|
397
|
+
documentation_uri: https://pgbus.zoolutions.llc
|
|
400
398
|
rubygems_mfa_required: 'true'
|
|
401
399
|
rdoc_options: []
|
|
402
400
|
require_paths:
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Pgbus
|
|
4
|
-
class JobLock < BusRecord
|
|
5
|
-
self.table_name = "pgbus_job_locks"
|
|
6
|
-
|
|
7
|
-
# States:
|
|
8
|
-
# queued — lock held from enqueue time (:until_executed), no worker yet
|
|
9
|
-
# executing — lock held by an active worker process
|
|
10
|
-
STATES = %w[queued executing].freeze
|
|
11
|
-
|
|
12
|
-
scope :executing, -> { where(state: "executing") }
|
|
13
|
-
scope :queued_locks, -> { where(state: "queued") }
|
|
14
|
-
scope :expired, ->(now = Time.current) { where("expires_at < ?", now) }
|
|
15
|
-
|
|
16
|
-
# Atomically try to acquire a lock.
|
|
17
|
-
# Cleans up expired locks for this key first (crash recovery at acquire time).
|
|
18
|
-
# Returns true if acquired, false if already locked.
|
|
19
|
-
#
|
|
20
|
-
# Uses raw SQL on the hot path to minimize ActiveRecord allocations
|
|
21
|
-
# (~29 objects vs ~304 per acquire+release cycle with AR query builder).
|
|
22
|
-
def self.acquire!(lock_key, job_class:, ttl:, job_id: nil, state: "queued", owner_pid: nil, owner_hostname: nil) # rubocop:disable Naming/PredicateMethod
|
|
23
|
-
expires_at = Time.current + ttl
|
|
24
|
-
|
|
25
|
-
# Remove any expired lock for this key inline (last-resort TTL recovery)
|
|
26
|
-
connection.exec_delete(
|
|
27
|
-
"DELETE FROM #{table_name} WHERE lock_key = $1 AND expires_at < $2",
|
|
28
|
-
"JobLock Expire", [lock_key, Time.current]
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
result = connection.exec_query(
|
|
32
|
-
"INSERT INTO #{table_name} (lock_key, job_class, job_id, state, owner_pid, owner_hostname, expires_at) " \
|
|
33
|
-
"VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (lock_key) DO NOTHING RETURNING id",
|
|
34
|
-
"JobLock Acquire", [lock_key, job_class, job_id, state, owner_pid, owner_hostname, expires_at]
|
|
35
|
-
)
|
|
36
|
-
result.rows.any?
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Transition a queued lock to executing state and claim ownership.
|
|
40
|
-
# Called when a worker starts executing a job that was locked at enqueue time.
|
|
41
|
-
def self.claim_for_execution!(lock_key, owner_pid:, owner_hostname:, ttl:)
|
|
42
|
-
connection.exec_update(
|
|
43
|
-
"UPDATE #{table_name} SET state = $1, owner_pid = $2, owner_hostname = $3, expires_at = $4 " \
|
|
44
|
-
"WHERE lock_key = $5",
|
|
45
|
-
"JobLock Claim", ["executing", owner_pid, owner_hostname, Time.current + ttl, lock_key]
|
|
46
|
-
)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Release a lock by key.
|
|
50
|
-
def self.release!(lock_key)
|
|
51
|
-
connection.exec_delete(
|
|
52
|
-
"DELETE FROM #{table_name} WHERE lock_key = $1",
|
|
53
|
-
"JobLock Release", [lock_key]
|
|
54
|
-
)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Check if a lock is currently held (regardless of expiry — reaper handles orphans).
|
|
58
|
-
def self.locked?(lock_key)
|
|
59
|
-
result = connection.select_value(
|
|
60
|
-
"SELECT 1 FROM #{table_name} WHERE lock_key = $1 LIMIT 1", "JobLock Check", [lock_key]
|
|
61
|
-
)
|
|
62
|
-
!result.nil?
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Reap orphaned locks whose owner is no longer alive, plus stale queued
|
|
66
|
-
# locks that were never claimed by a worker.
|
|
67
|
-
# Returns the total number of orphaned locks released.
|
|
68
|
-
def self.reap_orphaned!
|
|
69
|
-
reaped = 0
|
|
70
|
-
|
|
71
|
-
# 1. Executing locks whose owner process has no healthy heartbeat
|
|
72
|
-
alive_workers = ProcessEntry
|
|
73
|
-
.where("last_heartbeat_at >= ?", Time.current - Process::Heartbeat::ALIVE_THRESHOLD)
|
|
74
|
-
.pluck(:pid, :hostname)
|
|
75
|
-
|
|
76
|
-
orphaned_executing = executing.select do |lock|
|
|
77
|
-
alive_workers.none? { |pid, hostname| pid == lock.owner_pid && hostname == lock.owner_hostname }
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
reaped += where(id: orphaned_executing.map(&:id)).delete_all if orphaned_executing.any?
|
|
81
|
-
|
|
82
|
-
# 2. Queued locks older than the visibility timeout that were never
|
|
83
|
-
# claimed. These are left behind when enqueue fails after lock
|
|
84
|
-
# acquisition (e.g. network error, process crash).
|
|
85
|
-
threshold = Pgbus.configuration.visibility_timeout
|
|
86
|
-
stale_queued = queued_locks.where("locked_at < ?", Time.current - threshold)
|
|
87
|
-
reaped += stale_queued.delete_all if stale_queued.exists?
|
|
88
|
-
|
|
89
|
-
reaped
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Last-resort cleanup: delete locks whose expires_at has passed.
|
|
93
|
-
# This only fires when the reaper itself can't run (e.g., entire supervisor dead).
|
|
94
|
-
def self.cleanup_expired!
|
|
95
|
-
expired.delete_all
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
class AddPgbusJobLocks < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
-
def change
|
|
3
|
-
create_table :pgbus_job_locks do |t|
|
|
4
|
-
t.string :lock_key, null: false
|
|
5
|
-
t.string :job_class, null: false
|
|
6
|
-
t.string :job_id
|
|
7
|
-
t.string :state, null: false, default: "queued"
|
|
8
|
-
t.integer :owner_pid
|
|
9
|
-
t.string :owner_hostname
|
|
10
|
-
t.datetime :locked_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
|
11
|
-
t.datetime :expires_at, null: false
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
add_index :pgbus_job_locks, :lock_key,
|
|
15
|
-
unique: true, name: "idx_pgbus_job_locks_key"
|
|
16
|
-
add_index :pgbus_job_locks, :expires_at,
|
|
17
|
-
name: "idx_pgbus_job_locks_expires"
|
|
18
|
-
add_index :pgbus_job_locks, [:state, :owner_pid],
|
|
19
|
-
name: "idx_pgbus_job_locks_reaper"
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RuboCop
|
|
4
|
-
module Cop
|
|
5
|
-
module Pgbus
|
|
6
|
-
# Flags any use of Ruby's `Timeout.timeout`. It interrupts the target
|
|
7
|
-
# thread with `Thread#raise`, which can fire at an arbitrary point —
|
|
8
|
-
# including mid-libpq-call — and leave a pooled `PG::Connection` in a
|
|
9
|
-
# corrupt state that re-hangs or returns wrong results on reuse. Prefer a
|
|
10
|
-
# server-side/socket-level bound instead (Postgres `statement_timeout`,
|
|
11
|
-
# libpq `tcp_user_timeout`/`keepalives`, an explicit `IO`/socket timeout).
|
|
12
|
-
#
|
|
13
|
-
# There is no autocorrect on purpose: the safe replacement depends on what
|
|
14
|
-
# is being bounded, so a human must choose it.
|
|
15
|
-
#
|
|
16
|
-
# @example
|
|
17
|
-
# # bad
|
|
18
|
-
# Timeout.timeout(5) { do_work }
|
|
19
|
-
#
|
|
20
|
-
# # good — bound the actual resource (DB query, socket) directly
|
|
21
|
-
# conn.exec("SET statement_timeout = 5000")
|
|
22
|
-
# do_work
|
|
23
|
-
class NoRubyTimeout < Base
|
|
24
|
-
MSG = "Please be careful, Timeout is dangerous. Timeout.timeout uses " \
|
|
25
|
-
"Thread#raise and can corrupt a pooled connection mid-call. Bound the " \
|
|
26
|
-
"resource itself instead (statement_timeout / tcp_user_timeout / socket timeout)."
|
|
27
|
-
|
|
28
|
-
# Matches `Timeout.timeout(...)` and `::Timeout.timeout(...)`.
|
|
29
|
-
# @!method ruby_timeout?(node)
|
|
30
|
-
def_node_matcher :ruby_timeout?, <<~PATTERN
|
|
31
|
-
(send (const {nil? cbase} :Timeout) :timeout ...)
|
|
32
|
-
PATTERN
|
|
33
|
-
|
|
34
|
-
def on_send(node)
|
|
35
|
-
return unless ruby_timeout?(node)
|
|
36
|
-
|
|
37
|
-
add_offense(node.loc.selector)
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|