pg-replication-protocol 0.0.7 → 0.0.8
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/lib/pg/replication/buffer.rb +50 -24
- data/lib/pg/replication/version.rb +1 -1
- data/lib/pg/replication.rb +139 -51
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f00c769aee5ae5e1d41063e827f2d5fcaa19991aed6d4676f414ce5f6d6326ef
|
|
4
|
+
data.tar.gz: a99c6f062f9c33109a409b58ef465833fb1d0b3fd8499a91461db9ea3e2586af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a97e4828c3c0d41607c021fe3b3b87b9c2c2dc01ae70131a984ce4513a46c2386e8d86ce5fc3029f395cc16a9f410c1fbe4c2dff8f77c833bc40f4d75ff8e509
|
|
7
|
+
data.tar.gz: def00e2668b59de09ed0eb8a2fa8efb5974c25c36ea82fcf64b83115afb7519b38010019310267dc4efddeca76df56e6861696abd285d2b2dfab8c00a309e21b
|
|
@@ -1,21 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "delegate"
|
|
4
|
-
require "stringio"
|
|
5
|
-
|
|
6
3
|
module PG
|
|
7
4
|
module Replication
|
|
8
|
-
|
|
5
|
+
POSTGRES_EPOCH = Time.utc(2000, 1, 1).freeze
|
|
6
|
+
POSTGRES_EPOCH_USECS = (POSTGRES_EPOCH.to_r * 1_000_000).to_i
|
|
7
|
+
|
|
8
|
+
class Buffer
|
|
9
|
+
def initialize(data)
|
|
10
|
+
@data = data
|
|
11
|
+
@pos = 0
|
|
12
|
+
end
|
|
13
|
+
|
|
9
14
|
def self.from_string(str)
|
|
10
|
-
new(
|
|
15
|
+
new(str.b)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def eof?
|
|
19
|
+
@pos >= @data.bytesize
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def read(n = nil)
|
|
23
|
+
return nil if @pos >= @data.bytesize
|
|
24
|
+
if n.nil?
|
|
25
|
+
result = @data.byteslice(@pos..-1)
|
|
26
|
+
@pos = @data.bytesize
|
|
27
|
+
else
|
|
28
|
+
result = @data.byteslice(@pos, n)
|
|
29
|
+
@pos += result.bytesize
|
|
30
|
+
end
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def readbyte
|
|
35
|
+
raise EOFError if @pos >= @data.bytesize
|
|
36
|
+
byte = @data.getbyte(@pos)
|
|
37
|
+
@pos += 1
|
|
38
|
+
byte
|
|
11
39
|
end
|
|
12
40
|
|
|
13
41
|
def read_char
|
|
14
|
-
|
|
42
|
+
readbyte.chr
|
|
15
43
|
end
|
|
16
44
|
|
|
17
45
|
def read_bool
|
|
18
|
-
|
|
46
|
+
readbyte == 1
|
|
19
47
|
end
|
|
20
48
|
|
|
21
49
|
def read_int8
|
|
@@ -23,40 +51,38 @@ module PG
|
|
|
23
51
|
end
|
|
24
52
|
|
|
25
53
|
def read_int16
|
|
26
|
-
read_bytes(2).
|
|
54
|
+
read_bytes(2).unpack1("n")
|
|
27
55
|
end
|
|
28
56
|
|
|
29
57
|
def read_int32
|
|
30
|
-
read_bytes(4).
|
|
58
|
+
read_bytes(4).unpack1("N")
|
|
31
59
|
end
|
|
32
60
|
|
|
33
61
|
def read_int64
|
|
34
|
-
read_bytes(8).
|
|
62
|
+
read_bytes(8).unpack1("Q>")
|
|
35
63
|
end
|
|
36
64
|
|
|
37
65
|
def read_timestamp
|
|
38
|
-
usecs =
|
|
39
|
-
Time.at(usecs /
|
|
66
|
+
usecs = POSTGRES_EPOCH_USECS + read_int64
|
|
67
|
+
Time.at(usecs / 1_000_000, usecs % 1_000_000, :microsecond, in: "UTC")
|
|
40
68
|
end
|
|
41
69
|
|
|
42
70
|
def read_cstring
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
str << chr
|
|
50
|
-
end
|
|
51
|
-
end
|
|
71
|
+
null_pos = @data.index("\0", @pos)
|
|
72
|
+
raise EOFError, "Unterminated C-string" if null_pos.nil?
|
|
73
|
+
|
|
74
|
+
str = @data.byteslice(@pos, null_pos - @pos)
|
|
75
|
+
@pos = null_pos + 1 # Skip past null terminator
|
|
76
|
+
str
|
|
52
77
|
end
|
|
53
78
|
|
|
54
79
|
private
|
|
55
80
|
|
|
56
81
|
def read_bytes(n)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
raise EOFError if @pos + n > @data.bytesize
|
|
83
|
+
result = @data.byteslice(@pos, n)
|
|
84
|
+
@pos += n
|
|
85
|
+
result
|
|
60
86
|
end
|
|
61
87
|
end
|
|
62
88
|
end
|
data/lib/pg/replication.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "pg"
|
|
4
|
+
require "thread"
|
|
4
5
|
require_relative "replication/version"
|
|
5
6
|
require_relative "replication/buffer"
|
|
6
7
|
require_relative "replication/pg_output"
|
|
@@ -8,7 +9,12 @@ require_relative "replication/protocol"
|
|
|
8
9
|
|
|
9
10
|
module PG
|
|
10
11
|
module Replication
|
|
11
|
-
|
|
12
|
+
DEFAULT_QUEUE_SIZE = 10_000
|
|
13
|
+
|
|
14
|
+
StreamEnd = Object.new.freeze
|
|
15
|
+
StreamError = Data.define(:exception)
|
|
16
|
+
|
|
17
|
+
def start_replication_slot(slot, logical: true, auto_keep_alive: true, location: "0/0", queue_size: DEFAULT_QUEUE_SIZE, **params)
|
|
12
18
|
keep_alive_secs = wal_receiver_status_interval
|
|
13
19
|
@last_confirmed_lsn = confirmed_slot_lsn(slot) || 0
|
|
14
20
|
|
|
@@ -22,51 +28,11 @@ module PG
|
|
|
22
28
|
end
|
|
23
29
|
query(start_query)
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if auto_keep_alive && Time.now - last_keep_alive > keep_alive_secs
|
|
31
|
-
standby_status_update(write_lsn: @last_confirmed_lsn)
|
|
32
|
-
last_keep_alive = Time.now
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
consume_input
|
|
36
|
-
next if is_busy
|
|
37
|
-
|
|
38
|
-
case get_copy_data(async: true)
|
|
39
|
-
in nil
|
|
40
|
-
get_last_result
|
|
41
|
-
break
|
|
42
|
-
|
|
43
|
-
in false
|
|
44
|
-
IO.select([socket_io], nil, nil, keep_alive_secs)
|
|
45
|
-
next
|
|
46
|
-
|
|
47
|
-
in data
|
|
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) if lsn > 0
|
|
52
|
-
last_keep_alive = Time.now
|
|
53
|
-
|
|
54
|
-
in Protocol::PrimaryKeepalive(current_lsn:, server_time:, asap: true) if auto_keep_alive
|
|
55
|
-
standby_status_update(write_lsn: current_lsn)
|
|
56
|
-
last_keep_alive = Time.now
|
|
57
|
-
y << msg
|
|
58
|
-
|
|
59
|
-
in Protocol::PrimaryKeepalive(current_lsn:)
|
|
60
|
-
y << msg
|
|
61
|
-
@last_confirmed_lsn = [@last_confirmed_lsn, current_lsn].compact.max
|
|
62
|
-
|
|
63
|
-
else
|
|
64
|
-
y << msg
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
.lazy
|
|
31
|
+
if auto_keep_alive
|
|
32
|
+
start_threaded_replication(keep_alive_secs, queue_size)
|
|
33
|
+
else
|
|
34
|
+
start_sync_replication
|
|
35
|
+
end
|
|
70
36
|
end
|
|
71
37
|
|
|
72
38
|
def start_pgoutput_replication_slot(slot, publication_names, **kwargs)
|
|
@@ -96,17 +62,25 @@ module PG
|
|
|
96
62
|
write_lsn,
|
|
97
63
|
flush_lsn,
|
|
98
64
|
apply_lsn,
|
|
99
|
-
(timestamp -
|
|
65
|
+
((timestamp.to_r - POSTGRES_EPOCH.to_r) * 1_000_000).to_i,
|
|
100
66
|
reply ? 1 : 0,
|
|
101
67
|
].pack("CQ>Q>Q>Q>C")
|
|
102
68
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
69
|
+
status_update_mutex.synchronize do
|
|
70
|
+
put_copy_data(msg)
|
|
71
|
+
flush
|
|
72
|
+
@last_confirmed_lsn = [@last_confirmed_lsn, write_lsn].compact.max
|
|
73
|
+
end
|
|
106
74
|
end
|
|
107
75
|
|
|
108
76
|
def last_confirmed_lsn
|
|
109
|
-
@last_confirmed_lsn
|
|
77
|
+
status_update_mutex.synchronize { @last_confirmed_lsn }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def stop_replication
|
|
81
|
+
status_update_mutex.synchronize do
|
|
82
|
+
put_copy_end
|
|
83
|
+
end
|
|
110
84
|
end
|
|
111
85
|
|
|
112
86
|
def wal_receiver_status_interval
|
|
@@ -124,6 +98,120 @@ module PG
|
|
|
124
98
|
rescue StandardError
|
|
125
99
|
nil
|
|
126
100
|
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def status_update_mutex
|
|
105
|
+
@status_update_mutex ||= Mutex.new
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def start_threaded_replication(keep_alive_secs, queue_size)
|
|
109
|
+
conn = self
|
|
110
|
+
queue = SizedQueue.new(queue_size)
|
|
111
|
+
last_keepalive = Time.now
|
|
112
|
+
|
|
113
|
+
thread = Thread.new do
|
|
114
|
+
loop do
|
|
115
|
+
break if queue.closed?
|
|
116
|
+
|
|
117
|
+
conn.consume_input
|
|
118
|
+
next if conn.is_busy
|
|
119
|
+
|
|
120
|
+
case (data = conn.get_copy_data(async: true))
|
|
121
|
+
when nil
|
|
122
|
+
queue.push(StreamEnd, true) rescue nil
|
|
123
|
+
conn.get_last_result
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
when false
|
|
127
|
+
timeout = [keep_alive_secs - (Time.now - last_keepalive), 0.1].max
|
|
128
|
+
IO.select([conn.socket_io], nil, nil, timeout)
|
|
129
|
+
|
|
130
|
+
if Time.now - last_keepalive >= keep_alive_secs
|
|
131
|
+
conn.standby_status_update(write_lsn: conn.last_confirmed_lsn)
|
|
132
|
+
last_keepalive = Time.now
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
else
|
|
136
|
+
msg = Protocol.read_message(Buffer.from_string(data))
|
|
137
|
+
|
|
138
|
+
if msg.is_a?(Protocol::PrimaryKeepalive) && msg.asap
|
|
139
|
+
conn.standby_status_update(write_lsn: msg.current_lsn)
|
|
140
|
+
last_keepalive = Time.now
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Non-blocking push with keepalive retry loop
|
|
144
|
+
loop do
|
|
145
|
+
break if queue.closed?
|
|
146
|
+
begin
|
|
147
|
+
queue.push(msg, true)
|
|
148
|
+
break
|
|
149
|
+
rescue ThreadError
|
|
150
|
+
if Time.now - last_keepalive >= keep_alive_secs
|
|
151
|
+
conn.standby_status_update(write_lsn: conn.last_confirmed_lsn)
|
|
152
|
+
last_keepalive = Time.now
|
|
153
|
+
end
|
|
154
|
+
sleep(0.05)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
rescue ClosedQueueError
|
|
160
|
+
# Clean exit
|
|
161
|
+
rescue => e
|
|
162
|
+
queue.push(StreamError.new(e), true) rescue nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
Enumerator.new do |y|
|
|
166
|
+
loop do
|
|
167
|
+
msg = queue.pop
|
|
168
|
+
|
|
169
|
+
case msg
|
|
170
|
+
when StreamEnd
|
|
171
|
+
break
|
|
172
|
+
when StreamError
|
|
173
|
+
raise msg.exception
|
|
174
|
+
else
|
|
175
|
+
y << msg
|
|
176
|
+
|
|
177
|
+
lsn = case msg
|
|
178
|
+
when Protocol::XLogData
|
|
179
|
+
msg.lsn
|
|
180
|
+
when Protocol::PrimaryKeepalive
|
|
181
|
+
msg.current_lsn
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if lsn && lsn > 0
|
|
185
|
+
status_update_mutex.synchronize { @last_confirmed_lsn = lsn }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
ensure
|
|
190
|
+
queue.close
|
|
191
|
+
thread.join(5)
|
|
192
|
+
thread.kill if thread.alive?
|
|
193
|
+
end.lazy
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def start_sync_replication
|
|
197
|
+
Enumerator.new do |y|
|
|
198
|
+
loop do
|
|
199
|
+
consume_input
|
|
200
|
+
next if is_busy
|
|
201
|
+
|
|
202
|
+
case get_copy_data(async: true)
|
|
203
|
+
in nil
|
|
204
|
+
get_last_result
|
|
205
|
+
break
|
|
206
|
+
in false
|
|
207
|
+
IO.select([socket_io], nil, nil, 10)
|
|
208
|
+
next
|
|
209
|
+
in data
|
|
210
|
+
y << Protocol.read_message(Buffer.new(data))
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end.lazy
|
|
214
|
+
end
|
|
127
215
|
end
|
|
128
216
|
|
|
129
217
|
Connection.send(:include, Replication)
|
metadata
CHANGED
|
@@ -1,13 +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.
|
|
4
|
+
version: 0.0.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rodrigo Navarro
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-01-14 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: pg
|