wal 0.0.29 → 0.0.30

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: 7decfb78b8ccea001e781411dbcbbe939e9a7a30f29767a306e7c0c98107aba1
4
- data.tar.gz: 976ec1815c1f379b966fcdd963e522644be2c7a5623939dc839e17673970a71f
3
+ metadata.gz: 629931aa6280e9b0f00fbc62ae9cc588771be9631fe2ee4ab5b346f884d52b27
4
+ data.tar.gz: 9144033bc75fceec8b94f33bfb67b3577bc0a1c9731353c56b6c499afd213b33
5
5
  SHA512:
6
- metadata.gz: 1b8b9af4f0936ee756aeb492b3de17b68e17472af6d17ce05574c3bda747c28f1e73910490994a02d6f0255e59f2187d1e5e41927ddbdff39e113cc866950086
7
- data.tar.gz: b0e185bf497f7dbc188eab4ef6935277a16e7a521c06a001b81374d68761954198852a7af1c68f51db5b126699b7f97666be66d8d7ea0ab14c95fd32a8120b95
6
+ metadata.gz: 3eb7fcf6d3ceea6f5d72cbcc75ffd67c99fcd688a06cfb8a8e412a428e4c6c5ef7dc44178ab4219d94e4a6e15ea8b4d3c45f00bbf96c33dec6103f688295ca50
7
+ data.tar.gz: f5b8260db91d66be7cf77a391d9a98285bb5773638778afffb2aa02b9b89a306937310104fffed49d8a658f33f0ae818ea35adb138461a3e0634ce341e63af2c
data/exe/wal CHANGED
@@ -28,47 +28,6 @@ require "./config/environment"
28
28
 
29
29
  db_config = ActiveRecord::Base.configurations.configs_for(name: "primary").configuration_hash
30
30
 
31
- class Wal::LoggingReplicator
32
- def initialize(slot, replicator)
33
- @slot = slot
34
- @replicator = replicator
35
- end
36
-
37
- def replicate_forever(watcher, publications:)
38
- replication = @replicator.replicate(watcher, publications:)
39
- count = 0
40
- start = Time.now
41
- loop do
42
- case (event = replication.next)
43
- when Wal::BeginTransactionEvent
44
- start = Time.now
45
- count = 0
46
- if event.estimated_size > 0
47
- Wal.logger&.info("[#{@slot}] Begin transaction=#{event.transaction_id} size=#{event.estimated_size}")
48
- end
49
- when Wal::CommitTransactionEvent
50
- if count > 0
51
- elapsed = ((Time.now - start) * 1000.0).round(1)
52
- Wal.logger&.info("[#{@slot}] Commit transaction=#{event.transaction_id} elapsed=#{elapsed} events=#{count}")
53
- end
54
- when Wal::InsertEvent
55
- Wal.logger&.debug("[#{@slot}] Insert transaction=#{event.transaction_id} table=#{event.table} primary_key=#{event.primary_key}")
56
- count += 1
57
- when Wal::UpdateEvent
58
- Wal.logger&.debug("[#{@slot}] Update transaction=#{event.transaction_id} table=#{event.table} primary_key=#{event.primary_key}")
59
- count += 1
60
- when Wal::DeleteEvent
61
- Wal.logger&.debug("[#{@slot}] Delete transaction=#{event.transaction_id} table=#{event.table} primary_key=#{event.primary_key}")
62
- count += 1
63
- else
64
- count += 1
65
- end
66
- end
67
- rescue StopIteration
68
- nil
69
- end
70
- end
71
-
72
31
  def watch(db_config, cli)
73
32
  watcher = cli["--watcher"].constantize.new
74
33
  use_temporary_slot = cli["--tmp-slot"] || false
@@ -78,90 +37,15 @@ def watch(db_config, cli)
78
37
  replicator_class = cli["--replicator"].presence&.constantize || Wal::Replicator
79
38
 
80
39
  puts "Watcher started for #{replication_slot} slot (#{publications.join(", ")})"
81
- replicator = replicator_class.new(replication_slot:, use_temporary_slot:, db_config:)
82
- replicator = Wal::LoggingReplicator.new(replication_slot, replicator)
83
- replicator.replicate_forever(watcher, publications:)
40
+ replicator_class
41
+ .new(replication_slot:, use_temporary_slot:, db_config:)
42
+ .replicate_forever(Wal::LoggingWatcher.new(replication_slot, watcher), publications:)
84
43
  puts "Watcher finished for #{replication_slot}"
85
44
  end
86
45
 
87
- def start_worker(db_config, slot, config)
88
- watcher = config["watcher"].constantize.new
89
- temporary = config["temporary"] || false
90
- publications = config["publications"] || []
91
- replicator_class = config["replicator"].presence&.constantize || Wal::Replicator
92
- auto_restart = config["auto_restart"].nil? || config["auto_restart"]
93
- max_retries = config["retries"]&.to_i || (2**32 - 1)
94
- retries = 0
95
- backoff = config["retry_backoff"]&.to_f || 1
96
- backoff_expoent = config["retry_backoff_expoent"]&.to_f
97
-
98
- Thread.new(slot, watcher, temporary, publications) do |replication_slot, watcher, use_temporary_slot, publications|
99
- replication_slot = "#{replication_slot}_#{SecureRandom.alphanumeric(4)}" if use_temporary_slot
100
- puts "Watcher started for #{replication_slot} slot (#{publications.join(", ")})"
101
-
102
- begin
103
- replicator = replicator_class.new(replication_slot:, use_temporary_slot:, db_config:)
104
- replicator = Wal::LoggingReplicator.new(replication_slot, replicator)
105
- replicator.replicate_forever(watcher, publications:)
106
- if auto_restart
107
- backoff_time = backoff_expoent ? (backoff * retries) ** backoff_expoent : backoff
108
- puts "Watcher finished for #{replication_slot}, auto restarting in #{backoff_time.floor(2)}..."
109
- sleep backoff_time
110
- puts "Restarting #{replication_slot}"
111
- redo
112
- end
113
- rescue StandardError => err
114
- if retries < max_retries
115
- Wal.logger&.error("[#{replication_slot}] Error #{err}")
116
- retries += 1
117
- backoff_time = backoff_expoent ? (backoff * retries) ** backoff_expoent : backoff
118
- puts "Restarting #{replication_slot} in #{backoff_time.floor(2)}s..."
119
- sleep backoff_time
120
- puts "Restarting #{replication_slot}"
121
- retry
122
- end
123
- raise
124
- end
125
-
126
- puts "Watcher finished for #{replication_slot}"
127
-
128
- Process.kill("TERM", Process.pid)
129
- end
130
- end
131
-
132
- def start(db_config, cli)
133
- slots = YAML.load_file(cli["<config-file>"])["slots"]
134
-
135
- workers = slots.map do |slot, config|
136
- start_worker(db_config, slot, config)
137
- end
138
-
139
- ping = Thread.new do
140
- loop do
141
- ActiveRecord::Base.connection_pool.with_connection do |conn|
142
- conn.execute("SELECT pg_logical_emit_message(true, 'wal_ping', '{}')")
143
- end
144
- sleep 20
145
- end
146
- end
147
-
148
- workers << ping
149
-
150
- stop_workers = proc do
151
- puts "Stopping WAL workers..."
152
- workers.each(&:kill)
153
- puts "WAL workers stopped"
154
- exit 0
155
- end
156
-
157
- Signal.trap("TERM", &stop_workers)
158
- Signal.trap("INT", &stop_workers)
159
-
160
- workers.each(&:join)
161
- end
162
-
163
46
  if cli["watch"]
164
47
  watch(db_config, cli)
165
48
  elsif cli["start"]
166
- start(db_config, cli)
49
+ runner = Wal::Runner.new(config_file: cli["<config-file>"], db_config:)
50
+ runner.start
167
51
  end
