pgoutput-client 0.2.3 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a375cce8bbb2d330974bfd22bdc0a7542c1c1ed5fe73f448e41bba717532aac2
4
- data.tar.gz: 3c20c8e182548d5556d4a8a503a92d2d8c89d5c926a59eaaf537812d4bb5cdca
3
+ metadata.gz: e4f6889c573693726868891513ab942b96ea305cef6c70407216a0fdf9046a32
4
+ data.tar.gz: 6ae8423d421ba51472c3f777451bc970f986c2b2b52c9cf6a234f97491c0ea70
5
5
  SHA512:
6
- metadata.gz: 78fc7ede75ae5fcd4b27adddef61d1f6b59ba8a26943bb44bf93d0dc5f2d97a477f36d12ec935fdf083792cbc8d37418e0b2a9f84fb7d14d15aa7e5847f8d2fd
7
- data.tar.gz: 58ea9e8475789846b3caad462fcfef693df07ad8ef9cf724e877f38ba2ca53a73b3e1476971a7b5b1c9ad47bb0b8c20fae2e31f47674a377bf03c7d09cb1533c
6
+ metadata.gz: 57d9bdda60c19cfc8c09e403d998af9f26bfcdf9ddb6dd81874f22142c00b8fd1e20cea7120fcbc7484886490870f1c3efe4fcab20a0e0a52283a2d456332977
7
+ data.tar.gz: 2840604c443d755c6850154284dfe412d60821adac80428b76fa3569f8db986109ee0457a695938b6e051f224e1b5bb69923cea13934a95184836595bfad6b4f
data/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
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
+
5
34
  ## 0.2.3 - 2026-06-17
6
35
 
7
36
  ### Fixed
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 call is non-blocking because the underlying `pg` call receives
80
- # `false` for its blocking argument. `nil` means no complete CopyData
81
- # payload is currently available.
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
- @pg_connection.get_copy_data(false)
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.
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Pgoutput
4
4
  module Client
5
+ # Internal immutable base class generated by `Data.define` for {Keepalive}.
6
+ #
7
+ # @api private
5
8
  KeepaliveData = Data.define(:wal_end, :server_clock, :reply_requested)
6
9
 
7
10
  # Immutable primary keepalive replication message.
@@ -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.
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Pgoutput
4
4
  module Client
5
+ # Internal immutable base class generated by `Data.define` for {RunnerState}.
6
+ #
7
+ # @api private
5
8
  RunnerStateData = Data.define(
6
9
  :running,
7
10
  :stop_requested,
@@ -5,6 +5,6 @@ module Pgoutput
5
5
  # Current pgoutput-client gem version.
6
6
  #
7
7
  # @return [String]
8
- VERSION = "0.2.3"
8
+ VERSION = "0.2.4"
9
9
  end
10
10
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Pgoutput
4
4
  module Client
5
+ # Internal immutable base class generated by `Data.define` for {XLogData}.
6
+ #
7
+ # @api private
5
8
  ReplicationXLogData = Data.define(:wal_start, :wal_end, :server_clock, :payload)
6
9
 
7
10
  # Immutable XLogData replication envelope.
@@ -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 call is non-blocking because the underlying `pg` call receives
65
- # `false` for its blocking argument. `nil` means no complete CopyData
66
- # payload is currently available.
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgoutput-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa