pgmq-ruby 0.5.0 → 0.6.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5648229b33f490f7228d7f9b106fb40939632c6effff094d028d9f7dd5fc045a
4
- data.tar.gz: 7a7d0ae6dd7ee59cf329093f4772e8cd8706e25be0e627defca60f3983e96084
3
+ metadata.gz: c2a31db1ba5a65a0666bb496ae5b54e0a51de5cafe9b92ebf55b432ea309ef44
4
+ data.tar.gz: cbf292400bff6ca3f34bdb95ff8a7c6910df5da4db3cbabc574c7b3173864523
5
5
  SHA512:
6
- metadata.gz: 5da9b67730f7c1b58af6be47c2364092988823b2c536e762ca56cc91bd5f27d2d678aef116373c7f0c62fec5037438f30ae0482979c0df3f9414aa3e5f41987a
7
- data.tar.gz: 7c892fcc8d30d306670b7dd3fd3a3e3f70f573916b3e6526e0833a59acad85cdd44fcf830fd327ccba6608fe0e30eb072d2adc1bcb287ccc28c388e743cfd19e
6
+ metadata.gz: 687e92a06cb6fd93f2015390153693fb267faebc7b4bc8c54fd547cfda2ec87a1621e2be47bb9f2ac70c40b1394a854daee85c4d069845b3756c980672fc49c0
7
+ data.tar.gz: cb4c6c32f6d69e6329dd12fba2e319865087b08994e894ce169e3360595bc59fb425c6b432ca2ee040b13c4367f0b20ccf307f6fadf9b4d9619dbfc3a509f0a4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1 (2026-04-16)
4
+
5
+ ### Connection Management
6
+ - **[Fix]** `PGMQ::Connection#verify_connection!` now also resets connections whose status is `PG::CONNECTION_BAD`. Previously it only checked `conn.finished?`, which misses connections closed server-side by the database or an intermediate pooler (PgBouncer `server_idle_timeout` / `client_idle_timeout`, admin kill, TCP RST). A pooled connection with a dead socket would survive `verify_connection!` and fail on the next operation with `PQsocket() can't get socket descriptor`.
7
+ - **[Fix]** `PGMQ::Connection#connection_lost_error?` now recognises `"PQsocket() can't get socket descriptor"` (plus `"connection is closed"` / `"connection has been closed"`). This is the exact error the `pg` gem raises from its C extension when the cached libpq socket FD is gone. Without the match, the `auto_reconnect` retry path in `with_connection` skipped this error and producers failed on the first call following a server-side close.
8
+
9
+ ## 0.6.0 (2026-04-02)
10
+
11
+ ### Breaking Changes
12
+ - **[Breaking]** Drop Ruby 3.2 support. Minimum required Ruby version is now 3.3.0.
13
+
14
+ ### Connection Management
15
+ - **[Breaking]** Detect shared `PG::Connection` across pool slots at creation time. When a callable connection factory returns the same `PG::Connection` object to multiple pool slots, concurrent threads corrupt libpq's internal state, causing nil `PG::Result` (`NoMethodError: undefined method 'ntuples' for nil`), segfaults, or wrong data. The pool now tracks connection identity via `ObjectSpace::WeakKeyMap` and raises `PGMQ::Errors::ConfigurationError` immediately with a descriptive error message. WeakKeyMap entries are automatically cleaned up when connections are GC'd. **This change is breaking for configurations that intentionally share a single `PG::Connection` across multiple pool slots. Users must ensure their callable returns a distinct `PG::Connection` per pool slot or configure `pool_size: 1` when reusing a single shared connection.**
16
+
17
+ ### Infrastructure
18
+ - **[Change]** Migrate test framework from RSpec to Minitest/Spec with Mocha for mocking, aligning with the broader Karafka ecosystem conventions.
19
+ - **[Change]** Replace `rubocop-rspec` with `rubocop-minitest` for test linting.
20
+ - **[Change]** Add `bin/integrations` runner script that centralizes integration spec execution. Specs no longer need `require_relative "support/example_helper"` — the runner injects it via `-r` flag. Run all specs with `bin/integrations` or specific ones with `bin/integrations spec/integration/foo_spec.rb`.
21
+
3
22
  ## 0.5.0 (2026-02-24)
4
23
 
5
24
  ### Breaking Changes