@@ -0,0 +1,46 @@
1
+ module Wal
2
+ class LoggingWatcher
3
+ include Wal::Watcher
4
+
5
+ def initialize(slot, watcher)
6
+ @slot = slot
7
+ @watcher = watcher
8
+ end
9
+
10
+ def should_watch_table?(table)
11
+ @watcher.should_watch_table? table
12
+ end
13
+
14
+ def valid_context_prefix?(prefix)
15
+ @watcher.valid_context_prefix? prefix
16
+ end
17
+
18
+ def on_event(event)
19
+ case event
20
+ when Wal::BeginTransactionEvent
21
+ @start = Time.now
22
+ @count = 0
23
+ if event.estimated_size > 0
24
+ Wal.logger&.debug("[#{@slot}] Begin transaction=#{event.transaction_id} size=#{event.estimated_size}")
25
+ end
26
+ when Wal::CommitTransactionEvent
27
+ if @count > 0
28
+ elapsed = ((Time.now - @start) * 1000.0).round(1)
29
+ Wal.logger&.info("[#{@slot}] Commit transaction=#{event.transaction_id} elapsed=#{elapsed} events=#{@count}")
30
+ end
31
+ when Wal::InsertEvent
32
+ Wal.logger&.debug("[#{@slot}] Insert transaction=#{event.transaction_id} table=#{event.table} primary_key=#{event.primary_key}")
33
+ @count += 1
34
+ when Wal::UpdateEvent
35
+ Wal.logger&.debug("[#{@slot}] Update transaction=#{event.transaction_id} table=#{event.table} primary_key=#{event.primary_key}")
36
+ @count += 1
37
+ when Wal::DeleteEvent
38
+ Wal.logger&.debug("[#{@slot}] Delete transaction=#{event.transaction_id} table=#{event.table} primary_key=#{event.primary_key}")
39
+ @count += 1
40
+ else
41
+ @count += 1
42
+ end
43
+ @watcher.on_event(event)
44
+ end
45
+ end
46
+ end
data/lib/wal/runner.rb ADDED
@@ -0,0 +1,164 @@
1
+ module Wal
2
+ class Runner
3
+ class Worker
4
+ attr_reader :name, :slot_configs, :db_config
5
+
6
+ def initialize(name:, slot_configs:, db_config:)
7
+ @name = name
8
+ @slot_configs = slot_configs
9
+ @db_config = db_config
10
+ @threads = []
11
+ end
12
+
13
+ def run
14
+ @threads = slot_configs.map { |slot, config| start_worker(slot, config) }
15
+ setup_signal_handlers
16
+ @threads.each(&:join)
17
+ end
18
+
19
+ private
20
+
21
+ def start_worker(slot, config)
22
+ watcher = config["watcher"].constantize.new
23
+ temporary = config["temporary"] || false
24
+ publications = config["publications"] || []
25
+ replicator_class = config["replicator"].presence&.constantize || Wal::Replicator
26
+ auto_restart = config["auto_restart"].nil? || config["auto_restart"]
27
+ max_retries = config["retries"]&.to_i || (2**32 - 1)
28
+ backoff = config["retry_backoff"]&.to_f || 1
29
+ backoff_exponent = config["retry_backoff_expoent"]&.to_f
30
+
31
+ Thread.new(slot, watcher, temporary, publications) do |replication_slot, watcher, use_temporary_slot, publications|
32
+ retries = 0
33
+ replication_slot = "#{replication_slot}_#{SecureRandom.alphanumeric(4)}" if use_temporary_slot
34
+ puts "Watcher started for #{replication_slot} slot (#{publications.join(", ")})"
35
+
36
+ begin
37
+ replicator_class
38
+ .new(replication_slot:, use_temporary_slot:, db_config:)
39
+ .replicate_forever(Wal::LoggingWatcher.new(replication_slot, watcher), publications:)
40
+ if auto_restart
41
+ backoff_time = backoff_exponent ? (backoff * retries) ** backoff_exponent : backoff
42
+ puts "Watcher finished for #{replication_slot}, auto restarting in #{backoff_time.floor(2)}..."
43
+ sleep backoff_time
44
+ puts "Restarting #{replication_slot}"
45
+ redo
46
+ end
47
+ rescue StandardError => err
48
+ if retries < max_retries
49
+ Wal.logger&.error("[#{replication_slot}] Error #{err}")
50
+ Wal.logger&.error([err.message, *err.backtrace].join("\n"))
51
+ retries += 1
52
+ backoff_time = backoff_exponent ? (backoff * retries) ** backoff_exponent : backoff
53
+ puts "Restarting #{replication_slot} in #{backoff_time.floor(2)}s..."
54
+ sleep backoff_time
55
+ puts "Restarting #{replication_slot}"
56
+ retry
57
+ end
58
+ raise
59
+ end
60
+
61
+ puts "Watcher finished for #{replication_slot}"
62
+ Process.kill("TERM", Process.pid)
63
+ end
64
+ end
65
+
66
+ def setup_signal_handlers
67
+ stop_threads = proc do
68
+ puts "[#{name}] Stopping WAL threads..."
69
+ @threads.each(&:kill)
70
+ puts "[#{name}] WAL threads stopped"
71
+ exit 0
72
+ end
73
+
74
+ Signal.trap("TERM", &stop_threads)
75
+ Signal.trap("INT", &stop_threads)
76
+ end
77
+ end
78
+
79
+ attr_reader :config_file, :db_config
80
+
81
+ def initialize(config_file:, db_config:)
82
+ @config_file = config_file
83
+ @db_config = db_config
84
+ @child_pids = []
85
+ end
86
+
87
+ def start
88
+ slots = YAML.load_file(config_file)["slots"]
89
+ workers_slots = slots.group_by { |_slot, config| config["worker"] || "default" }
90
+
91
+ Wal.fork_hooks[:before_fork]&.call
92
+
93
+ workers_slots.each do |worker_name, slot_configs|
94
+ pid = fork_worker(worker_name, slot_configs)
95
+ @child_pids << pid
96
+ puts "Spawned worker '#{worker_name}' with PID #{pid}"
97
+ end
98
+
99
+ @ping_thread = start_ping_thread
100
+ setup_signal_handlers
101
+ wait_for_workers
102
+ end
103
+
104
+ private
105
+
106
+ def fork_worker(worker_name, slot_configs)
107
+ Process.fork do
108
+ Wal.fork_hooks[:after_fork]&.call
109
+ puts "[#{worker_name}] Starting worker process (PID: #{Process.pid})"
110
+ worker = Worker.new(name: worker_name, slot_configs: slot_configs, db_config: db_config)
111
+ worker.run
112
+ end
113
+ end
114
+
115
+ def start_ping_thread
116
+ Thread.new do
117
+ loop do
118
+ ActiveRecord::Base.connection_pool.with_connection do |conn|
119
+ conn.execute("SELECT pg_logical_emit_message(true, 'wal_ping', '{}')")
120
+ end
121
+ sleep 20
122
+ end
123
+ end
124
+ end
125
+
126
+ def setup_signal_handlers
127
+ stop_workers = proc do
128
+ puts "Stopping all worker processes..."
129
+ @ping_thread&.kill
130
+ @child_pids.each do |pid|
131
+ Process.kill("TERM", pid)
132
+ rescue Errno::ESRCH
133
+ # Process already dead
134
+ end
135
+ @child_pids.each do |pid|
136
+ Process.wait(pid)
137
+ rescue Errno::ECHILD
138
+ # Already reaped
139
+ end
140
+ puts "All worker processes stopped"
141
+ exit 0
142
+ end
143
+
144
+ Signal.trap("TERM", &stop_workers)
145
+ Signal.trap("INT", &stop_workers)
146
+
147
+ @stop_workers = stop_workers
148
+ end
149
+
150
+ def wait_for_workers
151
+ loop do
152
+ exited_pid = Process.wait(-1)
153
+ if @child_pids.include?(exited_pid)
154
+ @child_pids.delete(exited_pid)
155
+ puts "Worker process #{exited_pid} exited unexpectedly, shutting down..."
156
+ @stop_workers.call
157
+ end
158
+ rescue Errno::ECHILD
159
+ puts "All worker processes have exited"
160
+ break
161
+ end
162
+ end
163
+ end
164
+ end
data/lib/wal/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wal
4
- VERSION = "0.0.29"
4
+ VERSION = "0.0.30"
5
5
  end
