pg-replication-protocol 0.0.3 → 0.0.5

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: 44b3fb2ad8e9ddb30400a0cf8acaf04a70c09d9ae0641e5e475bdf1394c85d5e
4
- data.tar.gz: fd766d8b6157af89d017f9dbff382e80278614fe005320c26d7eb89c4f3c4701
3
+ metadata.gz: 07bd494baf2df079a175855749b79dc41460c5903a58e3cef89f7ac1304e6e9b
4
+ data.tar.gz: 28ad5dced1a6a73c84a62ce6eae434dbde34883378555c8bac7674d477928627
5
5
  SHA512:
6
- metadata.gz: 387313faa12bfa5084ea78920f8d860b048d57050e574793e99c9b8fd922cfb60dff8f843a68ce1ba3d05dc010aa011a3b5816a9a8c703e522411c4fff48c1e9
7
- data.tar.gz: 5df28c6e5fe755a34cf1544607b6439f903a6da5c38f355d41c9d4b8e6e1dbd834a01f2d2ab6cc46d0c9abd20ba98d0fc47fcbbda57146394f962ee59c2f22e1
6
+ metadata.gz: 7bd50c203eb711090bdbb0b16d745e46472d936191ac3c3653138bc9db42cecd6b1ed51a625eeae5194be3ca17c66e6f8a05dd4a860979a791d4fa54eee74139
7
+ data.tar.gz: 3ddaf038c9cdfab3c05c4ac2e6eac8c1799c7a0b1d334a4657b19a9251feeac835784bbdec5a94fba43260f50b932441cd49786f1f573c7a607687a742ef8eb4
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pg-replication-protocol (0.0.2)
4
+ pg-replication-protocol (0.0.5)
5
5
  pg (~> 1.0)
6
6
 
7
7
  GEM
@@ -34,6 +34,7 @@ GEM
34
34
  testcontainers-core (~> 0.1)
35
35
 
36
36
  PLATFORMS
37
+ arm64-darwin-23
37
38
  x86_64-linux
38
39
 
39
40
  DEPENDENCIES
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # PG::Replication
2
2
 
3
+ Adds support to `pg` for listening to replication slots
4
+
3
5
  ## Usage
4
6
 
5
7
  Add to your Gemfile:
@@ -14,7 +16,7 @@ gem "pg-replication-protocol", require: "pg/replication"
14
16
  require "pg"
15
17
  require "pg/replication"
16
18
 
17
- # Important to create a connection with the `replication: "database"` flag
19
+ # It is important to create a connection with the `replication: "database"` option
18
20
  connection = PG.connect(..., replication: "database")
19
21
 
20
22
  # Create a publication and a slot (in a real use case the slot will not be temporary)
