pgmq-ruby 0.6.2 → 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 +97 -2
- data/README.md +266 -20
- 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 +25 -38
- 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
|
@@ -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: []
|