pgmq-ruby 0.7.1 → 0.8.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 -0
- data/README.md +109 -6
- data/config/locales/errors.yml +46 -0
- data/lib/pgmq/client/autovacuum.rb +22 -33
- data/lib/pgmq/client/consumer.rb +3 -4
- data/lib/pgmq/client/maintenance.rb +11 -7
- data/lib/pgmq/client/message_lifecycle.rb +8 -5
- data/lib/pgmq/client/multi_queue.rb +13 -13
- data/lib/pgmq/client/producer.rb +32 -41
- data/lib/pgmq/client/queue_management.rb +25 -5
- data/lib/pgmq/client/topics.rb +23 -28
- data/lib/pgmq/client.rb +37 -6
- data/lib/pgmq/connection.rb +28 -55
- data/lib/pgmq/consumer.rb +222 -0
- data/lib/pgmq/contractable/contract.rb +193 -0
- data/lib/pgmq/contractable/result.rb +89 -0
- data/lib/pgmq/contractable/rule.rb +8 -0
- data/lib/pgmq/contractable.rb +12 -0
- data/lib/pgmq/contracts/autovacuum.rb +46 -0
- data/lib/pgmq/contracts/batch.rb +21 -0
- data/lib/pgmq/contracts/connection.rb +27 -0
- data/lib/pgmq/contracts/consumer.rb +45 -0
- data/lib/pgmq/contracts/message.rb +16 -0
- data/lib/pgmq/contracts/multi_queue.rb +28 -0
- data/lib/pgmq/contracts.rb +16 -0
- data/lib/pgmq/errors.rb +27 -1
- data/lib/pgmq/metrics.rb +11 -2
- data/lib/pgmq/queue_name.rb +2 -3
- data/lib/pgmq/transaction.rb +50 -3
- data/lib/pgmq/version.rb +1 -1
- data/lib/pgmq.rb +8 -1
- data/pgmq-ruby.gemspec +2 -2
- metadata +23 -4
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGMQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates the arguments of operations that span several queues
|
|
6
|
+
#
|
|
7
|
+
# Each key is optional, so one contract serves every multi-queue operation. Queue names themselves are validated
|
|
8
|
+
# by {PGMQ::QueueName}.
|
|
9
|
+
class MultiQueue < Contractable::Contract
|
|
10
|
+
# Maximum number of queues in one multi-queue operation
|
|
11
|
+
MAX_QUEUES = 50
|
|
12
|
+
|
|
13
|
+
self.error_messages = Contracts.error_messages("multi_queue")
|
|
14
|
+
|
|
15
|
+
optional(:queue_names) do |queue_names|
|
|
16
|
+
next :format unless queue_names.is_a?(Array)
|
|
17
|
+
next :empty if queue_names.empty?
|
|
18
|
+
next :too_many if queue_names.size > MAX_QUEUES
|
|
19
|
+
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
%i[deletions archives updates].each do |key|
|
|
24
|
+
optional(key) { |value| value.is_a?(Hash) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGMQ
|
|
4
|
+
# Contracts that validate user input before it reaches PostgreSQL
|
|
5
|
+
module Contracts
|
|
6
|
+
class << self
|
|
7
|
+
# @param scope [String] contract key under `en.validations` in `config/locales/errors.yml`
|
|
8
|
+
# @return [Hash{String => String}] error messages of one contract
|
|
9
|
+
def error_messages(scope)
|
|
10
|
+
@error_messages ||= YAML.safe_load_file(File.join(PGMQ.gem_root, "config", "locales", "errors.yml"))
|
|
11
|
+
|
|
12
|
+
@error_messages.fetch("en").fetch("validations").fetch(scope)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/pgmq/errors.rb
CHANGED
|
@@ -6,25 +6,51 @@ module PGMQ
|
|
|
6
6
|
# Base error class for all PGMQ errors
|
|
7
7
|
class BaseError < StandardError; end
|
|
8
8
|
|
|
9
|
-
# Raised when connection to PostgreSQL fails or is lost
|
|
9
|
+
# Raised when a database call fails, not only when the connection to PostgreSQL fails or is lost.
|
|
10
|
+
# {PGMQ::Connection#with_connection} wraps every `PG::Error` (for example a missing queue, invalid JSON or an
|
|
11
|
+
# invalid integer) in this class, with the message "Database connection error: ...". It also raises it for
|
|
12
|
+
# connection pool timeouts and a closed pool. Outside a transaction, the original error is in `#cause`.
|
|
13
|
+
# {PGMQ::Client#transaction} raises it when the transaction fails. Then `#cause` is the error raised in the block;
|
|
14
|
+
# for a database error that is the inner {ConnectionError}, with the `PG::Error` in its own `#cause`.
|
|
10
15
|
class ConnectionError < BaseError; end
|
|
11
16
|
|
|
12
17
|
# Raised when a queue operation is attempted on a non-existent queue
|
|
18
|
+
#
|
|
19
|
+
# @note Not raised by PGMQ-Ruby. A database error for a missing queue raises {ConnectionError}.
|
|
13
20
|
class QueueNotFoundError < BaseError; end
|
|
14
21
|
|
|
15
22
|
# Raised when a message cannot be found
|
|
23
|
+
#
|
|
24
|
+
# @note Not raised by PGMQ-Ruby.
|
|
16
25
|
class MessageNotFoundError < BaseError; end
|
|
17
26
|
|
|
18
27
|
# Raised when message serialization fails
|
|
28
|
+
#
|
|
29
|
+
# @note Not raised by PGMQ-Ruby.
|
|
19
30
|
class SerializationError < BaseError; end
|
|
20
31
|
|
|
21
32
|
# Raised when message deserialization fails
|
|
33
|
+
#
|
|
34
|
+
# @note Not raised by PGMQ-Ruby.
|
|
22
35
|
class DeserializationError < BaseError; end
|
|
23
36
|
|
|
24
37
|
# Raised when configuration is invalid
|
|
25
38
|
class ConfigurationError < BaseError; end
|
|
26
39
|
|
|
40
|
+
# Raised when a message, headers or a batch to produce do not match {PGMQ::Contracts::Message} or
|
|
41
|
+
# {PGMQ::Contracts::Batch}
|
|
42
|
+
class MessageInvalidError < BaseError; end
|
|
43
|
+
|
|
44
|
+
# Raised when the arguments of a multi-queue operation do not match {PGMQ::Contracts::MultiQueue}
|
|
45
|
+
class MultiQueueInvalidError < BaseError; end
|
|
46
|
+
|
|
47
|
+
# Raised when autovacuum settings do not match {PGMQ::Contracts::Autovacuum}
|
|
48
|
+
class AutovacuumSettingsInvalidError < BaseError; end
|
|
49
|
+
|
|
27
50
|
# Raised when an invalid queue name is provided
|
|
28
51
|
class InvalidQueueNameError < BaseError; end
|
|
52
|
+
|
|
53
|
+
# Raised when a transactional client is used outside its transaction (another thread/fiber, or after the block)
|
|
54
|
+
class TransactionError < BaseError; end
|
|
29
55
|
end
|
|
30
56
|
end
|
data/lib/pgmq/metrics.rb
CHANGED
|
@@ -5,6 +5,11 @@ require "time"
|
|
|
5
5
|
module PGMQ
|
|
6
6
|
# Represents metrics for a PGMQ queue
|
|
7
7
|
#
|
|
8
|
+
# `queue_visible_length` (messages whose visibility timeout has passed) needs PGMQ v1.5.0+.
|
|
9
|
+
# `default_partition_length` (estimated rows in the default partitions of a partitioned queue
|
|
10
|
+
# and its archive) needs PGMQ v1.13.0+ and is `nil` for non-partitioned queues. A non-zero value
|
|
11
|
+
# means pg_partman maintenance is failing. On older PGMQ versions both are `nil`.
|
|
12
|
+
#
|
|
8
13
|
# @example Getting queue metrics
|
|
9
14
|
# metrics = client.metrics("my_queue")
|
|
10
15
|
# puts metrics.queue_length # => 42
|
|
@@ -15,7 +20,9 @@ module PGMQ
|
|
|
15
20
|
:newest_msg_age_sec,
|
|
16
21
|
:oldest_msg_age_sec,
|
|
17
22
|
:total_messages,
|
|
18
|
-
:scrape_time
|
|
23
|
+
:scrape_time,
|
|
24
|
+
:queue_visible_length,
|
|
25
|
+
:default_partition_length
|
|
19
26
|
)
|
|
20
27
|
class << self
|
|
21
28
|
# Creates a new Metrics object from a database row
|
|
@@ -29,7 +36,9 @@ module PGMQ
|
|
|
29
36
|
newest_msg_age_sec: row["newest_msg_age_sec"],
|
|
30
37
|
oldest_msg_age_sec: row["oldest_msg_age_sec"],
|
|
31
38
|
total_messages: row["total_messages"],
|
|
32
|
-
scrape_time: row["scrape_time"]
|
|
39
|
+
scrape_time: row["scrape_time"],
|
|
40
|
+
queue_visible_length: row["queue_visible_length"],
|
|
41
|
+
default_partition_length: row["default_partition_length"]
|
|
33
42
|
)
|
|
34
43
|
end
|
|
35
44
|
end
|
data/lib/pgmq/queue_name.rb
CHANGED
|
@@ -126,13 +126,12 @@ module PGMQ
|
|
|
126
126
|
# a digit), truncates to fit {MAX_LENGTH}, and falls back to {SANITIZE_FALLBACK} when nothing usable remains. The
|
|
127
127
|
# return value always satisfies {.valid?}.
|
|
128
128
|
#
|
|
129
|
+
# @param name [String, #to_s] arbitrary input
|
|
130
|
+
# @return [String] a guaranteed-valid queue name
|
|
129
131
|
# @note Because it coerces rather than rejects, distinct inputs can map to the *same* name (e.g. +"a/b"+ and
|
|
130
132
|
# +"a-b"+ both become +"a_b"+; +"!!!"+ and +""+ both become +"queue"+). If your name selects a queue table,
|
|
131
133
|
# that means two logically different inputs could share one queue. When that matters - especially for untrusted
|
|
132
134
|
# 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
135
|
def sanitize(name)
|
|
137
136
|
cleaned = name.to_s.downcase
|
|
138
137
|
.gsub(/[^a-z0-9_]+/, "_").squeeze("_")
|
data/lib/pgmq/transaction.rb
CHANGED
|
@@ -20,14 +20,20 @@ module PGMQ
|
|
|
20
20
|
# txn.produce("orders", '{"order_id":123}')
|
|
21
21
|
# raise "Error" # Both operations rolled back
|
|
22
22
|
# end
|
|
23
|
+
#
|
|
24
|
+
# @note The transactional client is bound to the fiber (and thread) that opened the transaction and is valid only
|
|
25
|
+
# inside the block. Using it from another thread or fiber, or after the block returns, raises
|
|
26
|
+
# {PGMQ::Errors::TransactionError} instead of silently running the operation outside the transaction.
|
|
23
27
|
module Transaction
|
|
24
28
|
# Executes PGMQ operations atomically within a database transaction
|
|
25
29
|
#
|
|
26
30
|
# Obtains a connection from the pool, starts a transaction, and yields a client that uses that transaction for all
|
|
27
|
-
# operations.
|
|
31
|
+
# operations made from the calling thread/fiber while the block runs. The yielded client raises
|
|
32
|
+
# {PGMQ::Errors::TransactionError} when used from another thread or fiber, or after the block has returned.
|
|
28
33
|
#
|
|
29
34
|
# @yield [PGMQ::Client] transactional client using the same connection
|
|
30
35
|
# @return [Object] result of the block
|
|
36
|
+
# @raise [PGMQ::Errors::TransactionError] if the transactional client was misused and the error was not rescued
|
|
31
37
|
# @raise [PGMQ::Errors::ConnectionError] if transaction fails
|
|
32
38
|
#
|
|
33
39
|
# @example
|
|
@@ -38,9 +44,17 @@ module PGMQ
|
|
|
38
44
|
def transaction
|
|
39
45
|
@connection.with_connection do |conn|
|
|
40
46
|
conn.transaction do
|
|
41
|
-
|
|
47
|
+
txn = TransactionalClient.new(self, conn)
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
yield txn
|
|
51
|
+
ensure
|
|
52
|
+
txn.finish!
|
|
53
|
+
end
|
|
42
54
|
end
|
|
43
55
|
end
|
|
56
|
+
rescue PGMQ::Errors::TransactionError
|
|
57
|
+
raise
|
|
44
58
|
rescue PG::Error, StandardError => e
|
|
45
59
|
raise PGMQ::Errors::ConnectionError, "Transaction failed: #{e.message}"
|
|
46
60
|
end
|
|
@@ -57,13 +71,19 @@ module PGMQ
|
|
|
57
71
|
)
|
|
58
72
|
@parent = parent
|
|
59
73
|
@conn = conn
|
|
74
|
+
@owner = Fiber.current
|
|
75
|
+
@finished = false
|
|
60
76
|
end
|
|
61
77
|
|
|
62
78
|
# Forward all method calls to parent, but use our transaction connection
|
|
63
79
|
# @param method [Symbol] method name
|
|
64
80
|
# @return [Object] result of method call
|
|
81
|
+
# @raise [PGMQ::Errors::TransactionError] if used off the owner fiber or after the transaction ended
|
|
65
82
|
def method_missing(method, ...)
|
|
66
|
-
@parent.respond_to?(method, true)
|
|
83
|
+
return super unless @parent.respond_to?(method, true)
|
|
84
|
+
|
|
85
|
+
ensure_usable!(method)
|
|
86
|
+
@parent.__send__(method, ...)
|
|
67
87
|
end
|
|
68
88
|
|
|
69
89
|
# Check if method exists on parent
|
|
@@ -78,7 +98,9 @@ module PGMQ
|
|
|
78
98
|
end
|
|
79
99
|
|
|
80
100
|
# Inject our transaction connection instead of using the pool
|
|
101
|
+
# @raise [PGMQ::Errors::TransactionError] if used off the owner fiber or after the transaction ended
|
|
81
102
|
def with_connection
|
|
103
|
+
ensure_usable!(:with_connection)
|
|
82
104
|
yield @conn
|
|
83
105
|
end
|
|
84
106
|
|
|
@@ -86,6 +108,31 @@ module PGMQ
|
|
|
86
108
|
def connection
|
|
87
109
|
@parent.connection
|
|
88
110
|
end
|
|
111
|
+
|
|
112
|
+
# Marks the transaction as ended so any further use raises
|
|
113
|
+
# @return [void]
|
|
114
|
+
def finish!
|
|
115
|
+
@finished = true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# Parent operations reach the transaction connection only through the pool's re-entrant checkout, which is keyed
|
|
121
|
+
# on the current fiber. Another thread/fiber would get a different connection and commit outside the transaction.
|
|
122
|
+
#
|
|
123
|
+
# @param method [Symbol] method being called, for the error message
|
|
124
|
+
# @raise [PGMQ::Errors::TransactionError] if used off the owner fiber or after the transaction ended
|
|
125
|
+
def ensure_usable!(method)
|
|
126
|
+
if @finished
|
|
127
|
+
raise PGMQ::Errors::TransactionError, "Cannot call ##{method}: the transaction has already ended"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
return if Fiber.current.equal?(@owner)
|
|
131
|
+
|
|
132
|
+
raise PGMQ::Errors::TransactionError,
|
|
133
|
+
"Cannot call ##{method} from another thread or fiber: the transactional client is bound to the fiber " \
|
|
134
|
+
"that opened the transaction"
|
|
135
|
+
end
|
|
89
136
|
end
|
|
90
137
|
end
|
|
91
138
|
end
|
data/lib/pgmq/version.rb
CHANGED
data/lib/pgmq.rb
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require "zeitwerk"
|
|
4
4
|
require "time"
|
|
5
|
+
require "yaml"
|
|
5
6
|
|
|
6
7
|
loader = Zeitwerk::Loader.for_gem
|
|
7
8
|
loader.inflector.inflect(
|
|
8
9
|
"pgmq" => "PGMQ"
|
|
9
10
|
)
|
|
10
11
|
loader.setup
|
|
11
|
-
loader.eager_load
|
|
12
12
|
|
|
13
13
|
# PGMQ - Low-level Ruby client for Postgres Message Queue
|
|
14
14
|
#
|
|
@@ -38,6 +38,11 @@ loader.eager_load
|
|
|
38
38
|
# client.drop_queue('orders')
|
|
39
39
|
module PGMQ
|
|
40
40
|
class << self
|
|
41
|
+
# @return [String] root path of this gem
|
|
42
|
+
def gem_root
|
|
43
|
+
File.expand_path("..", __dir__)
|
|
44
|
+
end
|
|
45
|
+
|
|
41
46
|
# Convenience method to create a new client
|
|
42
47
|
#
|
|
43
48
|
# @return [PGMQ::Client] new client instance
|
|
@@ -50,3 +55,5 @@ module PGMQ
|
|
|
50
55
|
end
|
|
51
56
|
end
|
|
52
57
|
end
|
|
58
|
+
|
|
59
|
+
loader.eager_load
|
data/pgmq-ruby.gemspec
CHANGED
|
@@ -22,11 +22,11 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
spec.metadata["documentation_uri"] = "https://github.com/mensfeld/pgmq-ruby#readme"
|
|
23
23
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
24
24
|
|
|
25
|
-
spec.files = Dir["lib/**/*", "CHANGELOG.md", "LICENSE", "README.md", "pgmq-ruby.gemspec"]
|
|
25
|
+
spec.files = Dir["lib/**/*", "config/**/*", "CHANGELOG.md", "LICENSE", "README.md", "pgmq-ruby.gemspec"]
|
|
26
26
|
spec.require_paths = ["lib"]
|
|
27
27
|
|
|
28
28
|
# Runtime dependencies
|
|
29
|
-
spec.add_dependency "connection_pool", "
|
|
29
|
+
spec.add_dependency "connection_pool", ">= 2.4", "< 4"
|
|
30
30
|
spec.add_dependency "pg", "~> 1.5"
|
|
31
31
|
spec.add_dependency "zeitwerk", "~> 2.6"
|
|
32
32
|
end
|
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.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maciej Mensfeld
|
|
@@ -13,16 +13,22 @@ dependencies:
|
|
|
13
13
|
name: connection_pool
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '2.4'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '4'
|
|
19
22
|
type: :runtime
|
|
20
23
|
prerelease: false
|
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
25
|
requirements:
|
|
23
|
-
- - "
|
|
26
|
+
- - ">="
|
|
24
27
|
- !ruby/object:Gem::Version
|
|
25
28
|
version: '2.4'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '4'
|
|
26
32
|
- !ruby/object:Gem::Dependency
|
|
27
33
|
name: pg
|
|
28
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -62,6 +68,7 @@ files:
|
|
|
62
68
|
- CHANGELOG.md
|
|
63
69
|
- LICENSE
|
|
64
70
|
- README.md
|
|
71
|
+
- config/locales/errors.yml
|
|
65
72
|
- lib/pgmq.rb
|
|
66
73
|
- lib/pgmq/client.rb
|
|
67
74
|
- lib/pgmq/client/autovacuum.rb
|
|
@@ -74,6 +81,18 @@ files:
|
|
|
74
81
|
- lib/pgmq/client/queue_management.rb
|
|
75
82
|
- lib/pgmq/client/topics.rb
|
|
76
83
|
- lib/pgmq/connection.rb
|
|
84
|
+
- lib/pgmq/consumer.rb
|
|
85
|
+
- lib/pgmq/contractable.rb
|
|
86
|
+
- lib/pgmq/contractable/contract.rb
|
|
87
|
+
- lib/pgmq/contractable/result.rb
|
|
88
|
+
- lib/pgmq/contractable/rule.rb
|
|
89
|
+
- lib/pgmq/contracts.rb
|
|
90
|
+
- lib/pgmq/contracts/autovacuum.rb
|
|
91
|
+
- lib/pgmq/contracts/batch.rb
|
|
92
|
+
- lib/pgmq/contracts/connection.rb
|
|
93
|
+
- lib/pgmq/contracts/consumer.rb
|
|
94
|
+
- lib/pgmq/contracts/message.rb
|
|
95
|
+
- lib/pgmq/contracts/multi_queue.rb
|
|
77
96
|
- lib/pgmq/errors.rb
|
|
78
97
|
- lib/pgmq/message.rb
|
|
79
98
|
- lib/pgmq/metrics.rb
|
|
@@ -107,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
107
126
|
- !ruby/object:Gem::Version
|
|
108
127
|
version: '0'
|
|
109
128
|
requirements: []
|
|
110
|
-
rubygems_version: 4.0.
|
|
129
|
+
rubygems_version: 4.0.20
|
|
111
130
|
specification_version: 4
|
|
112
131
|
summary: Ruby client for PGMQ (Postgres Message Queue)
|
|
113
132
|
test_files: []
|