@@ -25,25 +27,26 @@ connection.query('CREATE_REPLICATION_SLOT some_slot TEMPORARY LOGICAL "pgoutput"
25
27
  tables = {}
26
28
 
27
29
  # Start a pgoutput plugin replication slot message stream
28
- connection.start_pgoutput_replication_slot(slot, publications).each do |msg|
30
+ # The `messages: true` option is required to be able to decode `PG::Replication::PGOutput::Message`
31
+ connection.start_pgoutput_replication_slot(slot, publications, messages: true).each do |msg|
29
32
  case msg
30
- in PG::Replication::PGOutput::Relation(oid:, name:, columns:)
31
- # We receive this message on the first row of each table
33
+ in PG::Replication::Protocol::XLogData(data: PG::Replication::PGOutput::Relation(oid:, name:, columns:))
34
+ # This message is received on the first row of each table, or when there are schema changes
32
35
  tables[oid] = { name:, columns: }
33
36
 
34
- in PG::Replication::PGOutput::Begin
37
+ in PG::Replication::Protocol::XLogData(data: PG::Replication::PGOutput::Begin)
35
38
  puts "Transaction start"
36
39
 
37
- in PG::Replication::PGOutput::Commit
40
+ in PG::Replication::Protocol::XLogData(data: PG::Replication::PGOutput::Commit)
38
41
  puts "Transaction end"
39
42
 
40
- in PG::Replication::PGOutput::Insert(oid:, new:)
43
+ in PG::Replication::Protocol::XLogData(data: PG::Replication::PGOutput::Insert(oid:, new:))
41
44
  puts "Insert #{tables[oid][:name]}"
42
45
  new.zip(tables[oid][:columns]).each do |tuple, col|
43
46
  puts "#{col.name}: #{tuple.data || "NULL"}"
44
47
  end
45
48
 
46
- in PG::Replication::PGOutput::Update(oid:, new:, old:)
49
+ in PG::Replication::Protocol::XLogData(data: PG::Replication::PGOutput::Update(oid:, new:, old:))
47
50
  puts "Update #{tables[oid][:name]}"
48
51
  if !old.empty? && new != old
49
52
  new.zip(old, tables[oid][:columns]).each do |new, old, col|
@@ -53,9 +56,12 @@ connection.start_pgoutput_replication_slot(slot, publications).each do |msg|
53
56
  end
54
57
  end
55
58
 
56
- in PG::Replication::PGOutput::Delete(oid:)
59
+ in PG::Replication::Protocol::XLogData(data: PG::Replication::PGOutput::Delete(oid:))
57
60
  puts "Delete #{tables[oid][:name]}"
58
61
 
62
+ in PG::Replication::Protocol::XLogData(data: PG::Replication::PGOutput::Message(prefix:, content:))
63
+ puts "Message #{prefix}: #{content}"
64
+
59
65
  else
60
66
  nil
61
67
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module PG
4
4
  module Replication
5
- VERSION = "0.0.3"
5
+ VERSION = "0.0.5"
6
6
  end
7
7
  end
@@ -8,10 +8,9 @@ require_relative "replication/protocol"
8
8
 
9
9
  module PG
10
10
  module Replication
11
- def start_replication_slot(slot, logical: true, location: "0/0", **params)
12
- keep_alive_secs = query(<<~SQL).getvalue(0, 0)&.to_i || 10
13
- SELECT setting FROM pg_catalog.pg_settings WHERE name = 'wal_receiver_status_interval'
14
- SQL
11
+ def start_replication_slot(slot, logical: true, auto_keep_alive: true, location: "0/0", **params)
12
+ keep_alive_secs = wal_receiver_status_interval
13
+ @last_confirmed_lsn = confirmed_slot_lsn(slot) || 0
15
14
 
16
15
  start_query = "START_REPLICATION SLOT #{slot} #{logical ? "LOGICAL" : "PHYSICAL"} #{location}"
17
16
  unless params.empty?
@@ -23,14 +22,13 @@ module PG
23
22
  end
24
23
  query(start_query)
25
24
 
26
- last_processed_lsn = 0
27
25
  last_keep_alive = Time.now
28
26
 
29
27
  Enumerator
30
28
  .new do |y|
31
29
  loop do
32
- if Time.now - last_keep_alive > keep_alive_secs
33
- standby_status_update(write_lsn: last_processed_lsn)
30
+ if auto_keep_alive && Time.now - last_keep_alive > keep_alive_secs
31
+ standby_status_update(write_lsn: @last_confirmed_lsn)
34
32
  last_keep_alive = Time.now
35
33
  end
36
34
 
@@ -47,37 +45,40 @@ module PG
47
45
  next
48
46
 
49
47
  in data
50
- buffer = Buffer.new(StringIO.new(data))
51
- y << Protocol.read_message(buffer)
48
+ case (msg = Protocol.read_message(Buffer.new(StringIO.new(data))))
49
+ in Protocol::XLogData(lsn:, data:) if auto_keep_alive
50
+ y << msg
51
+ standby_status_update(write_lsn: lsn)
52
+ @last_confirmed_lsn = lsn
53
+ last_keep_alive = Time.now
54
+
55
+ in Protocol::PrimaryKeepalive(server_time:, asap: true) if auto_keep_alive
56
+ standby_status_update(write_lsn: @last_confirmed_lsn)
57
+ last_keep_alive = Time.now
58
+ y << msg
59
+
60
+ else
61
+ y << msg
62
+ end
52
63
  end
53
64
  end
54
65
  end
55
66
  .lazy
56
- .filter_map do |msg|
57
- case msg
58
- in Protocol::XLogData(lsn:, data:)
59
- last_processed_lsn = lsn
60
- standby_status_update(write_lsn: last_processed_lsn)
61
- last_keep_alive = Time.now
62
- data
63
-
64
- in Protocol::PrimaryKeepalive(server_time:, asap: true)
65
- standby_status_update(write_lsn: last_processed_lsn)
66
- last_keep_alive = Time.now
67
- next
68
-
69
- else
70
- next
71
- end
72
- end
73
67
  end
74
68
 
75
69
  def start_pgoutput_replication_slot(slot, publication_names, **kwargs)
76
70
  publication_names = publication_names.join(",")
77
71
 
78
72
  start_replication_slot(slot, **kwargs.merge(proto_version: "1", publication_names:))
79
- .map { |data| data.force_encoding(internal_encoding) }
80
- .map { |data| PGOutput.read_message(Buffer.from_string(data)) }
73
+ .map do |msg|
74
+ case msg
75
+ in Protocol::XLogData(data:, lsn:)
76
+ data = data.force_encoding(internal_encoding)
77
+ msg.with(data: PGOutput.read_message(Buffer.from_string(data)))
78
+ else
79
+ msg
80
+ end
81
+ end
81
82
  end
82
83
 
83
84
  def standby_status_update(
@@ -98,6 +99,27 @@ module PG
98
99
 
99
100
  put_copy_data(msg)
100
101
  flush
102
+ @last_confirmed_lsn = [@last_confirmed_lsn, write_lsn].compact.max
103
+ end
104
+
105
+ def last_confirmed_lsn
106
+ @last_confirmed_lsn
107
+ end
108
+
109
+ def wal_receiver_status_interval
110
+ query(<<~SQL).getvalue(0, 0)&.to_i || 10
111
+ SELECT setting FROM pg_catalog.pg_settings WHERE name = 'wal_receiver_status_interval'
112
+ SQL
113
+ end
114
+
115
+ def confirmed_slot_lsn(slot)
116
+ lsn = query(<<~SQL).getvalue(0, 0)
117
+ SELECT confirmed_flush_lsn FROM pg_replication_slots WHERE slot_name = '#{slot}'
118
+ SQL
119
+ high, low = lsn.split("/")
120
+ (high.to_i(16) << 32) + low.to_i(16)
121
+ rescue StandardError
122
+ nil
101
123
  end
102
124
  end
103
125
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg-replication-protocol
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Navarro
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-11-29 00:00:00.000000000 Z
10
+ date: 2025-06-04 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: pg
@@ -24,7 +23,6 @@ dependencies:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
25
  version: '1.0'
27
- description:
28
26
  email:
29
27
  - rnavarro@rnavarro.com.br
30
28
  executables: []
@@ -43,13 +41,11 @@ files:
43
41
  - lib/pg/replication/protocol.rb
44
42
  - lib/pg/replication/version.rb
45
43
  - pg-replication-protocol.gemspec
46
- homepage:
47
44
  licenses:
48
45
  - MIT
49
46
  metadata:
50
47
  homepage_uri: https://github.com/reu/pg-replication-protocol-rb
51
48
  source_code_uri: https://github.com/reu/pg-replication-protocol-rb
52
- post_install_message:
53
49
  rdoc_options: []
54
50
  require_paths:
55
51
  - lib
@@ -64,8 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
60
  - !ruby/object:Gem::Version
65
61
  version: '0'
66
62
  requirements: []
67
- rubygems_version: 3.4.1
68
- signing_key:
63
+ rubygems_version: 3.6.2
69
64
  specification_version: 4
70
65
  summary: Postgres replication protocol
71
66
  test_files: []