pg-replication-protocol 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b723f4c6b03faf3c0ef5a638da7c08d4d65e6a0b723d4a7f00578333705803fc
4
+ data.tar.gz: 87577bceaff85726653296a50fd508113be910ecbe305c120d86c12f05ae1276
5
+ SHA512:
6
+ metadata.gz: 7693f24a4bee5fd0fdf232c18a126fc9f1728a2bad982c4225ebaa7ec7452158a1ec4ffe95f27b7793d7d2184cdeba6d8eb0561c61984c3687eb67d4ad3715b0
7
+ data.tar.gz: 97e43186afd2876c52dbaa7d0e8b9235bbdddcc9edcb0e5a5cede9d69617efb08040468de0dcdfbc13966ee93fedcfd6bd9387edb8c63ec173df4622dd3ca6cf
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "rake", "~> 13.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,21 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pg-replication-protocol (0.0.1)
5
+ pg (~> 1.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ pg (1.5.9)
11
+ rake (13.2.1)
12
+
13
+ PLATFORMS
14
+ x86_64-linux
15
+
16
+ DEPENDENCIES
17
+ pg-replication-protocol!
18
+ rake (~> 13.0)
19
+
20
+ BUNDLED WITH
21
+ 2.4.1
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Rodrigo Navarro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1 @@
1
+ # Pg::Replication::Protocol
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+ require "stringio"
3
+
4
+ module PG
5
+ module Replication
6
+ class Buffer
7
+ attr_reader :buffer
8
+
9
+ def self.from_string(str)
10
+ new(StringIO.new(str))
11
+ end
12
+
13
+ def initialize(buffer)
14
+ @buffer = buffer
15
+ end
16
+
17
+ def read_char
18
+ read_int8.chr
19
+ end
20
+
21
+ def read_bool
22
+ read_int8 == 1
23
+ end
24
+
25
+ def read_int8
26
+ @buffer.read(1).unpack("C").first
27
+ end
28
+
29
+ def read_int16
30
+ @buffer.read(2).unpack("n").first
31
+ end
32
+
33
+ def read_int32
34
+ @buffer.read(4).unpack("N").first
35
+ end
36
+
37
+ def read_int64
38
+ @buffer.read(8).unpack("Q>").first
39
+ end
40
+
41
+ def read_timestamp
42
+ usecs = Time.new(2_000, 1, 1, 0, 0, 0, 0).to_i * 10**6 + read_int64
43
+ Time.at(usecs / 10**6, usecs % 10**6, :microsecond)
44
+ end
45
+
46
+ def read_cstring
47
+ str = String.new
48
+ loop do
49
+ case read_char
50
+ in "\0"
51
+ return str
52
+ in chr
53
+ str << chr
54
+ end
55
+ end
56
+ end
57
+
58
+ def read(size = nil)
59
+ @buffer.read(size)
60
+ end
61
+
62
+ def eof?
63
+ @buffer.eof?
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PG
4
+ module Replication
5
+ module PGOutput
6
+ Begin = Data.define(:final_lsn, :timestamp, :xid)
7
+ Message = Data.define(:transactional, :lsn, :prefix, :content)
8
+ Commit = Data.define(:lsn, :end_lsn, :timestamp)
9
+ Origin = Data.define(:commit_lsn, :name)
10
+ Relation = Data.define(:oid, :namespace, :name, :replica_identity, :columns)
11
+ Type = Data.define(:oid, :namespace, :name)
12
+ Insert = Data.define(:oid, :new)
13
+ Update = Data.define(:oid, :key, :old, :new)
14
+ Delete = Data.define(:oid, :old)
15
+ Truncate = Data.define(:oid)
16
+ Tuple = Data.define(:type, :data)
17
+ Column = Data.define(:flags, :name, :oid, :modifier)
18
+
19
+ def self.read_message(buffer)
20
+ case buffer.read_char
21
+ in "B"
22
+ PGOutput::Begin.new(
23
+ final_lsn: buffer.read_int64,
24
+ timestamp: buffer.read_timestamp,
25
+ xid: buffer.read_int32,
26
+ )
27
+
28
+ in "M"
29
+ PGOutput::Message.new(
30
+ transactional: buffer.read_bool,
31
+ lsn: buffer.read_int64,
32
+ prefix: buffer.read_cstring,
33
+ content: case buffer.read_int32
34
+ in 0
35
+ nil
36
+ in n
37
+ buffer.read(n)
38
+ end,
39
+ )
40
+
41
+ in "C"
42
+ buffer.read_int8 # Unused bytes
43
+ PGOutput::Commit.new(
44
+ lsn: buffer.read_int64,
45
+ end_lsn: buffer.read_int64,
46
+ timestamp: buffer.read_timestamp,
47
+ )
48
+
49
+ in "O"
50
+ PGOutput::Origin.new(
51
+ commit_lsn: buffer.read_int64,
52
+ name: buffer.read_cstring,
53
+ )
54
+
55
+ in "R"
56
+ PGOutput::Relation.new(
57
+ oid: buffer.read_int32,
58
+ namespace: buffer.read_cstring,
59
+ name: buffer.read_cstring,
60
+ replica_identity: buffer.read_int8,
61
+ columns: case buffer.read_int16
62
+ in 0
63
+ []
64
+ in size
65
+ size.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
+ )
72
+ end
73
+ end
74
+ )
75
+
76
+ in "Y"
77
+ PGOutput::Type.new(
78
+ oid: buffer.read_int32,
79
+ namespace: buffer.read_cstring,
80
+ name: buffer.read_cstring,
81
+ )
82
+
83
+ in "I"
84
+ PGOutput::Insert.new(
85
+ oid: buffer.read_int32,
86
+ new: case a = buffer.read_char
87
+ when "N"
88
+ PGOutput.read_tuples(buffer)
89
+ else
90
+ []
91
+ end,
92
+ )
93
+
94
+ in "U"
95
+ oid = buffer.read_int32
96
+ key = []
97
+ new = []
98
+ old = []
99
+
100
+ until buffer.eof?
101
+ case buffer.read_char
102
+ when "K"
103
+ key = PGOutput.read_tuples(buffer)
104
+ when "N"
105
+ new = PGOutput.read_tuples(buffer)
106
+ when "O"
107
+ old = PGOutput.read_tuples(buffer)
108
+ end
109
+ end
110
+
111
+ PGOutput::Update.new(
112
+ oid:,
113
+ key:,
114
+ old:,
115
+ new:,
116
+ )
117
+
118
+ in "D"
119
+ PGOutput::Delete.new(
120
+ oid: buffer.read_int32,
121
+ old: case buffer.read_char
122
+ when "N", "K"
123
+ PGOutput.read_tuples(buffer)
124
+ else
125
+ []
126
+ end,
127
+ )
128
+
129
+ in "T"
130
+ PGOutput::Truncate.new(
131
+ oid: buffer.read_int32,
132
+ data: buffer.buffer,
133
+ )
134
+
135
+ in unknown
136
+ raise "Unknown PGOutput message type: #{unknown}"
137
+ end
138
+ end
139
+
140
+ def self.read_tuples(buffer)
141
+ buffer.read_int16.times.map { read_tuple(buffer) }
142
+ end
143
+
144
+ def self.read_tuple(buffer)
145
+ case buffer.read_char
146
+ in type if type == "n"
147
+ Tuple.new(type:, data: nil)
148
+ in type
149
+ size = buffer.read_int32
150
+ data = buffer.read(size)
151
+ Tuple.new(type:, data:)
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg"
4
+ require_relative "version"
5
+ require_relative "buffer"
6
+ require_relative "pg_output"
7
+ require_relative "stream"
8
+
9
+ module PG
10
+ module Replication
11
+ module Protocol
12
+ XLogData = Data.define(:lsn, :current_lsn, :server_time, :data)
13
+ PrimaryKeepalive = Data.define(:current_lsn, :server_time, :asap)
14
+
15
+ def self.read_message(buffer)
16
+ case buffer.read_char
17
+ in "k"
18
+ PrimaryKeepalive.new(
19
+ current_lsn: buffer.read_int64,
20
+ server_time: buffer.read_timestamp,
21
+ asap: buffer.read_bool,
22
+ )
23
+
24
+ in "w"
25
+ XLogData.new(
26
+ lsn: buffer.read_int64,
27
+ current_lsn: buffer.read_int64,
28
+ server_time: buffer.read_timestamp,
29
+ data: buffer
30
+ .read
31
+ .then { |msg| Buffer.from_string(msg) }
32
+ .then { |msg| PGOutput.read_message(msg) },
33
+ )
34
+
35
+ in unknown
36
+ raise "Unknown replication message type: #{unknown}"
37
+ end
38
+ end
39
+ 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
+ end
76
+
77
+ Connection.send(:include, Replication)
78
+ end
@@ -0,0 +1,53 @@
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PG
4
+ module Replication
5
+ VERSION = "0.0.1"
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/pg/replication/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "pg-replication-protocol"
7
+ spec.version = PG::Replication::VERSION
8
+ spec.authors = ["Rodrigo Navarro"]
9
+ spec.email = ["rnavarro@rnavarro.com.br"]
10
+
11
+ spec.summary = "Postgres replication protocol"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = ">= 2.6.0"
14
+
15
+ spec.metadata["homepage_uri"] = "https://github.com/reu/pg-replication-protocol-rb"
16
+ spec.metadata["source_code_uri"] = "https://github.com/reu/pg-replication-protocol-rb"
17
+
18
+ spec.files = Dir.chdir(__dir__) do
19
+ `git ls-files -z`.split("\x0").reject do |f|
20
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
21
+ end
22
+ end
23
+
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "pg", "~> 1.0"
27
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg-replication-protocol
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Rodrigo Navarro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-11-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pg
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ description:
28
+ email:
29
+ - rnavarro@rnavarro.com.br
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - Gemfile.lock
36
+ - LICENSE.txt
37
+ - README.md
38
+ - Rakefile
39
+ - lib/pg/replication/buffer.rb
40
+ - lib/pg/replication/pg_output.rb
41
+ - lib/pg/replication/protocol.rb
42
+ - lib/pg/replication/stream.rb
43
+ - lib/pg/replication/version.rb
44
+ - pg-replication-protocol.gemspec
45
+ homepage:
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ homepage_uri: https://github.com/reu/pg-replication-protocol-rb
50
+ source_code_uri: https://github.com/reu/pg-replication-protocol-rb
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 2.6.0
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 3.4.1
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: Postgres replication protocol
70
+ test_files: []