@@ -153,7 +172,7 @@ Initial release of pgmq-ruby - a low-level Ruby client for PGMQ (PostgreSQL Mess
153
172
  - [Enhancement] Example scripts demonstrating all features.
154
173
 
155
174
  ### Dependencies
156
- - Ruby >= 3.2.0
175
+ - Ruby >= 3.3.0
157
176
  - PostgreSQL >= 14 with PGMQ extension
158
177
  - `pg` gem (~> 1.5)
159
178
  - `connection_pool` gem (~> 2.4)
data/README.md CHANGED
@@ -877,7 +877,13 @@ bundle install
877
877
  docker compose up -d
878
878
 
879
879
  # Run tests
880
- bundle exec rspec
880
+ bundle exec rake test
881
+
882
+ # Run all integration specs
883
+ bin/integrations
884
+
885
+ # Run a specific integration spec
886
+ bin/integrations spec/integration/basic_produce_consume_spec.rb
881
887
 
882
888
  # Run console
883
889
  bundle exec bin/console
@@ -111,29 +111,41 @@ module PGMQ
111
111
  # @param error [PG::Error] the error to check
112
112
  # @return [Boolean] true if connection was lost
113
113
  def connection_lost_error?(error)
114
- # Common connection lost errors
114
+ # Common connection lost errors. Include the pg-gem C-extension message
115
+ # ("PQsocket() can't get socket descriptor") that is raised when the
116
+ # cached libpq socket descriptor is gone — e.g. after a server-side
117
+ # close by a connection pooler such as PgBouncer.
115
118
  lost_connection_messages = [
116
119
  "server closed the connection",
117
120
  "connection not open",
121
+ "connection is closed",
122
+ "connection has been closed",
118
123
  "no connection to the server",
119
124
  "terminating connection",
120
125
  "connection to server was lost",
121
- "could not receive data from server"
126
+ "could not receive data from server",
127
+ "pqsocket() can't get socket descriptor"
122
128
  ]
123
129
 
124
- message = error.message.downcase
130
+ message = error.message.to_s.downcase
125
131
  lost_connection_messages.any? { |pattern| message.include?(pattern) }
126
132
  end
127
133
 
128
- # Verifies a connection is alive and working
134
+ # Verifies a connection is alive and working.
135
+ #
136
+ # Also resets when the connection reports `PG::CONNECTION_BAD`, which
137
+ # happens when the server (or an intermediate pooler such as PgBouncer)
138
+ # has closed the socket while the client-side `PG::Connection` object
139
+ # still exists. `#finished?` alone only catches connections closed
140
+ # explicitly from the client side.
141
+ #
129
142
  # @param conn [PG::Connection] connection to verify
130
- # @raise [PG::Error] if connection is not working
143
+ # @raise [PG::Error] if the reset itself fails
131
144
  def verify_connection!(conn)
132
- # Quick check - is connection object in bad state?
133
- return unless conn.finished?
145
+ return conn.reset if conn.finished?
146
+ return conn.reset if conn.status == PG::CONNECTION_BAD
134
147
 
135
- # Connection is finished/closed, try to reset it
136
- conn.reset
148
+ nil
137
149
  end
138
150
 
139
151
  # Normalizes various connection parameter formats
@@ -169,9 +181,32 @@ module PGMQ
169
181
  # @return [ConnectionPool]
170
182
  def create_pool
171
183
  params = @conn_params
184
+ seen_connections = ObjectSpace::WeakKeyMap.new
185
+ seen_mutex = Mutex.new
172
186
 
173
187
  ConnectionPool.new(size: @pool_size, timeout: @pool_timeout) do
174
- create_connection(params)
188
+ conn = create_connection(params)
189
+
190
+ # Detect shared connections: if a callable returns the same PG::Connection
191
+ # object to multiple pool slots, concurrent use will corrupt libpq state
192
+ # (nil results, segfaults, wrong data). Fail fast with a clear message.
193
+ if conn.is_a?(PG::Connection)
194
+ seen_mutex.synchronize do
195
+ if seen_connections.key?(conn)
196
+ raise PGMQ::Errors::ConfigurationError,
197
+ "Connection callable returned the same PG::Connection object " \
198
+ "(object_id: #{conn.object_id}) to multiple pool slots. " \
199
+ "PG::Connection is NOT thread-safe — concurrent use causes nil results, " \
200
+ "segfaults, and data corruption. Ensure your callable returns a unique " \
201
+ "PG::Connection instance on each invocation (for example, by calling " \
202
+ "PG.connect inside the callable)."
203
+ end
204
+
205
+ seen_connections[conn] = true
206
+ end
207
+ end
208
+
209
+ conn
175
210
  end
176
211
  rescue => e
177
212
  raise PGMQ::Errors::ConnectionError, "Failed to create connection pool: #{e.message}"
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.5.0"
5
+ VERSION = "0.6.1"
6
6
  end
data/pgmq-ruby.gemspec CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
13
13
  "Like AWS SQS and RSMQ, but on Postgres."
14
14
  spec.homepage = "https://github.com/mensfeld/pgmq-ruby"
15
15
  spec.license = "LGPL-3.0"
16
- spec.required_ruby_version = ">= 3.2.0"
16
+ spec.required_ruby_version = ">= 3.3.0"
17
17
 
18
18
  spec.metadata["homepage_uri"] = spec.homepage
19
19
  spec.metadata["source_code_uri"] = "https://github.com/mensfeld/pgmq-ruby"
@@ -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 = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|examples)/}) }
25
+ spec.files = Dir["lib/**/*", "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.5.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -59,23 +59,9 @@ executables: []
59
59
  extensions: []
60
60
  extra_rdoc_files: []
61
61
  files:
62
- - ".github/workflows/ci.yml"
63
- - ".github/workflows/push.yml"
64
- - ".gitignore"
65
- - ".rspec"
66
- - ".rubocop.yml"
67
- - ".ruby-version"
68
- - ".yard-lint.yml"
69
62
  - CHANGELOG.md
70
- - CLAUDE.md
71
- - Gemfile
72
- - Gemfile.lint
73
- - Gemfile.lint.lock
74
- - Gemfile.lock
75
63
  - LICENSE
76
64
  - README.md
77
- - Rakefile
78
- - docker-compose.yml
79
65
  - lib/pgmq.rb
80
66
  - lib/pgmq/client.rb
81
67
  - lib/pgmq/client/consumer.rb
@@ -93,10 +79,7 @@ files:
93
79
  - lib/pgmq/queue_metadata.rb
94
80
  - lib/pgmq/transaction.rb
95
81
  - lib/pgmq/version.rb
96
- - package-lock.json
97
- - package.json
98
82
  - pgmq-ruby.gemspec
99
- - renovate.json
100
83
  homepage: https://github.com/mensfeld/pgmq-ruby
101
84
  licenses:
102
85
  - LGPL-3.0
@@ -114,14 +97,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
114
97
  requirements:
115
98
  - - ">="
116
99
  - !ruby/object:Gem::Version
117
- version: 3.2.0
100
+ version: 3.3.0
118
101
  required_rubygems_version: !ruby/object:Gem::Requirement
119
102
  requirements:
120
103
  - - ">="
121
104
  - !ruby/object:Gem::Version
122
105
  version: '0'
123
106
  requirements: []
124
- rubygems_version: 4.0.3
107
+ rubygems_version: 4.0.6
125
108
  specification_version: 4
126
109
  summary: Ruby client for PGMQ (Postgres Message Queue)
127
110
  test_files: []
@@ -1,183 +0,0 @@
1
- name: CI
2
-
3
- concurrency:
4
- group: ${{ github.workflow }}-${{ github.ref }}
5
- cancel-in-progress: true
6
-
7
- on:
8
- pull_request:
9
- branches: [ master ]
10
- schedule:
11
- - cron: '0 1 * * *'
12
-
13
- permissions:
14
- contents: read
15
-
16
- jobs:
17
- specs:
18
- timeout-minutes: 15
19
- runs-on: ubuntu-latest
20
- strategy:
21
- fail-fast: false
22
- matrix:
23
- ruby:
24
- - '4.0.0'
25
- - '3.4'
26
- - '3.3'
27
- - '3.2'
28
- postgres:
29
- - '14'
30
- - '15'
31
- - '16'
32
- - '17'
33
- - '18'
34
- include:
35
- - ruby: '3.4'
36
- postgres: '18'
37
- coverage: 'true'
38
-
39
- services:
40
- postgres:
41
- image: ghcr.io/pgmq/pg${{ matrix.postgres }}-pgmq:latest
42
- env:
43
- POSTGRES_USER: postgres
44
- POSTGRES_PASSWORD: postgres
45
- POSTGRES_DB: pgmq_test
46
- options: >-
47
- --health-cmd pg_isready
48
- --health-interval 10s
49
- --health-timeout 5s
50
- --health-retries 5
51
- ports:
52
- - 5433:5432
53
-
54
- steps:
55
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
56
- with:
57
- fetch-depth: 0
58
-
59
- - name: Install package dependencies
60
- run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends postgresql-client"
61
-
62
- - name: Remove Gemfile.lock for Ruby previews
63
- if: contains(matrix.ruby, '4.0')
64
- run: rm -f Gemfile.lock
65
-
66
- - name: Set up Ruby
67
- uses: ruby/setup-ruby@90be1154f987f4dc0fe0dd0feedac9e473aa4ba8 # v1.286.0
68
- with:
69
- ruby-version: ${{ matrix.ruby }}
70
- bundler-cache: true
71
- bundler: 'latest'
72
- self-hosted: false
73
-
74
- - name: Wait for PostgreSQL
75
- run: sleep 5
76
-
77
- - name: Install latest bundler
78
- run: |
79
- gem install bundler --no-document
80
- gem update --system --no-document
81
- bundle config set without 'tools benchmarks docs'
82
-
83
- - name: Bundle install
84
- run: |
85
- bundle config set without development
86
- bundle install --jobs 4 --retry 3
87
-
88
- - name: Create PGMQ extension
89
- env:
90
- PGPASSWORD: postgres
91
- run: |
92
- psql -h localhost -p 5433 -U postgres -d pgmq_test -c "CREATE EXTENSION IF NOT EXISTS pgmq CASCADE;"
93
-
94
- - name: Run all tests
95
- env:
96
- PG_HOST: localhost
97
- PG_PORT: 5433
98
- PG_DATABASE: pgmq_test
99
- PG_USER: postgres
100
- PG_PASSWORD: postgres
101
- CI: true
102
- GITHUB_COVERAGE: ${{ matrix.coverage }}
103
- run: bundle exec rspec
104
-
105
- - name: Run examples
106
- env:
107
- PG_HOST: localhost
108
- PG_PORT: 5433
109
- PG_DATABASE: pgmq_test
110
- PG_USER: postgres
111
- PG_PASSWORD: postgres
112
- run: bundle exec rake examples
113
-
114
- yard-lint:
115
- timeout-minutes: 5
116
- runs-on: ubuntu-latest
117
- strategy:
118
- fail-fast: false
119
- env:
120
- BUNDLE_GEMFILE: Gemfile.lint
121
- steps:
122
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
123
- with:
124
- fetch-depth: 0
125
- - name: Set up Ruby
126
- uses: ruby/setup-ruby@90be1154f987f4dc0fe0dd0feedac9e473aa4ba8 # v1.286.0
127
- with:
128
- ruby-version: '4.0.0'
129
- bundler-cache: true
130
- - name: Run yard-lint
131
- run: bundle exec yard-lint lib/
132
-
133
- rubocop:
134
- timeout-minutes: 5
135
- runs-on: ubuntu-latest
136
- env:
137
- BUNDLE_GEMFILE: Gemfile.lint
138
- steps:
139
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
140
- with:
141
- fetch-depth: 0
142
- - name: Set up Ruby
143
- uses: ruby/setup-ruby@90be1154f987f4dc0fe0dd0feedac9e473aa4ba8 # v1.286.0
144
- with:
145
- ruby-version: '4.0.0'
146
- bundler-cache: true
147
- - name: Run rubocop
148
- run: bundle exec rubocop
149
-
150
- lostconf:
151
- timeout-minutes: 5
152
- runs-on: ubuntu-latest
153
- steps:
154
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
155
- with:
156
- fetch-depth: 0
157
- - name: Set up Node.js
158
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
159
- with:
160
- node-version: '20'
161
- cache: 'npm'
162
- - name: Install dependencies
163
- run: npm ci
164
- - name: Run lostconf
165
- run: npx lostconf --fail-on-stale
166
-
167
- ci-success:
168
- name: CI Success
169
- runs-on: ubuntu-latest
170
- if: always()
171
- needs:
172
- - rubocop
173
- - specs
174
- - yard-lint
175
- - lostconf
176
- steps:
177
- - name: Check all jobs passed
178
- if: |
179
- contains(needs.*.result, 'failure') ||
180
- contains(needs.*.result, 'cancelled') ||
181
- contains(needs.*.result, 'skipped')
182
- run: exit 1
183
- - run: echo "All CI checks passed!"
@@ -1,35 +0,0 @@
1
- name: Push Gem
2
-
3
- on:
4
- push:
5
- tags:
6
- - v*
7
-
8
- permissions:
9
- contents: read
10
-
11
- jobs:
12
- push:
13
- if: github.repository_owner == 'mensfeld'
14
- runs-on: ubuntu-latest
15
- environment: deployment
16
-
17
- permissions:
18
- contents: write
19
- id-token: write
20
-
21
- steps:
22
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
23
- with:
24
- fetch-depth: 0
25
-
26
- - name: Set up Ruby
27
- uses: ruby/setup-ruby@90be1154f987f4dc0fe0dd0feedac9e473aa4ba8 # v1.286.0
28
- with:
29
- bundler-cache: false
30
-
31
- - name: Bundle install
32
- run: |
33
- bundle install --jobs 4 --retry 3
34
-
35
- - uses: rubygems/release-gem@1c162a739e8b4cb21a676e97b087e8268d8fc40b # v1.1.2
data/.gitignore DELETED
@@ -1,67 +0,0 @@
1
- *.gem
2
- *.rbc
3
- /.config
4
- /coverage/
5
- /InstalledFiles
6
- /pkg/
7
- /spec/reports/
8
- /spec/examples.txt
9
- /test/tmp/
10
- /test/version_tmp/
11
- /tmp/
12
-
13
- # Used by dotenv library to load environment variables.
14
- # .env
15
-
16
- # Ignore Byebug command history file.
17
- .byebug_history
18
-
19
- ## Specific to RubyMotion:
20
- .dat*
21
- .repl_history
22
- build/
23
- *.bridgesupport
24
- build-iPhoneOS/
25
- build-iPhoneSimulator/
26
-
27
- ## Specific to RubyMotion (use of CocoaPods):
28
- #
29
- # We recommend against adding the Pods directory to your .gitignore. However
30
- # you should judge for yourself, the pros and cons are mentioned at:
31
- # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
- #
33
- # vendor/Pods/
34
-
35
- ## Documentation cache and generated files:
36
- /.yardoc/
37
- /_yardoc/
38
- /doc/
39
- /rdoc/
40
-
41
- ## RSpec test status file
42
- .rspec_status
43
-
44
- ## Environment normalization:
45
- /.bundle/
46
- /vendor/bundle
47
- /lib/bundler/man/
48
-
49
- # for a library or gem, you might want to ignore these files since the code is
50
- # intended to run in multiple environments; otherwise, check them in:
51
- # Gemfile.lock
52
- # .ruby-version
53
- # .ruby-gemset
54
-
55
- # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
56
- .rvmrc
57
-
58
- # Used by RuboCop. Remote config files pulled in from inherit_from directive.
59
- # .rubocop-https?--*
60
-
61
- .DS_Store
62
-
63
- ## Claude Code settings (user-specific)
64
- .claude/
65
-
66
- ## Coditsu configuration (user-specific)
67
- .coditsu/local.yml
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --require spec_helper
2
- --exclude-pattern spec/integration/**/*
data/.rubocop.yml DELETED
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- plugins:
4
- - rubocop-capybara
5
- - rubocop-factory_bot
6
- - rubocop-performance
7
- - rubocop-rspec
8
- - rubocop-rspec_rails
9
-
10
- inherit_gem:
11
- standard: config/base.yml
12
- standard-performance: config/base.yml
13
- standard-rspec: config/base.yml
14
-
15
- AllCops:
16
- NewCops: enable
17
- TargetRubyVersion: 3.2
18
- Include:
19
- - "**/*.rb"
20
- - "**/*.gemspec"
21
- - "**/Gemfile"
22
- - "**/Rakefile"
23
- - Gemfile.lint
24
-
25
- # Disabled departments - not a Rails project, no Capybara/FactoryBot
26
- Capybara:
27
- Enabled: false
28
-
29
- FactoryBot:
30
- Enabled: false
31
-
32
- RSpecRails:
33
- Enabled: false
34
-
35
- # Layout
36
- Layout/LineLength:
37
- Max: 120
38
-
39
- Layout/SpaceInsideHashLiteralBraces:
40
- EnforcedStyle: space
41
-
42
- # RSpec - exclude integration examples (they use ExampleHelper, not RSpec)
43
- RSpec:
44
- Exclude:
45
- - spec/integration/**/*
46
-
47
- RSpec/ExampleLength:
48
- Enabled: false
49
-
50
- RSpec/IndexedLet:
51
- Enabled: false
52
-
53
- RSpec/MultipleExpectations:
54
- Enabled: false
55
-
56
- RSpec/MultipleMemoizedHelpers:
57
- Max: 20
58
-
59
- RSpec/NestedGroups:
60
- Max: 4
61
-
62
- RSpec/NoExpectationExample:
63
- Enabled: false
64
-
65
- RSpec/SpecFilePathFormat:
66
- Enabled: false
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 4.0.0