pgmq-ruby 0.7.2 → 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.
@@ -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
@@ -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
- yield TransactionalClient.new(self, conn)
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) ? @parent.__send__(method, ...) : super
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module PGMQ
4
4
  # Current version of the pgmq-ruby gem
5
- VERSION = "0.7.2"
5
+ VERSION = "0.8.0"
6
6
  end
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,7 +22,7 @@ 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
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.7.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -68,6 +68,7 @@ files:
68
68
  - CHANGELOG.md
69
69
  - LICENSE
70
70
  - README.md
71
+ - config/locales/errors.yml
71
72
  - lib/pgmq.rb
72
73
  - lib/pgmq/client.rb
73
74
  - lib/pgmq/client/autovacuum.rb
@@ -80,6 +81,18 @@ files:
80
81
  - lib/pgmq/client/queue_management.rb
81
82
  - lib/pgmq/client/topics.rb
82
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
83
96
  - lib/pgmq/errors.rb
84
97
  - lib/pgmq/message.rb
85
98
  - lib/pgmq/metrics.rb
@@ -113,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
126
  - !ruby/object:Gem::Version
114
127
  version: '0'
115
128
  requirements: []
116
- rubygems_version: 4.0.16
129
+ rubygems_version: 4.0.20
117
130
  specification_version: 4
118
131
  summary: Ruby client for PGMQ (Postgres Message Queue)
119
132
  test_files: []