pgmq-ruby 0.6.1 → 0.7.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 +105 -1
- data/README.md +299 -19
- data/lib/pgmq/client/autovacuum.rb +180 -0
- data/lib/pgmq/client/consumer.rb +121 -7
- data/lib/pgmq/client/maintenance.rb +154 -5
- data/lib/pgmq/client/message_lifecycle.rb +6 -7
- data/lib/pgmq/client/metrics.rb +1 -2
- data/lib/pgmq/client/multi_queue.rb +9 -11
- data/lib/pgmq/client/producer.rb +37 -12
- data/lib/pgmq/client/queue_management.rb +87 -5
- data/lib/pgmq/client/topics.rb +22 -12
- data/lib/pgmq/client.rb +49 -34
- data/lib/pgmq/connection.rb +139 -33
- data/lib/pgmq/message.rb +4 -5
- data/lib/pgmq/notify_throttle.rb +25 -0
- data/lib/pgmq/queue_name.rb +153 -0
- data/lib/pgmq/transaction.rb +6 -7
- data/lib/pgmq/version.rb +1 -1
- data/lib/pgmq.rb +2 -3
- metadata +5 -2
data/lib/pgmq/connection.rb
CHANGED
|
@@ -26,6 +26,92 @@ module PGMQ
|
|
|
26
26
|
# Default connection pool timeout in seconds
|
|
27
27
|
DEFAULT_POOL_TIMEOUT = 5
|
|
28
28
|
|
|
29
|
+
class << self
|
|
30
|
+
# Additional error message patterns (String or Regexp) that mean the connection is dead and a retry on a fresh
|
|
31
|
+
# socket is safe. Strings are matched as case-insensitive substrings; Regexps match the original message. The
|
|
32
|
+
# built-in LOST_CONNECTION_MESSAGES are always checked first - this list is appended to them.
|
|
33
|
+
#
|
|
34
|
+
# Thread-safe: reads are lock-free (frozen array swap); writes should be done at boot time before forking workers.
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<String, Regexp>]
|
|
37
|
+
# @example
|
|
38
|
+
# PGMQ::Connection.reconnectable_error_patterns << "connection reset by peer"
|
|
39
|
+
# PGMQ::Connection.reconnectable_error_patterns << /\Abroken pipe\b/i
|
|
40
|
+
attr_reader :reconnectable_error_patterns
|
|
41
|
+
|
|
42
|
+
# Additional exception classes that mean the connection is dead. `PG::ConnectionBad` and `PG::UnableToSend` are
|
|
43
|
+
# always matched - this list is appended to them. Subclasses also match.
|
|
44
|
+
#
|
|
45
|
+
# Thread-safe: reads are lock-free; writes should be done at boot time.
|
|
46
|
+
#
|
|
47
|
+
# @return [Array<Class>]
|
|
48
|
+
# @example
|
|
49
|
+
# PGMQ::Connection.reconnectable_error_classes << PG::ConnectionRefused
|
|
50
|
+
attr_reader :reconnectable_error_classes
|
|
51
|
+
|
|
52
|
+
# Replaces the extra reconnectable error patterns.
|
|
53
|
+
#
|
|
54
|
+
# @param patterns [Array<String, Regexp>]
|
|
55
|
+
# @raise [PGMQ::Errors::ConfigurationError] if any element is invalid
|
|
56
|
+
def reconnectable_error_patterns=(patterns)
|
|
57
|
+
@reconnectable_error_patterns = normalize_patterns(patterns)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Replaces the extra reconnectable error classes.
|
|
61
|
+
#
|
|
62
|
+
# @param classes [Array<Class>]
|
|
63
|
+
# @raise [PGMQ::Errors::ConfigurationError] if any element is invalid
|
|
64
|
+
def reconnectable_error_classes=(classes)
|
|
65
|
+
@reconnectable_error_classes = normalize_classes(classes)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Normalizes user-supplied reconnectable error patterns.
|
|
71
|
+
#
|
|
72
|
+
# Strings are downcased once at configuration time so the hot path (`connection_lost_error?`) only does substring
|
|
73
|
+
# checks. Regexps are passed through unchanged.
|
|
74
|
+
#
|
|
75
|
+
# @param patterns [Array<String, Regexp>, String, Regexp, nil]
|
|
76
|
+
# @return [Array<String, Regexp>]
|
|
77
|
+
def normalize_patterns(patterns)
|
|
78
|
+
Array(patterns).map do |pattern|
|
|
79
|
+
case pattern
|
|
80
|
+
when Regexp
|
|
81
|
+
pattern
|
|
82
|
+
when String
|
|
83
|
+
pattern.downcase
|
|
84
|
+
else
|
|
85
|
+
raise(
|
|
86
|
+
PGMQ::Errors::ConfigurationError,
|
|
87
|
+
"reconnectable_error_patterns must contain Strings or Regexps, got #{pattern.class}"
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end.freeze
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Normalizes user-supplied reconnectable error classes.
|
|
94
|
+
#
|
|
95
|
+
# @param classes [Array<Class>, Class, nil]
|
|
96
|
+
# @return [Array<Class>]
|
|
97
|
+
def normalize_classes(classes)
|
|
98
|
+
Array(classes).map do |klass|
|
|
99
|
+
unless klass.is_a?(Class) && klass <= Exception
|
|
100
|
+
raise(
|
|
101
|
+
PGMQ::Errors::ConfigurationError,
|
|
102
|
+
"reconnectable_error_classes must contain Exception subclasses, got #{klass.inspect}"
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
klass
|
|
107
|
+
end.freeze
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Initialize class-level defaults (empty arrays, users append at boot)
|
|
112
|
+
@reconnectable_error_patterns = [].freeze
|
|
113
|
+
@reconnectable_error_classes = [].freeze
|
|
114
|
+
|
|
29
115
|
# @return [ConnectionPool] the connection pool
|
|
30
116
|
attr_reader :pool
|
|
31
117
|
|
|
@@ -107,37 +193,58 @@ module PGMQ
|
|
|
107
193
|
|
|
108
194
|
private
|
|
109
195
|
|
|
110
|
-
#
|
|
196
|
+
# Messages libpq raises when the server/pooler has already torn down the socket. The list has grown organically with
|
|
197
|
+
# each pooler/TLS variant we see in the wild; the class check below catches future variants that libpq raises as
|
|
198
|
+
# `PG::ConnectionBad` or `PG::UnableToSend` without waiting for a new message to hit production.
|
|
199
|
+
LOST_CONNECTION_MESSAGES = [
|
|
200
|
+
"server closed the connection",
|
|
201
|
+
"connection not open",
|
|
202
|
+
"connection is closed",
|
|
203
|
+
"connection has been closed",
|
|
204
|
+
"no connection to the server",
|
|
205
|
+
"terminating connection",
|
|
206
|
+
"connection to server was lost",
|
|
207
|
+
"could not receive data from server",
|
|
208
|
+
"pqsocket() can't get socket descriptor",
|
|
209
|
+
"ssl error: unexpected eof",
|
|
210
|
+
"ssl syscall error"
|
|
211
|
+
].freeze
|
|
212
|
+
private_constant :LOST_CONNECTION_MESSAGES
|
|
213
|
+
|
|
214
|
+
# Checks if the error indicates a lost connection.
|
|
215
|
+
#
|
|
216
|
+
# Matches in three steps: first by class (`PG::ConnectionBad` / `PG::UnableToSend` are dedicated connection-failure
|
|
217
|
+
# classes libpq raises regardless of message, plus any user-supplied classes), then by built-in message substrings
|
|
218
|
+
# for the bare `PG::Error` cases where libpq doesn't reach for the specific subclass, and finally by user-supplied
|
|
219
|
+
# patterns (strings matched as case-insensitive substrings, Regexps matched against the original message).
|
|
220
|
+
#
|
|
111
221
|
# @param error [PG::Error] the error to check
|
|
112
|
-
# @return [Boolean] true if connection was lost
|
|
222
|
+
# @return [Boolean] true if the connection was lost and a retry on a
|
|
223
|
+
# fresh connection is appropriate
|
|
113
224
|
def connection_lost_error?(error)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
message = error.message.to_s.downcase
|
|
131
|
-
lost_connection_messages.any? { |pattern| message.include?(pattern) }
|
|
225
|
+
return true if error.is_a?(PG::ConnectionBad) || error.is_a?(PG::UnableToSend)
|
|
226
|
+
|
|
227
|
+
extra_classes = self.class.reconnectable_error_classes
|
|
228
|
+
return true if extra_classes.any? { |klass| error.is_a?(klass) }
|
|
229
|
+
|
|
230
|
+
original_message = error.message.to_s
|
|
231
|
+
downcased = original_message.downcase
|
|
232
|
+
|
|
233
|
+
return true if LOST_CONNECTION_MESSAGES.any? { |pattern| downcased.include?(pattern) }
|
|
234
|
+
|
|
235
|
+
self.class.reconnectable_error_patterns.any? do |pattern|
|
|
236
|
+
case pattern
|
|
237
|
+
when Regexp then pattern.match?(original_message)
|
|
238
|
+
else downcased.include?(pattern)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
132
241
|
end
|
|
133
242
|
|
|
134
243
|
# Verifies a connection is alive and working.
|
|
135
244
|
#
|
|
136
|
-
# Also resets when the connection reports `PG::CONNECTION_BAD`, which
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
# still exists. `#finished?` alone only catches connections closed
|
|
140
|
-
# explicitly from the client side.
|
|
245
|
+
# Also resets when the connection reports `PG::CONNECTION_BAD`, which happens when the server (or an intermediate
|
|
246
|
+
# pooler such as PgBouncer) has closed the socket while the client-side `PG::Connection` object still exists.
|
|
247
|
+
# `#finished?` alone only catches connections closed explicitly from the client side.
|
|
141
248
|
#
|
|
142
249
|
# @param conn [PG::Connection] connection to verify
|
|
143
250
|
# @raise [PG::Error] if the reset itself fails
|
|
@@ -187,16 +294,15 @@ module PGMQ
|
|
|
187
294
|
ConnectionPool.new(size: @pool_size, timeout: @pool_timeout) do
|
|
188
295
|
conn = create_connection(params)
|
|
189
296
|
|
|
190
|
-
# Detect shared connections: if a callable returns the same PG::Connection
|
|
191
|
-
#
|
|
192
|
-
# (nil results, segfaults, wrong data). Fail fast with a clear message.
|
|
297
|
+
# Detect shared connections: if a callable returns the same PG::Connection object to multiple pool slots,
|
|
298
|
+
# concurrent use will corrupt libpq state (nil results, segfaults, wrong data). Fail fast with a clear message.
|
|
193
299
|
if conn.is_a?(PG::Connection)
|
|
194
300
|
seen_mutex.synchronize do
|
|
195
301
|
if seen_connections.key?(conn)
|
|
196
302
|
raise PGMQ::Errors::ConfigurationError,
|
|
197
303
|
"Connection callable returned the same PG::Connection object " \
|
|
198
304
|
"(object_id: #{conn.object_id}) to multiple pool slots. " \
|
|
199
|
-
"PG::Connection is NOT thread-safe
|
|
305
|
+
"PG::Connection is NOT thread-safe - concurrent use causes nil results, " \
|
|
200
306
|
"segfaults, and data corruption. Ensure your callable returns a unique " \
|
|
201
307
|
"PG::Connection instance on each invocation (for example, by calling " \
|
|
202
308
|
"PG.connect inside the callable)."
|
|
@@ -219,10 +325,10 @@ module PGMQ
|
|
|
219
325
|
# If we have a callable (e.g., for Rails), call it to get the connection
|
|
220
326
|
return params.call if params.respond_to?(:call)
|
|
221
327
|
|
|
222
|
-
# Create new connection from parameters
|
|
223
|
-
# Low-level library: return all values as strings from PostgreSQL
|
|
224
|
-
# No automatic type conversion - let higher-level frameworks handle parsing
|
|
225
|
-
# conn.type_map_for_results intentionally NOT set
|
|
328
|
+
# Create new connection from parameters.
|
|
329
|
+
# Low-level library: return all values as strings from PostgreSQL.
|
|
330
|
+
# No automatic type conversion - let higher-level frameworks handle parsing.
|
|
331
|
+
# conn.type_map_for_results intentionally NOT set.
|
|
226
332
|
PG.connect(params[:conninfo] || params)
|
|
227
333
|
rescue PG::Error => e
|
|
228
334
|
raise PGMQ::Errors::ConnectionError, "Failed to connect to database: #{e.message}"
|
data/lib/pgmq/message.rb
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module PGMQ
|
|
4
4
|
# Represents a message read from a PGMQ queue
|
|
5
5
|
#
|
|
6
|
-
# Returns raw values from PostgreSQL without transformation.
|
|
7
|
-
#
|
|
6
|
+
# Returns raw values from PostgreSQL without transformation. Higher-level frameworks should handle parsing,
|
|
7
|
+
# deserialization, etc.
|
|
8
8
|
#
|
|
9
9
|
# @example Reading a message (raw values)
|
|
10
10
|
# msg = client.read("my_queue", vt: 30)
|
|
@@ -24,9 +24,8 @@ module PGMQ
|
|
|
24
24
|
# @param row [Hash] database row from PG result
|
|
25
25
|
# @return [Message]
|
|
26
26
|
def new(row, **)
|
|
27
|
-
# Return raw values as-is from PostgreSQL
|
|
28
|
-
#
|
|
29
|
-
# The pg gem returns JSONB as String by default
|
|
27
|
+
# Return raw values as-is from PostgreSQL. No parsing, no deserialization, no transformation.
|
|
28
|
+
# The pg gem returns JSONB as String by default.
|
|
30
29
|
super(
|
|
31
30
|
msg_id: row["msg_id"],
|
|
32
31
|
read_ct: row["read_ct"],
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGMQ
|
|
4
|
+
# Represents the NOTIFY throttle configuration for a queue
|
|
5
|
+
#
|
|
6
|
+
# @example Inspecting notification configuration
|
|
7
|
+
# throttles = client.list_notify_insert_throttles
|
|
8
|
+
# throttles.each do |t|
|
|
9
|
+
# puts "#{t.queue_name}: #{t.throttle_interval_ms}ms (last notified: #{t.last_notified_at})"
|
|
10
|
+
# end
|
|
11
|
+
class NotifyThrottle < Data.define(:queue_name, :throttle_interval_ms, :last_notified_at)
|
|
12
|
+
class << self
|
|
13
|
+
# Creates a new NotifyThrottle from a database row
|
|
14
|
+
# @param row [Hash] database row from PG result
|
|
15
|
+
# @return [NotifyThrottle]
|
|
16
|
+
def new(row, **)
|
|
17
|
+
super(
|
|
18
|
+
queue_name: row["queue_name"],
|
|
19
|
+
throttle_interval_ms: row["throttle_interval_ms"],
|
|
20
|
+
last_notified_at: row["last_notified_at"]
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGMQ
|
|
4
|
+
# Queue name validation, normalization, and sanitization.
|
|
5
|
+
#
|
|
6
|
+
# PGMQ interpolates a queue's name into PostgreSQL identifiers (it creates tables named +pgmq.q_<name>+ and
|
|
7
|
+
# +pgmq.a_<name>+), so a name has to be a valid, length-bounded SQL identifier. This module is the single source of
|
|
8
|
+
# truth for those rules and offers tiers depending on how much you trust the input:
|
|
9
|
+
#
|
|
10
|
+
# 1. {.validate!} / {.valid?} - assert a name is already valid. Use for names you control. {PGMQ::Client} calls
|
|
11
|
+
# {.validate!} before every operation.
|
|
12
|
+
# 2. {.normalize} - lightly rewrite a name that is *meant* to be valid but uses a friendlier separator. Maps the
|
|
13
|
+
# common stream separators (hyphens, dots, colons) to underscores, strips any other invalid character, then
|
|
14
|
+
# validates - so a Turbo Stream channel like +"chat:room-7"+ becomes +"chat_room_7"+. Raises if the result still
|
|
15
|
+
# can't be a valid name (empty, or starts with a digit).
|
|
16
|
+
# 3. {.sanitize!} - coerce *untrusted* input into a valid name by stripping every invalid character, then validate.
|
|
17
|
+
# Raises if nothing valid remains. Use this as a SQL-identifier guard: the result is always either a name you
|
|
18
|
+
# know is safe, or an exception - never a silent substitute.
|
|
19
|
+
# 4. {.sanitize} - the lenient sibling of {.sanitize!}: best-effort coercion that *never* raises for content and
|
|
20
|
+
# always returns a usable identifier (falling back to a default). Convenient, but see the collision caveat on
|
|
21
|
+
# the method - distinct inputs can map to the same name.
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# PGMQ::QueueName.valid?("orders") # => true
|
|
25
|
+
# PGMQ::QueueName.validate!("my-queue") # => raises InvalidQueueNameError
|
|
26
|
+
# PGMQ::QueueName.normalize("chat:room-7") # => "chat_room_7"
|
|
27
|
+
# PGMQ::QueueName.sanitize!("orders!!") # => "orders"
|
|
28
|
+
# PGMQ::QueueName.sanitize!("!!!") # => raises InvalidQueueNameError
|
|
29
|
+
# PGMQ::QueueName.sanitize("99 Problems!") # => "q_99_problems"
|
|
30
|
+
module QueueName
|
|
31
|
+
# Maximum queue name length. PGMQ creates tables with prefixes (+q_+, +a_+) and PostgreSQL caps identifiers at 63
|
|
32
|
+
# characters; PGMQ enforces 48 to leave room for those prefixes and suffixes.
|
|
33
|
+
MAX_LENGTH = 48
|
|
34
|
+
|
|
35
|
+
# A valid queue name: starts with a letter or underscore, then letters, digits, or underscores.
|
|
36
|
+
PATTERN = /\A[a-zA-Z_][a-zA-Z0-9_]*\z/
|
|
37
|
+
|
|
38
|
+
# Prefix used by {.sanitize} when the input would otherwise start with an illegal leading character (e.g. a
|
|
39
|
+
# digit) but still has usable trailing characters.
|
|
40
|
+
SANITIZE_PREFIX = "q_"
|
|
41
|
+
|
|
42
|
+
# Name {.sanitize} falls back to when the input has no usable characters at all.
|
|
43
|
+
SANITIZE_FALLBACK = "queue"
|
|
44
|
+
|
|
45
|
+
module_function
|
|
46
|
+
|
|
47
|
+
# Returns true if the name is already a valid queue name.
|
|
48
|
+
#
|
|
49
|
+
# @param name [String, #to_s] candidate queue name
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
def valid?(name)
|
|
52
|
+
str = name.to_s
|
|
53
|
+
!str.empty? && str.length < MAX_LENGTH && str.match?(PATTERN)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Validates a queue name, returning it unchanged when valid and raising otherwise.
|
|
57
|
+
#
|
|
58
|
+
# @param name [String, #to_s] candidate queue name
|
|
59
|
+
# @return [String] the validated name (as a String)
|
|
60
|
+
# @raise [PGMQ::Errors::InvalidQueueNameError] if the name is empty, too long, or not a valid identifier
|
|
61
|
+
def validate!(name)
|
|
62
|
+
str = name.to_s
|
|
63
|
+
|
|
64
|
+
if name.nil? || str.strip.empty?
|
|
65
|
+
raise Errors::InvalidQueueNameError, "Queue name cannot be empty"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if str.length >= MAX_LENGTH
|
|
69
|
+
raise Errors::InvalidQueueNameError,
|
|
70
|
+
"Queue name '#{str}' exceeds maximum length of #{MAX_LENGTH} characters " \
|
|
71
|
+
"(current length: #{str.length})"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
return str if str.match?(PATTERN)
|
|
75
|
+
|
|
76
|
+
raise Errors::InvalidQueueNameError,
|
|
77
|
+
"Invalid queue name '#{str}': must start with a letter or underscore " \
|
|
78
|
+
"and contain only letters, digits, and underscores"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Rewrites a name that is meant to be valid but uses friendlier separators, then validates it.
|
|
82
|
+
#
|
|
83
|
+
# Maps the common stream-name separators - hyphens, dots, and colons - to underscores, strips any *other* invalid
|
|
84
|
+
# character, then collapses repeated underscores and trims them from the ends. So +"chat:room-7"+ becomes
|
|
85
|
+
# +"chat_room_7"+ and +"order.events"+ becomes +"order_events"+, while a stray +"a@b"+ becomes +"ab"+ (the +@+ is
|
|
86
|
+
# dropped, not turned into a separator). The result is validated, so names that still can't be valid (empty, or
|
|
87
|
+
# starting with a digit) raise rather than being silently mangled.
|
|
88
|
+
#
|
|
89
|
+
# Colons in particular are the turbo-rails stream-name separator, so they are mapped to a safe character rather
|
|
90
|
+
# than stripped - otherwise +"a:b"+ and +"ab"+ would collide on the same queue.
|
|
91
|
+
#
|
|
92
|
+
# @param name [String, #to_s] a name using friendly separators
|
|
93
|
+
# @return [String] the normalized, validated queue name
|
|
94
|
+
# @raise [PGMQ::Errors::InvalidQueueNameError] if the normalized result is not a valid queue name
|
|
95
|
+
def normalize(name)
|
|
96
|
+
str = name.to_s
|
|
97
|
+
return validate!(str) if str.match?(PATTERN)
|
|
98
|
+
|
|
99
|
+
normalized = str
|
|
100
|
+
.gsub(/[-.:]/, "_") # hyphens / dots / colons -> underscores
|
|
101
|
+
.gsub(/[^a-zA-Z0-9_]/, "") # strip any remaining invalid character
|
|
102
|
+
.squeeze("_") # collapse consecutive underscores
|
|
103
|
+
.gsub(/\A_+|_+\z/, "") # trim leading / trailing underscores
|
|
104
|
+
|
|
105
|
+
validate!(normalized)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Strips every invalid character from untrusted input, then validates the result.
|
|
109
|
+
#
|
|
110
|
+
# This is the SQL-identifier guard: it removes anything outside +[A-Za-z0-9_]+ and passes the remainder through
|
|
111
|
+
# {.validate!}. If nothing valid remains (empty result, leading digit, too long) it raises, so a caller can never
|
|
112
|
+
# accidentally operate on a different queue than intended. Use this for names from untrusted sources (URL params,
|
|
113
|
+
# external systems) where a wrong-but-valid name would be worse than an error.
|
|
114
|
+
#
|
|
115
|
+
# @param name [String, #to_s] untrusted input
|
|
116
|
+
# @return [String] the sanitized, validated queue name
|
|
117
|
+
# @raise [PGMQ::Errors::InvalidQueueNameError] if nothing valid remains after stripping
|
|
118
|
+
def sanitize!(name)
|
|
119
|
+
validate!(name.to_s.gsub(/[^a-zA-Z0-9_]/, ""))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Best-effort coercion of arbitrary input into a valid queue name; the lenient sibling of {.sanitize!}.
|
|
123
|
+
#
|
|
124
|
+
# Never raises for content: it lowercases, replaces every illegal character run with an underscore, trims
|
|
125
|
+
# underscores, prefixes {SANITIZE_PREFIX} when the first surviving character is not a valid identifier start (e.g.
|
|
126
|
+
# a digit), truncates to fit {MAX_LENGTH}, and falls back to {SANITIZE_FALLBACK} when nothing usable remains. The
|
|
127
|
+
# return value always satisfies {.valid?}.
|
|
128
|
+
#
|
|
129
|
+
# @note Because it coerces rather than rejects, distinct inputs can map to the *same* name (e.g. +"a/b"+ and
|
|
130
|
+
# +"a-b"+ both become +"a_b"+; +"!!!"+ and +""+ both become +"queue"+). If your name selects a queue table,
|
|
131
|
+
# that means two logically different inputs could share one queue. When that matters - especially for untrusted
|
|
132
|
+
# input - prefer {.sanitize!}, which raises instead of substituting.
|
|
133
|
+
#
|
|
134
|
+
# @param name [String, #to_s] arbitrary input
|
|
135
|
+
# @return [String] a guaranteed-valid queue name
|
|
136
|
+
def sanitize(name)
|
|
137
|
+
cleaned = name.to_s.downcase
|
|
138
|
+
.gsub(/[^a-z0-9_]+/, "_").squeeze("_")
|
|
139
|
+
.gsub(/\A_+|_+\z/, "")
|
|
140
|
+
|
|
141
|
+
# Nothing usable survived the scrub - use the fallback rather than emit a bare prefix like "q_".
|
|
142
|
+
return SANITIZE_FALLBACK if cleaned.empty?
|
|
143
|
+
|
|
144
|
+
# A valid identifier can't start with a digit; prefix so the leading character is legal.
|
|
145
|
+
cleaned = "#{SANITIZE_PREFIX}#{cleaned}" unless cleaned.match?(/\A[a-z_]/)
|
|
146
|
+
|
|
147
|
+
# Keep under MAX_LENGTH (valid? requires strictly less than), trimming any underscore left at the cut.
|
|
148
|
+
cleaned = cleaned[0, MAX_LENGTH - 1].sub(/_+\z/, "") if cleaned.length >= MAX_LENGTH
|
|
149
|
+
|
|
150
|
+
cleaned.empty? ? SANITIZE_FALLBACK : cleaned
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
data/lib/pgmq/transaction.rb
CHANGED
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
module PGMQ
|
|
4
4
|
# Low-level transaction support for PGMQ operations
|
|
5
5
|
#
|
|
6
|
-
# Provides atomic execution of PGMQ operations within PostgreSQL transactions.
|
|
7
|
-
#
|
|
8
|
-
# PostgreSQL's native transaction support.
|
|
6
|
+
# Provides atomic execution of PGMQ operations within PostgreSQL transactions. Transactions are a database primitive -
|
|
7
|
+
# this is a thin wrapper around PostgreSQL's native transaction support.
|
|
9
8
|
#
|
|
10
|
-
# This is analogous to rdkafka-ruby providing Kafka transaction support -
|
|
11
|
-
#
|
|
9
|
+
# This is analogous to rdkafka-ruby providing Kafka transaction support - it's a protocol/database feature, not a
|
|
10
|
+
# framework abstraction.
|
|
12
11
|
#
|
|
13
12
|
# @example Atomic multi-queue operations
|
|
14
13
|
# client.transaction do |txn|
|
|
@@ -24,8 +23,8 @@ module PGMQ
|
|
|
24
23
|
module Transaction
|
|
25
24
|
# Executes PGMQ operations atomically within a database transaction
|
|
26
25
|
#
|
|
27
|
-
# Obtains a connection from the pool, starts a transaction, and yields
|
|
28
|
-
#
|
|
26
|
+
# Obtains a connection from the pool, starts a transaction, and yields a client that uses that transaction for all
|
|
27
|
+
# operations.
|
|
29
28
|
#
|
|
30
29
|
# @yield [PGMQ::Client] transactional client using the same connection
|
|
31
30
|
# @return [Object] result of the block
|
data/lib/pgmq/version.rb
CHANGED
data/lib/pgmq.rb
CHANGED
|
@@ -12,9 +12,8 @@ loader.eager_load
|
|
|
12
12
|
|
|
13
13
|
# PGMQ - Low-level Ruby client for Postgres Message Queue
|
|
14
14
|
#
|
|
15
|
-
# This is a low-level library providing direct access to PGMQ operations.
|
|
16
|
-
#
|
|
17
|
-
# see pgmq-framework (similar to how rdkafka-ruby relates to Karafka).
|
|
15
|
+
# This is a low-level library providing direct access to PGMQ operations. For higher-level abstractions, job processing,
|
|
16
|
+
# and framework integrations, see pgmq-framework (similar to how rdkafka-ruby relates to Karafka).
|
|
18
17
|
#
|
|
19
18
|
# @example Basic usage
|
|
20
19
|
# require 'pgmq'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgmq-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maciej Mensfeld
|
|
@@ -64,6 +64,7 @@ files:
|
|
|
64
64
|
- README.md
|
|
65
65
|
- lib/pgmq.rb
|
|
66
66
|
- lib/pgmq/client.rb
|
|
67
|
+
- lib/pgmq/client/autovacuum.rb
|
|
67
68
|
- lib/pgmq/client/consumer.rb
|
|
68
69
|
- lib/pgmq/client/maintenance.rb
|
|
69
70
|
- lib/pgmq/client/message_lifecycle.rb
|
|
@@ -76,7 +77,9 @@ files:
|
|
|
76
77
|
- lib/pgmq/errors.rb
|
|
77
78
|
- lib/pgmq/message.rb
|
|
78
79
|
- lib/pgmq/metrics.rb
|
|
80
|
+
- lib/pgmq/notify_throttle.rb
|
|
79
81
|
- lib/pgmq/queue_metadata.rb
|
|
82
|
+
- lib/pgmq/queue_name.rb
|
|
80
83
|
- lib/pgmq/transaction.rb
|
|
81
84
|
- lib/pgmq/version.rb
|
|
82
85
|
- pgmq-ruby.gemspec
|
|
@@ -104,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
104
107
|
- !ruby/object:Gem::Version
|
|
105
108
|
version: '0'
|
|
106
109
|
requirements: []
|
|
107
|
-
rubygems_version: 4.0.
|
|
110
|
+
rubygems_version: 4.0.10
|
|
108
111
|
specification_version: 4
|
|
109
112
|
summary: Ruby client for PGMQ (Postgres Message Queue)
|
|
110
113
|
test_files: []
|