data/lib/wal.rb CHANGED
@@ -4,20 +4,35 @@ require "active_support"
4
4
  require "active_record"
5
5
  require_relative "wal/watcher"
6
6
  require_relative "wal/noop_watcher"
7
+ require_relative "wal/logging_watcher"
7
8
  require_relative "wal/record_watcher"
8
9
  require_relative "wal/streaming_watcher"
9
10
  require_relative "wal/replicator"
10
11
  require_relative "wal/active_record_context_extension"
11
12
  require_relative "wal/railtie"
13
+ require_relative "wal/runner"
12
14
  require_relative "wal/version"
13
15
 
14
16
  module Wal
15
17
  class << self
16
18
  attr_accessor :logger
19
+ attr_accessor :fork_hooks
17
20
 
18
21
  def logger
19
22
  @logger ||= Logger.new($stdout, level: :info)
20
23
  end
24
+
25
+ def fork_hooks
26
+ @fork_hooks ||= {}
27
+ end
28
+
29
+ def before_fork(&block)
30
+ fork_hooks[:before_fork] = block
31
+ end
32
+
33
+ def after_fork(&block)
34
+ fork_hooks[:after_fork] = block
35
+ end
21
36
  end
22
37
 
23
38
  def self.configure(&block)
data/rbi/wal.rbi CHANGED
@@ -7,11 +7,17 @@ module Wal
7
7
  UpdateEvent,
8
8
  DeleteEvent,
9
9
  ) }
10
- VERSION = "0.0.29"
10
+ VERSION = "0.0.30"
11
11
 
12
12
  class << self
13
13
  sig { returns(T.class_of(Logger)) }
14
14
  attr_accessor :logger
15
+
16
+ sig { params(block: T.proc.void).void }
17
+ def before_fork(&block); end
18
+
19
+ sig { params(block: T.proc.void).void }
20
+ def after_fork(&block); end
15
21
  end
16
22
 
17
23
  sig { params(block: T.proc.params(config: T.class_of(Wal)).void).void }
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.29
4
+ version: 0.0.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Navarro
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-01-20 00:00:00.000000000 Z
10
+ date: 2026-01-21 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: pg
@@ -95,11 +95,13 @@ files:
95
95
  - lib/wal/active_record_context_extension.rb
96
96
  - lib/wal/generators/migration.rb
97
97
  - lib/wal/generators/templates/migration.rb.erb
98
+ - lib/wal/logging_watcher.rb
98
99
  - lib/wal/migration.rb
99
100
  - lib/wal/noop_watcher.rb
100
101
  - lib/wal/railtie.rb
101
102
  - lib/wal/record_watcher.rb
102
103
  - lib/wal/replicator.rb
104
+ - lib/wal/runner.rb
103
105
  - lib/wal/streaming_watcher.rb
104
106
  - lib/wal/version.rb
105
107
  - lib/wal/watcher.rb