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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc030b9fec7b0a01acbf309813046913b36da4ddc289e7201d17e78b6de0d471
4
- data.tar.gz: 7fa53e2ce183c315b70812f1d58af99afa492419d208704f2cbf97d67a7e57f8
3
+ metadata.gz: f00c769aee5ae5e1d41063e827f2d5fcaa19991aed6d4676f414ce5f6d6326ef
4
+ data.tar.gz: a99c6f062f9c33109a409b58ef465833fb1d0b3fd8499a91461db9ea3e2586af
5
5
  SHA512:
6
- metadata.gz: 2a9ab36b64b600bbd3f306d818f2bfc06666e520bba5588614ed5783c78227b1277522f03737de90b185ef659bd099cbb4064ece4d2afbafe5e8815e71916ca8
7
- data.tar.gz: a94a6ca7fddac35799d4dad64099aacc0af68b4d7e37ffcd777d504852a29f26016595d3e5a15c3a3a9174a9706709787342f80fb059e4a01292d00f219e8df6
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
- class Buffer < SimpleDelegator
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(StringIO.new(str))
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
- read_int8.chr
42
+ readbyte.chr
15
43
  end
16
44
 
17
45
  def read_bool
18
- read_int8 == 1
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).unpack("n").first
54
+ read_bytes(2).unpack1("n")
27
55
  end
28
56
 
29
57
  def read_int32
30
- read_bytes(4).unpack("N").first
58
+ read_bytes(4).unpack1("N")
31
59
  end
32
60
 
33
61
  def read_int64
34
- read_bytes(8).unpack("Q>").first
62
+ read_bytes(8).unpack1("Q>")
35
63
  end
36
64
 
37
65
  def read_timestamp
38
- usecs = Time.new(2_000, 1, 1, 0, 0, 0, 0).to_i * 10**6 + read_int64
39
- Time.at(usecs / 10**6, usecs % 10**6, :microsecond)
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
- str = String.new
44
- loop do
45
- case read_char
46
- in "\0"
47
- return str
48
- in chr
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
- bytes = read(n)
58
- raise EOFError if bytes.nil? || bytes.size < n
59
- bytes
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module PG
4
4
  module Replication
5
- VERSION = "0.0.7"
5
+ VERSION = "0.0.8"
6
6
  end
7
7
  end
@@ -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
- def start_replication_slot(slot, logical: true, auto_keep_alive: true, location: "0/0", **params)
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
- last_keep_alive = Time.now
26
-
27
- Enumerator
28
- .new do |y|
29
- loop do
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 - Time.new(2_000, 1, 1, 0, 0, 0, 0)) * 10**6,
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
- put_copy_data(msg)
104
- flush
105
- @last_confirmed_lsn = [@last_confirmed_lsn, write_lsn].compact.max
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.7
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: 2025-06-22 00:00:00.000000000 Z
10
+ date: 2026-01-14 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: pg