pg-replication-protocol 0.0.1 → 0.0.3
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/.rspec +1 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +26 -1
- data/README.md +63 -1
- data/lib/pg/replication/buffer.rb +7 -19
- data/lib/pg/replication/pg_output.rb +32 -24
- data/lib/pg/replication/protocol.rb +1 -47
- data/lib/pg/replication/version.rb +1 -1
- data/lib/pg/replication.rb +105 -0
- metadata +4 -3
- data/lib/pg/replication/stream.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 44b3fb2ad8e9ddb30400a0cf8acaf04a70c09d9ae0641e5e475bdf1394c85d5e
|
4
|
+
data.tar.gz: fd766d8b6157af89d017f9dbff382e80278614fe005320c26d7eb89c4f3c4701
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 387313faa12bfa5084ea78920f8d860b048d57050e574793e99c9b8fd922cfb60dff8f843a68ce1ba3d05dc010aa011a3b5816a9a8c703e522411c4fff48c1e9
|
7
|
+
data.tar.gz: 5df28c6e5fe755a34cf1544607b6439f903a6da5c38f355d41c9d4b8e6e1dbd834a01f2d2ab6cc46d0c9abd20ba98d0fc47fcbbda57146394f962ee59c2f22e1
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,14 +1,37 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pg-replication-protocol (0.0.
|
4
|
+
pg-replication-protocol (0.0.2)
|
5
5
|
pg (~> 1.0)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
+
diff-lcs (1.5.1)
|
11
|
+
docker-api (2.4.0)
|
12
|
+
excon (>= 0.64.0)
|
13
|
+
multi_json
|
14
|
+
excon (1.2.1)
|
15
|
+
multi_json (1.15.0)
|
10
16
|
pg (1.5.9)
|
11
17
|
rake (13.2.1)
|
18
|
+
rspec (3.13.0)
|
19
|
+
rspec-core (~> 3.13.0)
|
20
|
+
rspec-expectations (~> 3.13.0)
|
21
|
+
rspec-mocks (~> 3.13.0)
|
22
|
+
rspec-core (3.13.2)
|
23
|
+
rspec-support (~> 3.13.0)
|
24
|
+
rspec-expectations (3.13.3)
|
25
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
26
|
+
rspec-support (~> 3.13.0)
|
27
|
+
rspec-mocks (3.13.2)
|
28
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
29
|
+
rspec-support (~> 3.13.0)
|
30
|
+
rspec-support (3.13.1)
|
31
|
+
testcontainers-core (0.2.0)
|
32
|
+
docker-api (~> 2.2)
|
33
|
+
testcontainers-postgres (0.2.0)
|
34
|
+
testcontainers-core (~> 0.1)
|
12
35
|
|
13
36
|
PLATFORMS
|
14
37
|
x86_64-linux
|
@@ -16,6 +39,8 @@ PLATFORMS
|
|
16
39
|
DEPENDENCIES
|
17
40
|
pg-replication-protocol!
|
18
41
|
rake (~> 13.0)
|
42
|
+
rspec (~> 3.0)
|
43
|
+
testcontainers-postgres
|
19
44
|
|
20
45
|
BUNDLED WITH
|
21
46
|
2.4.1
|
data/README.md
CHANGED
@@ -1 +1,63 @@
|
|
1
|
-
#
|
1
|
+
# PG::Replication
|
2
|
+
|
3
|
+
## Usage
|
4
|
+
|
5
|
+
Add to your Gemfile:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem "pg-replication-protocol", require: "pg/replication"
|
9
|
+
```
|
10
|
+
|
11
|
+
## Demo
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
require "pg"
|
15
|
+
require "pg/replication"
|
16
|
+
|
17
|
+
# Important to create a connection with the `replication: "database"` flag
|
18
|
+
connection = PG.connect(..., replication: "database")
|
19
|
+
|
20
|
+
# Create a publication and a slot (in a real use case the slot will not be temporary)
|
21
|
+
connection.query("CREATE PUBLICATION some_publication FOR ALL TABLES")
|
22
|
+
connection.query('CREATE_REPLICATION_SLOT some_slot TEMPORARY LOGICAL "pgoutput"')
|
23
|
+
|
24
|
+
# Just a storage for our table relation data
|
25
|
+
tables = {}
|
26
|
+
|
27
|
+
# Start a pgoutput plugin replication slot message stream
|
28
|
+
connection.start_pgoutput_replication_slot(slot, publications).each do |msg|
|
29
|
+
case msg
|
30
|
+
in PG::Replication::PGOutput::Relation(oid:, name:, columns:)
|
31
|
+
# We receive this message on the first row of each table
|
32
|
+
tables[oid] = { name:, columns: }
|
33
|
+
|
34
|
+
in PG::Replication::PGOutput::Begin
|
35
|
+
puts "Transaction start"
|
36
|
+
|
37
|
+
in PG::Replication::PGOutput::Commit
|
38
|
+
puts "Transaction end"
|
39
|
+
|
40
|
+
in PG::Replication::PGOutput::Insert(oid:, new:)
|
41
|
+
puts "Insert #{tables[oid][:name]}"
|
42
|
+
new.zip(tables[oid][:columns]).each do |tuple, col|
|
43
|
+
puts "#{col.name}: #{tuple.data || "NULL"}"
|
44
|
+
end
|
45
|
+
|
46
|
+
in PG::Replication::PGOutput::Update(oid:, new:, old:)
|
47
|
+
puts "Update #{tables[oid][:name]}"
|
48
|
+
if !old.empty? && new != old
|
49
|
+
new.zip(old, tables[oid][:columns]).each do |new, old, col|
|
50
|
+
if new != old
|
51
|
+
puts "Changed #{col.name}: #{old.data || "NULL"} > #{new.data || "NULL"}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
in PG::Replication::PGOutput::Delete(oid:)
|
57
|
+
puts "Delete #{tables[oid][:name]}"
|
58
|
+
|
59
|
+
else
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
@@ -1,19 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "delegate"
|
2
4
|
require "stringio"
|
3
5
|
|
4
6
|
module PG
|
5
7
|
module Replication
|
6
|
-
class Buffer
|
7
|
-
attr_reader :buffer
|
8
|
-
|
8
|
+
class Buffer < SimpleDelegator
|
9
9
|
def self.from_string(str)
|
10
10
|
new(StringIO.new(str))
|
11
11
|
end
|
12
12
|
|
13
|
-
def initialize(buffer)
|
14
|
-
@buffer = buffer
|
15
|
-
end
|
16
|
-
|
17
13
|
def read_char
|
18
14
|
read_int8.chr
|
19
15
|
end
|
@@ -23,19 +19,19 @@ module PG
|
|
23
19
|
end
|
24
20
|
|
25
21
|
def read_int8
|
26
|
-
|
22
|
+
read(1).unpack("C").first
|
27
23
|
end
|
28
24
|
|
29
25
|
def read_int16
|
30
|
-
|
26
|
+
read(2).unpack("n").first
|
31
27
|
end
|
32
28
|
|
33
29
|
def read_int32
|
34
|
-
|
30
|
+
read(4).unpack("N").first
|
35
31
|
end
|
36
32
|
|
37
33
|
def read_int64
|
38
|
-
|
34
|
+
read(8).unpack("Q>").first
|
39
35
|
end
|
40
36
|
|
41
37
|
def read_timestamp
|
@@ -54,14 +50,6 @@ module PG
|
|
54
50
|
end
|
55
51
|
end
|
56
52
|
end
|
57
|
-
|
58
|
-
def read(size = nil)
|
59
|
-
@buffer.read(size)
|
60
|
-
end
|
61
|
-
|
62
|
-
def eof?
|
63
|
-
@buffer.eof?
|
64
|
-
end
|
65
53
|
end
|
66
54
|
end
|
67
55
|
end
|
@@ -11,10 +11,14 @@ module PG
|
|
11
11
|
Type = Data.define(:oid, :namespace, :name)
|
12
12
|
Insert = Data.define(:oid, :new)
|
13
13
|
Update = Data.define(:oid, :key, :old, :new)
|
14
|
-
Delete = Data.define(:oid, :old)
|
14
|
+
Delete = Data.define(:oid, :key, :old)
|
15
15
|
Truncate = Data.define(:oid)
|
16
16
|
Tuple = Data.define(:type, :data)
|
17
|
-
Column = Data.define(:flags, :name, :oid, :modifier)
|
17
|
+
Column = Data.define(:flags, :name, :oid, :modifier) do
|
18
|
+
def key?
|
19
|
+
flags == 1
|
20
|
+
end
|
21
|
+
end
|
18
22
|
|
19
23
|
def self.read_message(buffer)
|
20
24
|
case buffer.read_char
|
@@ -39,7 +43,7 @@ module PG
|
|
39
43
|
)
|
40
44
|
|
41
45
|
in "C"
|
42
|
-
buffer.read_int8 # Unused
|
46
|
+
buffer.read_int8 # Unused byte
|
43
47
|
PGOutput::Commit.new(
|
44
48
|
lsn: buffer.read_int64,
|
45
49
|
end_lsn: buffer.read_int64,
|
@@ -55,21 +59,16 @@ module PG
|
|
55
59
|
in "R"
|
56
60
|
PGOutput::Relation.new(
|
57
61
|
oid: buffer.read_int32,
|
58
|
-
namespace: buffer.read_cstring,
|
62
|
+
namespace: buffer.read_cstring.then { |ns| ns == "" ? "pg_catalog" : ns },
|
59
63
|
name: buffer.read_cstring,
|
60
|
-
replica_identity: buffer.
|
61
|
-
columns:
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
name: buffer.read_cstring,
|
69
|
-
oid: buffer.read_int32,
|
70
|
-
modifier: buffer.read_int32,
|
71
|
-
)
|
72
|
-
end
|
64
|
+
replica_identity: buffer.read_char,
|
65
|
+
columns: buffer.read_int16.times.map do
|
66
|
+
PGOutput::Column.new(
|
67
|
+
flags: buffer.read_int8,
|
68
|
+
name: buffer.read_cstring,
|
69
|
+
oid: buffer.read_int32,
|
70
|
+
modifier: buffer.read_int32,
|
71
|
+
)
|
73
72
|
end
|
74
73
|
)
|
75
74
|
|
@@ -116,14 +115,23 @@ module PG
|
|
116
115
|
)
|
117
116
|
|
118
117
|
in "D"
|
118
|
+
oid = buffer.read_int32
|
119
|
+
key = []
|
120
|
+
old = []
|
121
|
+
|
122
|
+
until buffer.eof?
|
123
|
+
case buffer.read_char
|
124
|
+
when "K"
|
125
|
+
key = PGOutput.read_tuples(buffer)
|
126
|
+
when "O"
|
127
|
+
old = PGOutput.read_tuples(buffer)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
119
131
|
PGOutput::Delete.new(
|
120
|
-
oid
|
121
|
-
|
122
|
-
|
123
|
-
PGOutput.read_tuples(buffer)
|
124
|
-
else
|
125
|
-
[]
|
126
|
-
end,
|
132
|
+
oid:,
|
133
|
+
key:,
|
134
|
+
old:,
|
127
135
|
)
|
128
136
|
|
129
137
|
in "T"
|
@@ -1,11 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "pg"
|
4
|
-
require_relative "version"
|
5
|
-
require_relative "buffer"
|
6
|
-
require_relative "pg_output"
|
7
|
-
require_relative "stream"
|
8
|
-
|
9
3
|
module PG
|
10
4
|
module Replication
|
11
5
|
module Protocol
|
@@ -26,10 +20,7 @@ module PG
|
|
26
20
|
lsn: buffer.read_int64,
|
27
21
|
current_lsn: buffer.read_int64,
|
28
22
|
server_time: buffer.read_timestamp,
|
29
|
-
data: buffer
|
30
|
-
.read
|
31
|
-
.then { |msg| Buffer.from_string(msg) }
|
32
|
-
.then { |msg| PGOutput.read_message(msg) },
|
23
|
+
data: buffer.read,
|
33
24
|
)
|
34
25
|
|
35
26
|
in unknown
|
@@ -37,42 +28,5 @@ module PG
|
|
37
28
|
end
|
38
29
|
end
|
39
30
|
end
|
40
|
-
|
41
|
-
def start_pgoutput_replication_slot(slot, publications, location: "0/0", keep_alive_time: 10)
|
42
|
-
publications = Array(publications)
|
43
|
-
.map { |name| quote_ident(name) }
|
44
|
-
.map { |name| "'#{name}'" }
|
45
|
-
.join(",")
|
46
|
-
|
47
|
-
query(<<~SQL)
|
48
|
-
START_REPLICATION SLOT
|
49
|
-
#{slot} LOGICAL #{location}
|
50
|
-
("proto_version" '1', "publication_names" #{publications})
|
51
|
-
SQL
|
52
|
-
|
53
|
-
last_keep_alive = Time.now
|
54
|
-
last_processed_lsn = 0
|
55
|
-
|
56
|
-
stream = PG::Replication::Stream.new(self)
|
57
|
-
stream.lazy.map do |msg|
|
58
|
-
case msg
|
59
|
-
in PG::Replication::Protocol::XLogData(lsn:, data:)
|
60
|
-
last_processed_lsn = lsn
|
61
|
-
data
|
62
|
-
in PG::Replication::Protocol::PrimaryKeepalive(server_time:, asap: true)
|
63
|
-
stream.standby_status_update(write_lsn: last_processed_lsn)
|
64
|
-
next
|
65
|
-
in PG::Replication::Protocol::PrimaryKeepalive(server_time:)
|
66
|
-
now = Time.now
|
67
|
-
if now - last_keep_alive > keep_alive_time
|
68
|
-
stream.standby_status_update(write_lsn: last_processed_lsn)
|
69
|
-
last_keep_alive = now
|
70
|
-
end
|
71
|
-
next
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
31
|
end
|
76
|
-
|
77
|
-
Connection.send(:include, Replication)
|
78
32
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pg"
|
4
|
+
require_relative "replication/version"
|
5
|
+
require_relative "replication/buffer"
|
6
|
+
require_relative "replication/pg_output"
|
7
|
+
require_relative "replication/protocol"
|
8
|
+
|
9
|
+
module PG
|
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
|
15
|
+
|
16
|
+
start_query = "START_REPLICATION SLOT #{slot} #{logical ? "LOGICAL" : "PHYSICAL"} #{location}"
|
17
|
+
unless params.empty?
|
18
|
+
start_query << "("
|
19
|
+
start_query << params
|
20
|
+
.map { |k, v| "#{quote_ident(k.to_s)} '#{escape_string(v.to_s)}'" }
|
21
|
+
.join(", ")
|
22
|
+
start_query << ")"
|
23
|
+
end
|
24
|
+
query(start_query)
|
25
|
+
|
26
|
+
last_processed_lsn = 0
|
27
|
+
last_keep_alive = Time.now
|
28
|
+
|
29
|
+
Enumerator
|
30
|
+
.new do |y|
|
31
|
+
loop do
|
32
|
+
if Time.now - last_keep_alive > keep_alive_secs
|
33
|
+
standby_status_update(write_lsn: last_processed_lsn)
|
34
|
+
last_keep_alive = Time.now
|
35
|
+
end
|
36
|
+
|
37
|
+
consume_input
|
38
|
+
next if is_busy
|
39
|
+
|
40
|
+
case get_copy_data(async: true)
|
41
|
+
in nil
|
42
|
+
get_last_result
|
43
|
+
break
|
44
|
+
|
45
|
+
in false
|
46
|
+
IO.select([socket_io], nil, nil, keep_alive_secs)
|
47
|
+
next
|
48
|
+
|
49
|
+
in data
|
50
|
+
buffer = Buffer.new(StringIO.new(data))
|
51
|
+
y << Protocol.read_message(buffer)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
.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
|
+
end
|
74
|
+
|
75
|
+
def start_pgoutput_replication_slot(slot, publication_names, **kwargs)
|
76
|
+
publication_names = publication_names.join(",")
|
77
|
+
|
78
|
+
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)) }
|
81
|
+
end
|
82
|
+
|
83
|
+
def standby_status_update(
|
84
|
+
write_lsn:,
|
85
|
+
flush_lsn: write_lsn,
|
86
|
+
apply_lsn: write_lsn,
|
87
|
+
timestamp: Time.now,
|
88
|
+
reply: false
|
89
|
+
)
|
90
|
+
msg = [
|
91
|
+
"r".bytes.first,
|
92
|
+
write_lsn,
|
93
|
+
flush_lsn,
|
94
|
+
apply_lsn,
|
95
|
+
(timestamp - Time.new(2_000, 1, 1, 0, 0, 0, 0)) * 10**6,
|
96
|
+
reply ? 1 : 0,
|
97
|
+
].pack("CQ>Q>Q>Q>C")
|
98
|
+
|
99
|
+
put_copy_data(msg)
|
100
|
+
flush
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
Connection.send(:include, Replication)
|
105
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg-replication-protocol
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rodrigo Navarro
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-11-
|
11
|
+
date: 2024-11-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pg
|
@@ -31,15 +31,16 @@ executables: []
|
|
31
31
|
extensions: []
|
32
32
|
extra_rdoc_files: []
|
33
33
|
files:
|
34
|
+
- ".rspec"
|
34
35
|
- Gemfile
|
35
36
|
- Gemfile.lock
|
36
37
|
- LICENSE.txt
|
37
38
|
- README.md
|
38
39
|
- Rakefile
|
40
|
+
- lib/pg/replication.rb
|
39
41
|
- lib/pg/replication/buffer.rb
|
40
42
|
- lib/pg/replication/pg_output.rb
|
41
43
|
- lib/pg/replication/protocol.rb
|
42
|
-
- lib/pg/replication/stream.rb
|
43
44
|
- lib/pg/replication/version.rb
|
44
45
|
- pg-replication-protocol.gemspec
|
45
46
|
homepage:
|
@@ -1,53 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module PG
|
4
|
-
module Replication
|
5
|
-
class Stream
|
6
|
-
include Enumerable
|
7
|
-
|
8
|
-
def initialize(connection)
|
9
|
-
@connection = connection
|
10
|
-
end
|
11
|
-
|
12
|
-
def each
|
13
|
-
loop do
|
14
|
-
@connection.consume_input
|
15
|
-
|
16
|
-
next if @connection.is_busy
|
17
|
-
|
18
|
-
case @connection.get_copy_data(async: true)
|
19
|
-
in nil
|
20
|
-
@connection.get_last_result
|
21
|
-
break
|
22
|
-
in false
|
23
|
-
IO.select([@connection.socket_io], nil, nil)
|
24
|
-
next
|
25
|
-
in data
|
26
|
-
buffer = Buffer.new(StringIO.new(data))
|
27
|
-
yield Protocol.read_message(buffer)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def standby_status_update(
|
33
|
-
write_lsn:,
|
34
|
-
flush_lsn: write_lsn,
|
35
|
-
apply_lsn: write_lsn,
|
36
|
-
timestamp: Time.now,
|
37
|
-
reply: false
|
38
|
-
)
|
39
|
-
msg = [
|
40
|
-
"r".bytes.first,
|
41
|
-
write_lsn,
|
42
|
-
flush_lsn,
|
43
|
-
apply_lsn,
|
44
|
-
(timestamp - Time.new(2000, 1, 1, 0, 0, 0, 0)) * 10**6,
|
45
|
-
reply ? 1 : 0,
|
46
|
-
].pack("CQ>Q>Q>Q>C")
|
47
|
-
|
48
|
-
@connection.put_copy_data(msg)
|
49
|
-
@connection.flush
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|