pgoutput-client 0.2.3 → 0.3.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 +38 -0
- data/README.md +97 -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 +27 -4
- data/lib/pgoutput/client/slot_inspector.rb +74 -0
- data/lib/pgoutput/client/slot_status.rb +64 -0
- data/lib/pgoutput/client/state.rb +3 -0
- data/lib/pgoutput/client/stream.rb +2 -2
- data/lib/pgoutput/client/version.rb +1 -1
- data/lib/pgoutput/client/xlog_data.rb +3 -0
- data/lib/pgoutput/client.rb +9 -0
- data/sig/json.rbs +3 -0
- data/sig/pgoutput/client/connection.rbs +7 -3
- data/sig/pgoutput/client/runner.rbs +2 -0
- data/sig/pgoutput/client/slot_inspector.rbs +18 -0
- data/sig/pgoutput/client/slot_status.rbs +41 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a0c63a95d0df00f5a4b8b5a0e85b01bbec1ed71688a41f853c065e2f2b01a03
|
|
4
|
+
data.tar.gz: 3e86b0d6e62c54c03edd73a86be9dce33997a60f2d5bd097459b3a2a0d75795a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0577c07284f05bb0daa59ae055a39cdd456e2104f936c33b8a921473fb5554cf65dfae2f9d9d69ce82900e27dfb1a0f632c960654bae7526b73b62ab664f6007
|
|
7
|
+
data.tar.gz: e2ea5d853aee2079d280139f58f287063fc411e8ef846782c14b8f41c286bd1975027022ecfa395619f5494ce5622d640fc0a9b6bd1a0ca9a192ebbab5701d20
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.0 - 2026-07-17
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `Runner#slot_status` and `SlotInspector` for version-tolerant
|
|
10
|
+
`pg_replication_slots` inspection.
|
|
11
|
+
- Added immutable `SlotStatus` snapshots exposing transport and slot-health
|
|
12
|
+
fields without imposing downstream recovery policy.
|
|
13
|
+
|
|
14
|
+
## 0.2.4 - 2026-06-17
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Added socket-aware replication stream polling.
|
|
19
|
+
- Added E2E coverage for PostgreSQL restart and replication stream recovery.
|
|
20
|
+
- Added connection readiness checks to E2E infrastructure.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Improved replication stream handling during idle periods.
|
|
25
|
+
- Improved reconnect behavior after PostgreSQL restarts.
|
|
26
|
+
- Improved standby feedback reliability during long-running streams.
|
|
27
|
+
- Improved E2E test stability across PostgreSQL startup and restart scenarios.
|
|
28
|
+
- Normalized `PG#get_copy_data` idle responses to simplify stream processing.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Fixed replication stream recovery after PostgreSQL restart.
|
|
33
|
+
- Fixed handling of idle COPY stream reads.
|
|
34
|
+
- Fixed reconnect loops triggered by PostgreSQL replication timeouts.
|
|
35
|
+
- Fixed E2E race conditions during PostgreSQL initialization.
|
|
36
|
+
- Fixed replication slot creation behavior when a slot already exists.
|
|
37
|
+
- Fixed several edge cases uncovered by Mammoth integration testing.
|
|
38
|
+
|
|
39
|
+
### Documentation
|
|
40
|
+
|
|
41
|
+
- Expanded YARD documentation coverage to 98.95%.
|
|
42
|
+
|
|
5
43
|
## 0.2.3 - 2026-06-17
|
|
6
44
|
|
|
7
45
|
### Fixed
|
data/README.md
CHANGED
|
@@ -58,6 +58,7 @@ Decoded row events
|
|
|
58
58
|
- Supports `CREATE_REPLICATION_SLOT`
|
|
59
59
|
- Supports `DROP_REPLICATION_SLOT`
|
|
60
60
|
- Supports `START_REPLICATION SLOT ... LOGICAL ...`
|
|
61
|
+
- Inspects version-dependent `pg_replication_slots` state
|
|
61
62
|
- Parses XLogData envelopes
|
|
62
63
|
- Parses primary keepalive messages
|
|
63
64
|
- Builds standby feedback messages
|
|
@@ -175,6 +176,7 @@ It owns:
|
|
|
175
176
|
- Keepalive handling
|
|
176
177
|
- Standby status feedback
|
|
177
178
|
- LSN conversion
|
|
179
|
+
- Replication-slot catalog inspection
|
|
178
180
|
|
|
179
181
|
---
|
|
180
182
|
|
|
@@ -241,6 +243,45 @@ client = Pgoutput::Client::Runner.new(...)
|
|
|
241
243
|
client.start { |payload, metadata| ... }
|
|
242
244
|
```
|
|
243
245
|
|
|
246
|
+
Inspect the configured replication slot before applying a downstream recovery
|
|
247
|
+
policy:
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
status = client.slot_status
|
|
251
|
+
|
|
252
|
+
if status.nil?
|
|
253
|
+
warn "replication slot is missing"
|
|
254
|
+
else
|
|
255
|
+
puts status.restart_lsn
|
|
256
|
+
puts status.confirmed_flush_lsn
|
|
257
|
+
puts status.wal_status
|
|
258
|
+
puts status.invalidation_reason
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`SlotStatus` reflects the fields available on the connected PostgreSQL version.
|
|
263
|
+
Optional fields are `nil` on versions that do not expose them. The client
|
|
264
|
+
reports catalog state only; deciding whether a durable checkpoint is safe to
|
|
265
|
+
resume remains the downstream runtime's responsibility.
|
|
266
|
+
|
|
267
|
+
### Pgoutput::Client::SlotInspector
|
|
268
|
+
|
|
269
|
+
Queries `pg_replication_slots` through a short-lived ordinary PostgreSQL
|
|
270
|
+
connection. `#fetch(slot_name)` returns `nil` when the slot is missing and a
|
|
271
|
+
`SlotStatus` snapshot otherwise. `Runner#slot_status` is the convenient entry
|
|
272
|
+
point for inspecting the runner's configured slot.
|
|
273
|
+
|
|
274
|
+
### Pgoutput::Client::SlotStatus
|
|
275
|
+
|
|
276
|
+
Immutable replication-slot catalog snapshot. It exposes identity, activity,
|
|
277
|
+
WAL position, retention, and invalidation fields including:
|
|
278
|
+
|
|
279
|
+
- `slot_name`, `plugin`, `slot_type`, and `database`
|
|
280
|
+
- `active`, `active_pid`, and `inactive_since`
|
|
281
|
+
- `restart_lsn` and `confirmed_flush_lsn`
|
|
282
|
+
- `wal_status`, `safe_wal_size`, and `catalog_xmin`
|
|
283
|
+
- `conflicting` and `invalidation_reason`
|
|
284
|
+
|
|
244
285
|
### Pgoutput::Client::Configuration
|
|
245
286
|
|
|
246
287
|
Immutable configuration object.
|
|
@@ -363,6 +404,62 @@ Equivalent Rake task:
|
|
|
363
404
|
bundle exec rake e2e:run
|
|
364
405
|
```
|
|
365
406
|
|
|
407
|
+
## Transport lifecycle behavior
|
|
408
|
+
|
|
409
|
+
`pgoutput-client` owns PostgreSQL logical replication transport and lifecycle
|
|
410
|
+
management. It opens the replication connection, optionally creates the logical
|
|
411
|
+
replication slot, starts streaming, sends standby status feedback, and retries
|
|
412
|
+
reconnectable failures.
|
|
413
|
+
|
|
414
|
+
### Idle standby feedback
|
|
415
|
+
|
|
416
|
+
Long-running replication streams can be quiet for long periods when no WAL
|
|
417
|
+
changes are produced. During those idle periods the client wakes periodically
|
|
418
|
+
and sends standby status feedback so PostgreSQL does not terminate the walsender
|
|
419
|
+
for replication timeout.
|
|
420
|
+
|
|
421
|
+
Control the feedback cadence with `feedback_interval`:
|
|
422
|
+
|
|
423
|
+
```ruby
|
|
424
|
+
runner = Pgoutput::Client::Runner.new(
|
|
425
|
+
database_url: ENV.fetch("DATABASE_URL"),
|
|
426
|
+
slot_name: "mammoth_live",
|
|
427
|
+
publication_names: ["mammoth_publication"],
|
|
428
|
+
feedback_interval: 10.0
|
|
429
|
+
)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Idempotent automatic slot creation
|
|
433
|
+
|
|
434
|
+
When `auto_create_slot` is enabled, the client treats slot creation as
|
|
435
|
+
"ensure this slot exists". Missing slots are created before streaming; existing
|
|
436
|
+
slots are reused and do not cause startup failure.
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
runner = Pgoutput::Client::Runner.new(
|
|
440
|
+
database_url: ENV.fetch("DATABASE_URL"),
|
|
441
|
+
slot_name: "mammoth_live",
|
|
442
|
+
publication_names: ["mammoth_publication"],
|
|
443
|
+
auto_create_slot: true,
|
|
444
|
+
temporary_slot: false
|
|
445
|
+
)
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Existence alone does not prove that an existing or newly created slot can serve
|
|
449
|
+
a downstream durable checkpoint. Use `Runner#slot_status` to obtain catalog
|
|
450
|
+
state and apply continuity or recovery policy in the downstream runtime before
|
|
451
|
+
streaming.
|
|
452
|
+
|
|
453
|
+
Publication creation remains outside this gem. Create publications through
|
|
454
|
+
application migrations, database bootstrap SQL, or infrastructure tooling.
|
|
455
|
+
|
|
456
|
+
### Restart recovery
|
|
457
|
+
|
|
458
|
+
After a stream has connected successfully, transient PostgreSQL outages are
|
|
459
|
+
retried through the reconnect lifecycle. This includes ordinary container or
|
|
460
|
+
process restart windows where PostgreSQL temporarily refuses connections or
|
|
461
|
+
reports that the database system is starting up.
|
|
462
|
+
|
|
366
463
|
---
|
|
367
464
|
|
|
368
465
|
## 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
|
+
!!socket.wait_readable(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.
|
|
@@ -60,6 +70,7 @@ module Pgoutput
|
|
|
60
70
|
# {Configuration#initialize}
|
|
61
71
|
# @return [void]
|
|
62
72
|
# @raise [ConfigurationError] if the supplied configuration is invalid
|
|
73
|
+
# rubocop:disable Style/ArgumentsForwarding -- YARD needs a named parameter
|
|
63
74
|
def initialize(**options)
|
|
64
75
|
@configuration = Configuration.new(
|
|
65
76
|
**options # : untyped
|
|
@@ -74,6 +85,7 @@ module Pgoutput
|
|
|
74
85
|
@last_error = nil
|
|
75
86
|
@reconnect_attempts = 0
|
|
76
87
|
end
|
|
88
|
+
# rubocop:enable Style/ArgumentsForwarding
|
|
77
89
|
|
|
78
90
|
# Start streaming raw pgoutput payloads.
|
|
79
91
|
#
|
|
@@ -134,9 +146,9 @@ module Pgoutput
|
|
|
134
146
|
#
|
|
135
147
|
# @yield [payload, metadata] called once for each XLogData payload
|
|
136
148
|
# @return [void]
|
|
137
|
-
def restart(&
|
|
149
|
+
def restart(&)
|
|
138
150
|
stop
|
|
139
|
-
start(&
|
|
151
|
+
start(&)
|
|
140
152
|
end
|
|
141
153
|
|
|
142
154
|
# Whether the runner is currently inside its streaming loop.
|
|
@@ -191,6 +203,17 @@ module Pgoutput
|
|
|
191
203
|
)
|
|
192
204
|
end
|
|
193
205
|
|
|
206
|
+
# Inspect the configured replication slot through PostgreSQL's catalog.
|
|
207
|
+
#
|
|
208
|
+
# This reports transport state without applying downstream checkpoint or
|
|
209
|
+
# recovery policy.
|
|
210
|
+
#
|
|
211
|
+
# @return [SlotStatus, nil] current slot state, or `nil` when missing
|
|
212
|
+
# @raise [ConnectionError] when PostgreSQL cannot be queried
|
|
213
|
+
def slot_status
|
|
214
|
+
SlotInspector.new(database_url: configuration.database_url).fetch(configuration.slot_name)
|
|
215
|
+
end
|
|
216
|
+
|
|
194
217
|
private
|
|
195
218
|
|
|
196
219
|
def setup_connection(connection)
|
|
@@ -212,12 +235,12 @@ module Pgoutput
|
|
|
212
235
|
error.message.match?(/replication slot .* already exists/i)
|
|
213
236
|
end
|
|
214
237
|
|
|
215
|
-
def run_stream_cycle(configuration, &
|
|
238
|
+
def run_stream_cycle(configuration, &)
|
|
216
239
|
connection = Connection.open(configuration)
|
|
217
240
|
setup_connection(connection)
|
|
218
241
|
@connected_once = true
|
|
219
242
|
@stream = Stream.new(connection:, configuration:, acked_lsn: @acked_lsn)
|
|
220
|
-
@stream.start(&
|
|
243
|
+
@stream.start(&)
|
|
221
244
|
:done
|
|
222
245
|
rescue ConnectionError => e
|
|
223
246
|
@last_error = e
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Pgoutput
|
|
6
|
+
module Client
|
|
7
|
+
# Reads PostgreSQL replication-slot catalog state.
|
|
8
|
+
#
|
|
9
|
+
# Inspection uses a short-lived ordinary database connection because
|
|
10
|
+
# catalog queries are separate from the long-lived replication protocol
|
|
11
|
+
# connection. Querying the whole catalog row through `to_jsonb` keeps the
|
|
12
|
+
# result compatible with PostgreSQL versions that expose different optional
|
|
13
|
+
# slot-health columns.
|
|
14
|
+
#
|
|
15
|
+
# @api public
|
|
16
|
+
class SlotInspector
|
|
17
|
+
# Version-tolerant catalog query for one replication slot.
|
|
18
|
+
#
|
|
19
|
+
# @return [String]
|
|
20
|
+
QUERY = <<~SQL
|
|
21
|
+
SELECT to_jsonb(slot) AS slot
|
|
22
|
+
FROM pg_catalog.pg_replication_slots AS slot
|
|
23
|
+
WHERE slot.slot_name = $1
|
|
24
|
+
SQL
|
|
25
|
+
|
|
26
|
+
# @return [String] PostgreSQL connection URL
|
|
27
|
+
attr_reader :database_url
|
|
28
|
+
|
|
29
|
+
# @param database_url [String] PostgreSQL connection URL
|
|
30
|
+
# @param connection_factory [#call, nil] optional connection factory for tests
|
|
31
|
+
def initialize(database_url:, connection_factory: nil)
|
|
32
|
+
@database_url = database_url
|
|
33
|
+
@connection_factory = connection_factory
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Fetch the configured slot's current catalog state.
|
|
37
|
+
#
|
|
38
|
+
# @param slot_name [String] replication slot name
|
|
39
|
+
# @return [SlotStatus, nil] snapshot, or `nil` when the slot is missing
|
|
40
|
+
# @raise [ConnectionError] when PostgreSQL cannot be queried
|
|
41
|
+
def fetch(slot_name)
|
|
42
|
+
connection = open_connection
|
|
43
|
+
row = connection.exec_params(QUERY, [String(slot_name)]).first
|
|
44
|
+
return nil unless row
|
|
45
|
+
|
|
46
|
+
SlotStatus.from_catalog(parse_catalog(row.fetch("slot")))
|
|
47
|
+
rescue pg_error_class => e
|
|
48
|
+
raise ConnectionError, e.message
|
|
49
|
+
ensure
|
|
50
|
+
connection&.close unless connection&.finished?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def open_connection
|
|
56
|
+
return @connection_factory.call(database_url) if @connection_factory
|
|
57
|
+
|
|
58
|
+
require "pg"
|
|
59
|
+
PG.connect(database_url)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def parse_catalog(value)
|
|
63
|
+
return value if value.is_a?(Hash)
|
|
64
|
+
|
|
65
|
+
JSON.parse(value)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def pg_error_class
|
|
69
|
+
require "pg"
|
|
70
|
+
PG::Error
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgoutput
|
|
4
|
+
module Client
|
|
5
|
+
# Immutable snapshot of one PostgreSQL logical replication slot.
|
|
6
|
+
#
|
|
7
|
+
# Fields introduced by newer PostgreSQL versions are `nil` when the server
|
|
8
|
+
# does not expose them. The client reports transport/catalog state only;
|
|
9
|
+
# downstream runtimes remain responsible for deciding whether a checkpoint
|
|
10
|
+
# can safely resume from this slot.
|
|
11
|
+
#
|
|
12
|
+
# @api public
|
|
13
|
+
SlotStatusData = Data.define(
|
|
14
|
+
:slot_name,
|
|
15
|
+
:plugin,
|
|
16
|
+
:slot_type,
|
|
17
|
+
:database,
|
|
18
|
+
:active,
|
|
19
|
+
:active_pid,
|
|
20
|
+
:catalog_xmin,
|
|
21
|
+
:restart_lsn,
|
|
22
|
+
:confirmed_flush_lsn,
|
|
23
|
+
:wal_status,
|
|
24
|
+
:safe_wal_size,
|
|
25
|
+
:inactive_since,
|
|
26
|
+
:conflicting,
|
|
27
|
+
:invalidation_reason
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Typed replication-slot catalog snapshot.
|
|
31
|
+
#
|
|
32
|
+
# @api public
|
|
33
|
+
class SlotStatus < SlotStatusData
|
|
34
|
+
# Build a snapshot from a `pg_replication_slots` JSON object.
|
|
35
|
+
#
|
|
36
|
+
# @param attributes [Hash{String, Symbol=>Object}] catalog attributes
|
|
37
|
+
# @return [SlotStatus]
|
|
38
|
+
def self.from_catalog(attributes)
|
|
39
|
+
fetch = lambda do |key|
|
|
40
|
+
next attributes[key] if attributes.key?(key)
|
|
41
|
+
|
|
42
|
+
attributes[key.to_sym]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
new(
|
|
46
|
+
slot_name: fetch.call("slot_name"),
|
|
47
|
+
plugin: fetch.call("plugin"),
|
|
48
|
+
slot_type: fetch.call("slot_type"),
|
|
49
|
+
database: fetch.call("database"),
|
|
50
|
+
active: fetch.call("active") == true,
|
|
51
|
+
active_pid: fetch.call("active_pid"),
|
|
52
|
+
catalog_xmin: fetch.call("catalog_xmin"),
|
|
53
|
+
restart_lsn: fetch.call("restart_lsn"),
|
|
54
|
+
confirmed_flush_lsn: fetch.call("confirmed_flush_lsn"),
|
|
55
|
+
wal_status: fetch.call("wal_status"),
|
|
56
|
+
safe_wal_size: fetch.call("safe_wal_size"),
|
|
57
|
+
inactive_since: fetch.call("inactive_since"),
|
|
58
|
+
conflicting: fetch.call("conflicting"),
|
|
59
|
+
invalidation_reason: fetch.call("invalidation_reason")
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -71,7 +71,7 @@ module Pgoutput
|
|
|
71
71
|
# @raise [ProtocolError] if an unknown or malformed replication message is
|
|
72
72
|
# received
|
|
73
73
|
# @raise [ConnectionError] if standby feedback cannot be sent
|
|
74
|
-
def start(&
|
|
74
|
+
def start(&)
|
|
75
75
|
raise ArgumentError, "block required" unless block_given?
|
|
76
76
|
|
|
77
77
|
@running = true
|
|
@@ -83,7 +83,7 @@ module Pgoutput
|
|
|
83
83
|
next
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
process_copy_data(copy_data, &
|
|
86
|
+
process_copy_data(copy_data, &)
|
|
87
87
|
send_periodic_feedback
|
|
88
88
|
end
|
|
89
89
|
ensure
|
data/lib/pgoutput/client.rb
CHANGED
|
@@ -8,11 +8,20 @@ require_relative "client/xlog_data"
|
|
|
8
8
|
require_relative "client/keepalive"
|
|
9
9
|
require_relative "client/feedback"
|
|
10
10
|
require_relative "client/state"
|
|
11
|
+
require_relative "client/slot_status"
|
|
12
|
+
require_relative "client/slot_inspector"
|
|
11
13
|
require_relative "client/commands"
|
|
12
14
|
require_relative "client/connection"
|
|
13
15
|
require_relative "client/stream"
|
|
14
16
|
require_relative "client/runner"
|
|
15
17
|
|
|
18
|
+
# Namespace for PostgreSQL pgoutput logical replication components.
|
|
19
|
+
#
|
|
20
|
+
# The top-level namespace is shared by pgoutput ecosystem gems. This gem
|
|
21
|
+
# defines only the `Pgoutput::Client` transport namespace and leaves protocol
|
|
22
|
+
# parsing, value decoding, and CDC normalization to sibling libraries.
|
|
23
|
+
#
|
|
24
|
+
# @api public
|
|
16
25
|
module Pgoutput
|
|
17
26
|
# Namespace for PostgreSQL logical replication transport support.
|
|
18
27
|
#
|
data/sig/json.rbs
ADDED
|
@@ -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
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Pgoutput
|
|
2
|
+
module Client
|
|
3
|
+
class SlotInspector
|
|
4
|
+
QUERY: String
|
|
5
|
+
|
|
6
|
+
attr_reader database_url: String
|
|
7
|
+
|
|
8
|
+
def initialize: (database_url: String, ?connection_factory: untyped) -> void
|
|
9
|
+
def fetch: (String slot_name) -> (SlotStatus | nil)
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def open_connection: () -> untyped
|
|
14
|
+
def parse_catalog: (untyped value) -> Hash[(String | Symbol), untyped]
|
|
15
|
+
def pg_error_class: () -> singleton(::PG::Error)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Pgoutput
|
|
2
|
+
module Client
|
|
3
|
+
class SlotStatusData < Data
|
|
4
|
+
attr_reader slot_name: String
|
|
5
|
+
attr_reader plugin: String?
|
|
6
|
+
attr_reader slot_type: String
|
|
7
|
+
attr_reader database: String?
|
|
8
|
+
attr_reader active: bool
|
|
9
|
+
attr_reader active_pid: Integer?
|
|
10
|
+
attr_reader catalog_xmin: String?
|
|
11
|
+
attr_reader restart_lsn: String?
|
|
12
|
+
attr_reader confirmed_flush_lsn: String?
|
|
13
|
+
attr_reader wal_status: String?
|
|
14
|
+
attr_reader safe_wal_size: Integer?
|
|
15
|
+
attr_reader inactive_since: String?
|
|
16
|
+
attr_reader conflicting: bool?
|
|
17
|
+
attr_reader invalidation_reason: String?
|
|
18
|
+
|
|
19
|
+
def self.new: (
|
|
20
|
+
slot_name: String,
|
|
21
|
+
plugin: String?,
|
|
22
|
+
slot_type: String,
|
|
23
|
+
database: String?,
|
|
24
|
+
active: bool,
|
|
25
|
+
active_pid: Integer?,
|
|
26
|
+
catalog_xmin: String?,
|
|
27
|
+
restart_lsn: String?,
|
|
28
|
+
confirmed_flush_lsn: String?,
|
|
29
|
+
wal_status: String?,
|
|
30
|
+
safe_wal_size: Integer?,
|
|
31
|
+
inactive_since: String?,
|
|
32
|
+
conflicting: bool?,
|
|
33
|
+
invalidation_reason: String?
|
|
34
|
+
) -> instance
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class SlotStatus < SlotStatusData
|
|
38
|
+
def self.from_catalog: (Hash[(String | Symbol), untyped] attributes) -> SlotStatus
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
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.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
@@ -53,11 +53,14 @@ files:
|
|
|
53
53
|
- lib/pgoutput/client/keepalive.rb
|
|
54
54
|
- lib/pgoutput/client/lsn.rb
|
|
55
55
|
- lib/pgoutput/client/runner.rb
|
|
56
|
+
- lib/pgoutput/client/slot_inspector.rb
|
|
57
|
+
- lib/pgoutput/client/slot_status.rb
|
|
56
58
|
- lib/pgoutput/client/state.rb
|
|
57
59
|
- lib/pgoutput/client/stream.rb
|
|
58
60
|
- lib/pgoutput/client/version.rb
|
|
59
61
|
- lib/pgoutput/client/xlog_data.rb
|
|
60
62
|
- lib/pgoutput_client.rb
|
|
63
|
+
- sig/json.rbs
|
|
61
64
|
- sig/pg.rbs
|
|
62
65
|
- sig/pgoutput/client/commands.rbs
|
|
63
66
|
- sig/pgoutput/client/configuration.rbs
|
|
@@ -67,6 +70,8 @@ files:
|
|
|
67
70
|
- sig/pgoutput/client/keepalive.rbs
|
|
68
71
|
- sig/pgoutput/client/lsn.rbs
|
|
69
72
|
- sig/pgoutput/client/runner.rbs
|
|
73
|
+
- sig/pgoutput/client/slot_inspector.rbs
|
|
74
|
+
- sig/pgoutput/client/slot_status.rbs
|
|
70
75
|
- sig/pgoutput/client/state.rbs
|
|
71
76
|
- sig/pgoutput/client/stream.rbs
|
|
72
77
|
- sig/pgoutput/client/version.rbs
|