pgoutput-client 0.0.0 → 0.2.0

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.
@@ -0,0 +1,36 @@
1
+ module Pgoutput
2
+ module Client
3
+ # PostgreSQL Log Sequence Number conversion helpers.
4
+ #
5
+ # PostgreSQL represents LSNs as two hexadecimal 32-bit halves separated by a
6
+ # slash, such as `0/16B6C50`. The replication protocol transmits the same WAL
7
+ # position as an unsigned 64-bit integer. This module converts between those
8
+ # two representations.
9
+ #
10
+ # @example Parse a textual LSN
11
+ # Pgoutput::Client::LSN.parse("0/10")
12
+ # # => 16
13
+ #
14
+ # @example Format an integer WAL position
15
+ # Pgoutput::Client::LSN.format(16)
16
+ # # => "0/10"
17
+ #
18
+ # @api public
19
+ module LSN
20
+ # Parse a PostgreSQL LSN string into an integer WAL position.
21
+ #
22
+ # @param value [#to_s] LSN string in `HEX/HEX` form
23
+ # @return [Integer] unsigned 64-bit WAL position
24
+ # @raise [ArgumentError] if the value is not a valid LSN string
25
+ def self?.parse: (untyped value) -> untyped
26
+
27
+ # Format an integer WAL position as a PostgreSQL LSN string.
28
+ #
29
+ # @param value [#to_int, #to_s] non-negative integer WAL position
30
+ # @return [String] LSN string in uppercase hexadecimal `HEX/HEX` form
31
+ # @raise [ArgumentError] if the value is negative or cannot be coerced to
32
+ # an integer
33
+ def self?.format: (untyped value) -> ::String
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,99 @@
1
+ module Pgoutput
2
+ module Client
3
+ # High-level logical replication client facade.
4
+ #
5
+ # `Runner` is the simplest public entry point for consumers that want to
6
+ # stream raw pgoutput payloads without manually managing a replication
7
+ # connection. It owns the connection lifecycle for one logical replication
8
+ # stream:
9
+ #
10
+ # 1. Build an immutable {Configuration} from keyword arguments.
11
+ # 2. Open a PostgreSQL replication connection.
12
+ # 3. Optionally create the configured replication slot.
13
+ # 4. Start logical replication.
14
+ # 5. Yield raw pgoutput payload bytes and {XLogData} metadata.
15
+ # 6. Close the connection when streaming exits.
16
+ #
17
+ # If a live stream loses its connection, the runner retries a small number
18
+ # of times with a backoff and resumes from the latest confirmed WAL
19
+ # position. Replay, checkpointing, and deduplication are not owned here;
20
+ # those concerns belong to the downstream CDC runtime and sink layer.
21
+ #
22
+ # @example Stream raw pgoutput messages
23
+ # runner = Pgoutput::Client::Runner.new(
24
+ # database_url: "postgres://localhost/app",
25
+ # slot_name: "cdc_slot",
26
+ # publication_names: ["app_publication"]
27
+ # )
28
+ #
29
+ # runner.start do |payload, metadata|
30
+ # puts "received #{payload.bytesize} bytes at #{metadata.wal_end_lsn}"
31
+ # end
32
+ #
33
+ # @see Configuration
34
+ # @see Connection
35
+ # @see Stream
36
+ # @api public
37
+ class Runner
38
+ @configuration: untyped
39
+
40
+ @stopped: untyped
41
+
42
+ @running: untyped
43
+
44
+ @stream: untyped
45
+
46
+ @resume_lsn: untyped
47
+
48
+ @acked_lsn: untyped
49
+
50
+ @slot_created: untyped
51
+
52
+ @last_error: untyped
53
+
54
+ @reconnect_attempts: untyped
55
+
56
+ DEFAULT_RECONNECT_ATTEMPTS: 3
57
+
58
+ DEFAULT_RECONNECT_BACKOFF: ::Float
59
+
60
+ attr_reader configuration: untyped
61
+
62
+ attr_reader last_error: untyped
63
+
64
+ def initialize: (**untyped options) -> void
65
+
66
+ def start: () ?{ (?) -> untyped } -> untyped
67
+
68
+ def stop: () -> nil
69
+
70
+ def restart: () ?{ (?) -> untyped } -> untyped
71
+
72
+ def running?: () -> bool
73
+
74
+ def stopped?: () -> bool
75
+
76
+ def connected?: () -> bool
77
+
78
+ def ack: (untyped lsn) -> untyped
79
+
80
+ def monitor: () -> Pgoutput::Client::RunnerState
81
+
82
+ private
83
+
84
+ def setup_connection: (untyped connection) -> untyped
85
+
86
+ def run_stream_cycle: (untyped configuration) { (?) -> untyped } -> untyped
87
+
88
+ def configuration_for_resume: () -> untyped
89
+
90
+ def normalize_lsn_value: (untyped value) -> untyped
91
+
92
+ def current_lsn_string: (untyped value) -> untyped
93
+
94
+ def reconnect_backoff_for: (untyped attempts) -> untyped
95
+
96
+ def sleep: (untyped duration) -> untyped
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,29 @@
1
+ module Pgoutput
2
+ module Client
3
+ class RunnerStateData < Data
4
+ attr_reader running: bool
5
+ attr_reader stop_requested: bool
6
+ attr_reader connected: bool
7
+ attr_reader last_received_lsn: String?
8
+ attr_reader last_feedback_lsn: String?
9
+ attr_reader last_keepalive_at: Time?
10
+ attr_reader last_error: String?
11
+ attr_reader reconnect_attempts: Integer
12
+
13
+ def self.new: (
14
+ running: bool,
15
+ stop_requested: bool,
16
+ connected: bool,
17
+ last_received_lsn: String?,
18
+ last_feedback_lsn: String?,
19
+ last_keepalive_at: Time?,
20
+ last_error: String?,
21
+ reconnect_attempts: Integer
22
+ ) -> instance
23
+ end
24
+
25
+ class RunnerState < RunnerStateData
26
+ def stopped?: () -> bool
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,89 @@
1
+ module Pgoutput
2
+ module Client
3
+ # Logical replication stream loop.
4
+ #
5
+ # `Stream` consumes PostgreSQL CopyData payloads from a {Connection}. It
6
+ # understands the two replication envelope message types used by PostgreSQL's
7
+ # streaming replication protocol:
8
+ #
9
+ # * `w` — XLogData, containing logical decoding plugin payload bytes.
10
+ # * `k` — primary keepalive, optionally requesting immediate feedback.
11
+ #
12
+ # The stream yields only XLogData plugin payloads. Keepalive messages are
13
+ # handled internally by updating the latest known WAL position and sending
14
+ # standby feedback when requested.
15
+ #
16
+ # @api private
17
+ class Stream
18
+ @connection: untyped
19
+
20
+ @configuration: untyped
21
+
22
+ @latest_lsn: untyped
23
+
24
+ @acked_lsn: untyped
25
+
26
+ @last_feedback_at: untyped
27
+
28
+ @last_keepalive_at: untyped
29
+
30
+ @running: untyped
31
+
32
+ @stop_requested: untyped
33
+
34
+ # Latest confirmed WAL position observed by this stream.
35
+ #
36
+ # @return [Integer]
37
+ attr_reader latest_lsn: untyped
38
+
39
+ attr_reader acked_lsn: untyped
40
+
41
+ attr_reader last_keepalive_at: untyped
42
+
43
+ # Build a stream loop.
44
+ #
45
+ # @param connection [Connection] replication connection
46
+ # @param configuration [Configuration] stream configuration
47
+ # @return [void]
48
+ def initialize: (connection: untyped, configuration: untyped, ?acked_lsn: untyped) -> void
49
+
50
+ # Start the stream loop.
51
+ #
52
+ # The method blocks while the stream is running. For every XLogData
53
+ # envelope, it yields the raw pgoutput payload and the parsed envelope
54
+ # metadata. When no CopyData payload is currently available, the loop
55
+ # pauses briefly before checking again.
56
+ #
57
+ # @yield [payload, metadata] called for each XLogData payload
58
+ # @yieldparam payload [String] frozen raw pgoutput payload bytes
59
+ # @yieldparam metadata [XLogData] parsed WAL envelope metadata
60
+ # @return [void]
61
+ # @raise [ArgumentError] if no block is provided
62
+ # @raise [ProtocolError] if an unknown or malformed replication message is
63
+ # received
64
+ # @raise [ConnectionError] if standby feedback cannot be sent
65
+ def start: () ?{ (?) -> untyped } -> untyped
66
+
67
+ # Stop the stream loop after the current iteration.
68
+ #
69
+ # @return [void]
70
+ def stop: () -> nil
71
+
72
+ def running?: () -> bool
73
+
74
+ def ack: (untyped lsn) -> untyped
75
+
76
+ private
77
+
78
+ def process_copy_data: (untyped copy_data) { (untyped, untyped) -> untyped } -> untyped
79
+
80
+ def send_periodic_feedback: () -> (nil | untyped)
81
+
82
+ def send_feedback: (reply_requested: untyped) -> untyped
83
+
84
+ def normalize_lsn_value: (untyped value) -> untyped
85
+
86
+ def monotonic_time: () -> untyped
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,8 @@
1
+ module Pgoutput
2
+ module Client
3
+ # Current pgoutput-client gem version.
4
+ #
5
+ # @return [String]
6
+ VERSION: "0.1.0"
7
+ end
8
+ end
@@ -0,0 +1,63 @@
1
+ module Pgoutput
2
+ module Client
3
+ class ReplicationXLogData < Data
4
+ attr_reader wal_start: Integer
5
+ attr_reader wal_end: Integer
6
+ attr_reader server_clock: Integer
7
+ attr_reader payload: String
8
+
9
+ def self.new: (
10
+ Integer wal_start,
11
+ Integer wal_end,
12
+ Integer server_clock,
13
+ String payload
14
+ ) -> instance
15
+ end
16
+
17
+ # Immutable XLogData replication envelope.
18
+ #
19
+ # PostgreSQL wraps logical decoding plugin output in an XLogData CopyData
20
+ # payload while streaming replication is active. The payload layout is:
21
+ #
22
+ # ```text
23
+ # Byte 0 : message tag `w`
24
+ # Bytes 1-8 : WAL start position, unsigned 64-bit big-endian
25
+ # Bytes 9-16 : WAL end position, unsigned 64-bit big-endian
26
+ # Bytes 17-24 : server clock, PostgreSQL timestamp format
27
+ # Bytes 25.. : logical decoding plugin payload
28
+ # ```
29
+ #
30
+ # Instances are created through {.parse} and made shareable so they can cross
31
+ # Ractor boundaries with their frozen payload bytes.
32
+ #
33
+ # @attr_reader wal_start [Integer] WAL position where this message begins
34
+ # @attr_reader wal_end [Integer] WAL position after this message
35
+ # @attr_reader server_clock [Integer] PostgreSQL server timestamp in
36
+ # microseconds since 2000-01-01 UTC
37
+ # @attr_reader payload [String] frozen raw logical decoding plugin payload
38
+ class XLogData < ReplicationXLogData
39
+ # Parse an XLogData CopyData payload.
40
+ #
41
+ # @param bytes [String] raw CopyData payload beginning with `w`
42
+ # @return [XLogData] immutable parsed envelope
43
+ # @raise [ProtocolError] if the payload is empty, has the wrong message
44
+ # tag, or is too short to contain the required fields
45
+ def self.parse: (untyped bytes) -> untyped
46
+
47
+ # Starting WAL position formatted as a PostgreSQL LSN string.
48
+ #
49
+ # @return [String]
50
+ def wal_start_lsn: () -> untyped
51
+
52
+ # Ending WAL position formatted as a PostgreSQL LSN string.
53
+ #
54
+ # @return [String]
55
+ def wal_end_lsn: () -> untyped
56
+
57
+ # @param binary [String]
58
+ # @param offset [Integer]
59
+ # @return [Integer]
60
+ def self.unpack_u64: (untyped binary, untyped offset) -> untyped
61
+ end
62
+ end
63
+ end
metadata CHANGED
@@ -1,15 +1,30 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgoutput-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
12
- description: Write a longer description or delete this line.
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: pg
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.6'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.6'
26
+ description: Transport-only PostgreSQL logical replication client for receiving pgoutput
27
+ CopyData payloads.
13
28
  email:
14
29
  - kenneth.c.demanawa@gmail.com
15
30
  executables: []
@@ -17,20 +32,44 @@ extensions: []
17
32
  extra_rdoc_files: []
18
33
  files:
19
34
  - CHANGELOG.md
20
- - CODE_OF_CONDUCT.md
21
35
  - LICENSE.txt
22
36
  - README.md
23
- - Rakefile
24
37
  - lib/pgoutput/client.rb
38
+ - lib/pgoutput/client/commands.rb
39
+ - lib/pgoutput/client/configuration.rb
40
+ - lib/pgoutput/client/connection.rb
41
+ - lib/pgoutput/client/errors.rb
42
+ - lib/pgoutput/client/feedback.rb
43
+ - lib/pgoutput/client/keepalive.rb
44
+ - lib/pgoutput/client/lsn.rb
45
+ - lib/pgoutput/client/runner.rb
46
+ - lib/pgoutput/client/state.rb
47
+ - lib/pgoutput/client/stream.rb
25
48
  - lib/pgoutput/client/version.rb
26
- - sig/pgoutput/client.rbs
27
- homepage: https://github.com/kanutocd/pgoutput-client
49
+ - lib/pgoutput/client/xlog_data.rb
50
+ - lib/pgoutput_client.rb
51
+ - sig/pg.rbs
52
+ - sig/pgoutput/client/commands.rbs
53
+ - sig/pgoutput/client/configuration.rbs
54
+ - sig/pgoutput/client/connection.rbs
55
+ - sig/pgoutput/client/errors.rbs
56
+ - sig/pgoutput/client/feedback.rbs
57
+ - sig/pgoutput/client/keepalive.rbs
58
+ - sig/pgoutput/client/lsn.rbs
59
+ - sig/pgoutput/client/runner.rbs
60
+ - sig/pgoutput/client/state.rbs
61
+ - sig/pgoutput/client/stream.rbs
62
+ - sig/pgoutput/client/version.rbs
63
+ - sig/pgoutput/client/xlog_data.rbs
64
+ homepage: https://kanutocd.github.io/pgoutput-client/
28
65
  licenses:
29
66
  - MIT
30
67
  metadata:
31
- homepage_uri: https://github.com/kanutocd/pgoutput-client
68
+ homepage_uri: https://kanutocd.github.io/pgoutput-client/
32
69
  source_code_uri: https://github.com/kanutocd/pgoutput-client
33
70
  changelog_uri: https://github.com/kanutocd/pgoutput-client/blob/main/CHANGELOG.md
71
+ documentation_uri: https://kanutocd.github.io/pgoutput-client/
72
+ rubygems_mfa_required: 'true'
34
73
  rdoc_options: []
35
74
  require_paths:
36
75
  - lib
@@ -45,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
45
84
  - !ruby/object:Gem::Version
46
85
  version: '0'
47
86
  requirements: []
48
- rubygems_version: 4.0.10
87
+ rubygems_version: 3.6.9
49
88
  specification_version: 4
50
- summary: Write a short summary, because RubyGems requires one.
89
+ summary: PostgreSQL pgoutput logical replication transport client.
51
90
  test_files: []
data/CODE_OF_CONDUCT.md DELETED
@@ -1,10 +0,0 @@
1
- # Code of Conduct
2
-
3
- "pgoutput-client" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
-
5
- * Participants will be tolerant of opposing views.
6
- * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
- * When interpreting the words and actions of others, participants should always assume good intentions.
8
- * Behaviour which can be reasonably considered harassment will not be tolerated.
9
-
10
- If you have any concerns about behaviour within this project, please contact us at ["kenneth.c.demanawa@gmail.com"](mailto:"kenneth.c.demanawa@gmail.com").
data/Rakefile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- require "rubocop/rake_task"
9
-
10
- RuboCop::RakeTask.new
11
-
12
- task default: %i[spec rubocop]
@@ -1,6 +0,0 @@
1
- module Pgoutput
2
- module Client
3
- VERSION: String
4
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
- end
6
- end