pgoutput-client 0.2.4 → 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 +9 -0
- data/README.md +46 -0
- data/lib/pgoutput/client/connection.rb +1 -1
- data/lib/pgoutput/client/runner.rb +17 -4
- data/lib/pgoutput/client/slot_inspector.rb +74 -0
- data/lib/pgoutput/client/slot_status.rb +64 -0
- data/lib/pgoutput/client/stream.rb +2 -2
- data/lib/pgoutput/client/version.rb +1 -1
- data/lib/pgoutput/client.rb +2 -0
- data/sig/json.rbs +3 -0
- 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,15 @@
|
|
|
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
|
+
|
|
5
14
|
## 0.2.4 - 2026-06-17
|
|
6
15
|
|
|
7
16
|
### Added
|
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.
|
|
@@ -404,6 +445,11 @@ runner = Pgoutput::Client::Runner.new(
|
|
|
404
445
|
)
|
|
405
446
|
```
|
|
406
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
|
+
|
|
407
453
|
Publication creation remains outside this gem. Create publications through
|
|
408
454
|
application migrations, database bootstrap SQL, or infrastructure tooling.
|
|
409
455
|
|
|
@@ -70,6 +70,7 @@ module Pgoutput
|
|
|
70
70
|
# {Configuration#initialize}
|
|
71
71
|
# @return [void]
|
|
72
72
|
# @raise [ConfigurationError] if the supplied configuration is invalid
|
|
73
|
+
# rubocop:disable Style/ArgumentsForwarding -- YARD needs a named parameter
|
|
73
74
|
def initialize(**options)
|
|
74
75
|
@configuration = Configuration.new(
|
|
75
76
|
**options # : untyped
|
|
@@ -84,6 +85,7 @@ module Pgoutput
|
|
|
84
85
|
@last_error = nil
|
|
85
86
|
@reconnect_attempts = 0
|
|
86
87
|
end
|
|
88
|
+
# rubocop:enable Style/ArgumentsForwarding
|
|
87
89
|
|
|
88
90
|
# Start streaming raw pgoutput payloads.
|
|
89
91
|
#
|
|
@@ -144,9 +146,9 @@ module Pgoutput
|
|
|
144
146
|
#
|
|
145
147
|
# @yield [payload, metadata] called once for each XLogData payload
|
|
146
148
|
# @return [void]
|
|
147
|
-
def restart(&
|
|
149
|
+
def restart(&)
|
|
148
150
|
stop
|
|
149
|
-
start(&
|
|
151
|
+
start(&)
|
|
150
152
|
end
|
|
151
153
|
|
|
152
154
|
# Whether the runner is currently inside its streaming loop.
|
|
@@ -201,6 +203,17 @@ module Pgoutput
|
|
|
201
203
|
)
|
|
202
204
|
end
|
|
203
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
|
+
|
|
204
217
|
private
|
|
205
218
|
|
|
206
219
|
def setup_connection(connection)
|
|
@@ -222,12 +235,12 @@ module Pgoutput
|
|
|
222
235
|
error.message.match?(/replication slot .* already exists/i)
|
|
223
236
|
end
|
|
224
237
|
|
|
225
|
-
def run_stream_cycle(configuration, &
|
|
238
|
+
def run_stream_cycle(configuration, &)
|
|
226
239
|
connection = Connection.open(configuration)
|
|
227
240
|
setup_connection(connection)
|
|
228
241
|
@connected_once = true
|
|
229
242
|
@stream = Stream.new(connection:, configuration:, acked_lsn: @acked_lsn)
|
|
230
|
-
@stream.start(&
|
|
243
|
+
@stream.start(&)
|
|
231
244
|
:done
|
|
232
245
|
rescue ConnectionError => e
|
|
233
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,6 +8,8 @@ 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"
|
data/sig/json.rbs
ADDED
|
@@ -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
|