pgoutput-client 0.2.2 → 0.2.4
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 +51 -0
- data/README.md +51 -0
- data/lib/pgoutput/client/connection.rb +18 -4
- data/lib/pgoutput/client/feedback.rb +3 -0
- data/lib/pgoutput/client/keepalive.rb +3 -0
- data/lib/pgoutput/client/runner.rb +24 -4
- data/lib/pgoutput/client/state.rb +3 -0
- data/lib/pgoutput/client/version.rb +1 -1
- data/lib/pgoutput/client/xlog_data.rb +3 -0
- data/lib/pgoutput/client.rb +7 -0
- data/sig/pgoutput/client/connection.rbs +7 -3
- data/sig/pgoutput/client/runner.rbs +4 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4f6889c573693726868891513ab942b96ea305cef6c70407216a0fdf9046a32
|
|
4
|
+
data.tar.gz: 6ae8423d421ba51472c3f777451bc970f986c2b2b52c9cf6a234f97491c0ea70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57d9bdda60c19cfc8c09e403d998af9f26bfcdf9ddb6dd81874f22142c00b8fd1e20cea7120fcbc7484886490870f1c3efe4fcab20a0e0a52283a2d456332977
|
|
7
|
+
data.tar.gz: 2840604c443d755c6850154284dfe412d60821adac80428b76fa3569f8db986109ee0457a695938b6e051f224e1b5bb69923cea13934a95184836595bfad6b4f
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.4 - 2026-06-17
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added socket-aware replication stream polling.
|
|
10
|
+
- Added E2E coverage for PostgreSQL restart and replication stream recovery.
|
|
11
|
+
- Added connection readiness checks to E2E infrastructure.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Improved replication stream handling during idle periods.
|
|
16
|
+
- Improved reconnect behavior after PostgreSQL restarts.
|
|
17
|
+
- Improved standby feedback reliability during long-running streams.
|
|
18
|
+
- Improved E2E test stability across PostgreSQL startup and restart scenarios.
|
|
19
|
+
- Normalized `PG#get_copy_data` idle responses to simplify stream processing.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Fixed replication stream recovery after PostgreSQL restart.
|
|
24
|
+
- Fixed handling of idle COPY stream reads.
|
|
25
|
+
- Fixed reconnect loops triggered by PostgreSQL replication timeouts.
|
|
26
|
+
- Fixed E2E race conditions during PostgreSQL initialization.
|
|
27
|
+
- Fixed replication slot creation behavior when a slot already exists.
|
|
28
|
+
- Fixed several edge cases uncovered by Mammoth integration testing.
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
- Expanded YARD documentation coverage to 98.95%.
|
|
33
|
+
|
|
34
|
+
## 0.2.3 - 2026-06-17
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
* Fixed automatic replication slot creation when `auto_create_slot` is enabled.
|
|
39
|
+
* Fixed startup failure when the configured replication slot already exists.
|
|
40
|
+
* Improved logical replication restart behavior for persistent replication slots.
|
|
41
|
+
* Improved lifecycle management of existing replication slots across process and container restarts.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
* `auto_create_slot` now follows **ensure slot exists** semantics.
|
|
46
|
+
* Existing replication slots are treated as valid and reusable when automatic slot creation is enabled.
|
|
47
|
+
* Slot creation remains automatic for missing slots and idempotent for existing slots.
|
|
48
|
+
|
|
49
|
+
### Internal
|
|
50
|
+
|
|
51
|
+
* Hardened replication slot lifecycle handling.
|
|
52
|
+
* Expanded coverage around automatic slot creation scenarios.
|
|
53
|
+
* Updated signatures and tests for slot creation behavior.
|
|
54
|
+
|
|
55
|
+
|
|
5
56
|
## 0.2.2 - 2026-06-17
|
|
6
57
|
|
|
7
58
|
### Added
|
data/README.md
CHANGED
|
@@ -363,6 +363,57 @@ Equivalent Rake task:
|
|
|
363
363
|
bundle exec rake e2e:run
|
|
364
364
|
```
|
|
365
365
|
|
|
366
|
+
## Transport lifecycle behavior
|
|
367
|
+
|
|
368
|
+
`pgoutput-client` owns PostgreSQL logical replication transport and lifecycle
|
|
369
|
+
management. It opens the replication connection, optionally creates the logical
|
|
370
|
+
replication slot, starts streaming, sends standby status feedback, and retries
|
|
371
|
+
reconnectable failures.
|
|
372
|
+
|
|
373
|
+
### Idle standby feedback
|
|
374
|
+
|
|
375
|
+
Long-running replication streams can be quiet for long periods when no WAL
|
|
376
|
+
changes are produced. During those idle periods the client wakes periodically
|
|
377
|
+
and sends standby status feedback so PostgreSQL does not terminate the walsender
|
|
378
|
+
for replication timeout.
|
|
379
|
+
|
|
380
|
+
Control the feedback cadence with `feedback_interval`:
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
runner = Pgoutput::Client::Runner.new(
|
|
384
|
+
database_url: ENV.fetch("DATABASE_URL"),
|
|
385
|
+
slot_name: "mammoth_live",
|
|
386
|
+
publication_names: ["mammoth_publication"],
|
|
387
|
+
feedback_interval: 10.0
|
|
388
|
+
)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Idempotent automatic slot creation
|
|
392
|
+
|
|
393
|
+
When `auto_create_slot` is enabled, the client treats slot creation as
|
|
394
|
+
"ensure this slot exists". Missing slots are created before streaming; existing
|
|
395
|
+
slots are reused and do not cause startup failure.
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
runner = Pgoutput::Client::Runner.new(
|
|
399
|
+
database_url: ENV.fetch("DATABASE_URL"),
|
|
400
|
+
slot_name: "mammoth_live",
|
|
401
|
+
publication_names: ["mammoth_publication"],
|
|
402
|
+
auto_create_slot: true,
|
|
403
|
+
temporary_slot: false
|
|
404
|
+
)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Publication creation remains outside this gem. Create publications through
|
|
408
|
+
application migrations, database bootstrap SQL, or infrastructure tooling.
|
|
409
|
+
|
|
410
|
+
### Restart recovery
|
|
411
|
+
|
|
412
|
+
After a stream has connected successfully, transient PostgreSQL outages are
|
|
413
|
+
retried through the reconnect lifecycle. This includes ordinary container or
|
|
414
|
+
process restart windows where PostgreSQL temporarily refuses connections or
|
|
415
|
+
reports that the database system is starting up.
|
|
416
|
+
|
|
366
417
|
---
|
|
367
418
|
|
|
368
419
|
## License
|
|
@@ -76,14 +76,19 @@ module Pgoutput
|
|
|
76
76
|
|
|
77
77
|
# Receive one CopyData payload from the server.
|
|
78
78
|
#
|
|
79
|
-
# The
|
|
80
|
-
#
|
|
81
|
-
#
|
|
79
|
+
# The stream must not block forever while PostgreSQL is idle, because the
|
|
80
|
+
# caller needs opportunities to send periodic standby feedback. Wait
|
|
81
|
+
# briefly for socket readability, then use the pg driver's blocking
|
|
82
|
+
# CopyData read only when data is available. `nil` means the stream is
|
|
83
|
+
# currently idle.
|
|
82
84
|
#
|
|
83
85
|
# @return [String, nil] raw CopyData payload or `nil`
|
|
84
86
|
# @raise [ConnectionError] if receiving fails
|
|
85
87
|
def get_copy_data # rubocop:disable Naming/AccessorMethodName
|
|
86
|
-
|
|
88
|
+
return nil unless copy_data_readable?
|
|
89
|
+
|
|
90
|
+
copy_data = @pg_connection.get_copy_data(false)
|
|
91
|
+
copy_data == false ? nil : copy_data
|
|
87
92
|
rescue PG::Error => e
|
|
88
93
|
raise ConnectionError, e.message
|
|
89
94
|
end
|
|
@@ -110,6 +115,15 @@ module Pgoutput
|
|
|
110
115
|
|
|
111
116
|
private
|
|
112
117
|
|
|
118
|
+
def copy_data_readable?
|
|
119
|
+
return true unless @pg_connection.respond_to?(:socket_io)
|
|
120
|
+
|
|
121
|
+
socket = @pg_connection.socket_io
|
|
122
|
+
return true unless socket
|
|
123
|
+
|
|
124
|
+
!!IO.select([socket], nil, nil, 0.1)
|
|
125
|
+
end
|
|
126
|
+
|
|
113
127
|
def exec(sql)
|
|
114
128
|
@pg_connection.exec(sql)
|
|
115
129
|
rescue PG::Error => e
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Pgoutput
|
|
4
4
|
module Client
|
|
5
|
+
# Internal immutable base class generated by `Data.define` for {Feedback}.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
5
8
|
FeedbackData = Data.define(:received_lsn, :flushed_lsn, :applied_lsn, :client_clock, :reply_requested)
|
|
6
9
|
|
|
7
10
|
# Standby status feedback message builder.
|
|
@@ -37,7 +37,17 @@ module Pgoutput
|
|
|
37
37
|
# @see Stream
|
|
38
38
|
# @api public
|
|
39
39
|
class Runner
|
|
40
|
+
# Default number of reconnect attempts after a previously healthy stream
|
|
41
|
+
# fails. The default is intentionally large enough to survive ordinary
|
|
42
|
+
# PostgreSQL restart windows.
|
|
43
|
+
#
|
|
44
|
+
# @return [Integer]
|
|
40
45
|
DEFAULT_RECONNECT_ATTEMPTS = 30
|
|
46
|
+
|
|
47
|
+
# Base reconnect backoff, in seconds. Attempt `n` sleeps for
|
|
48
|
+
# `n * DEFAULT_RECONNECT_BACKOFF`.
|
|
49
|
+
#
|
|
50
|
+
# @return [Float]
|
|
41
51
|
DEFAULT_RECONNECT_BACKOFF = 0.5
|
|
42
52
|
|
|
43
53
|
# Configuration used by this runner.
|
|
@@ -194,14 +204,24 @@ module Pgoutput
|
|
|
194
204
|
private
|
|
195
205
|
|
|
196
206
|
def setup_connection(connection)
|
|
197
|
-
if configuration.auto_create_slot && !@slot_created
|
|
198
|
-
connection.create_replication_slot
|
|
199
|
-
@slot_created = true
|
|
200
|
-
end
|
|
207
|
+
ensure_replication_slot(connection) if configuration.auto_create_slot && !@slot_created
|
|
201
208
|
|
|
202
209
|
connection.start_replication
|
|
203
210
|
end
|
|
204
211
|
|
|
212
|
+
def ensure_replication_slot(connection)
|
|
213
|
+
connection.create_replication_slot
|
|
214
|
+
@slot_created = true
|
|
215
|
+
rescue ConnectionError => e
|
|
216
|
+
raise unless replication_slot_already_exists?(e)
|
|
217
|
+
|
|
218
|
+
@slot_created = true
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def replication_slot_already_exists?(error)
|
|
222
|
+
error.message.match?(/replication slot .* already exists/i)
|
|
223
|
+
end
|
|
224
|
+
|
|
205
225
|
def run_stream_cycle(configuration, &block)
|
|
206
226
|
connection = Connection.open(configuration)
|
|
207
227
|
setup_connection(connection)
|
data/lib/pgoutput/client.rb
CHANGED
|
@@ -13,6 +13,13 @@ require_relative "client/connection"
|
|
|
13
13
|
require_relative "client/stream"
|
|
14
14
|
require_relative "client/runner"
|
|
15
15
|
|
|
16
|
+
# Namespace for PostgreSQL pgoutput logical replication components.
|
|
17
|
+
#
|
|
18
|
+
# The top-level namespace is shared by pgoutput ecosystem gems. This gem
|
|
19
|
+
# defines only the `Pgoutput::Client` transport namespace and leaves protocol
|
|
20
|
+
# parsing, value decoding, and CDC normalization to sibling libraries.
|
|
21
|
+
#
|
|
22
|
+
# @api public
|
|
16
23
|
module Pgoutput
|
|
17
24
|
# Namespace for PostgreSQL logical replication transport support.
|
|
18
25
|
#
|
|
@@ -61,9 +61,11 @@ module Pgoutput
|
|
|
61
61
|
|
|
62
62
|
# Receive one CopyData payload from the server.
|
|
63
63
|
#
|
|
64
|
-
# The
|
|
65
|
-
#
|
|
66
|
-
#
|
|
64
|
+
# The stream must not block forever while PostgreSQL is idle, because the
|
|
65
|
+
# caller needs opportunities to send periodic standby feedback. Wait
|
|
66
|
+
# briefly for socket readability, then use the pg driver's blocking
|
|
67
|
+
# CopyData read only when data is available. `nil` means the stream is
|
|
68
|
+
# currently idle.
|
|
67
69
|
#
|
|
68
70
|
# @return [String, nil] raw CopyData payload or `nil`
|
|
69
71
|
# @raise [ConnectionError] if receiving fails
|
|
@@ -85,6 +87,8 @@ module Pgoutput
|
|
|
85
87
|
|
|
86
88
|
private
|
|
87
89
|
|
|
90
|
+
def copy_data_readable?: () -> bool
|
|
91
|
+
|
|
88
92
|
def exec: (untyped sql) -> untyped
|
|
89
93
|
end
|
|
90
94
|
end
|
|
@@ -83,6 +83,10 @@ module Pgoutput
|
|
|
83
83
|
|
|
84
84
|
def setup_connection: (untyped connection) -> untyped
|
|
85
85
|
|
|
86
|
+
def ensure_replication_slot: (untyped connection) -> untyped
|
|
87
|
+
|
|
88
|
+
def replication_slot_already_exists?: (untyped error) -> bool
|
|
89
|
+
|
|
86
90
|
def run_stream_cycle: (untyped configuration) { (?) -> untyped } -> untyped
|
|
87
91
|
|
|
88
92
|
def configuration_for_resume: () -> untyped
|