wal 0.0.32 → 0.0.34

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: e8f18e327f41668911967dbb73d0f6c51ab43f2b5d1d25b7c39b5f5130a04081
4
- data.tar.gz: d0ffc96073a5faa4fd3949a02348b029bd206196fdd61c71883e5115ee53d6fa
3
+ metadata.gz: f7a8f82addc9ffce03d2f1ee6a77bac1f3ae2074a5b427a8c60ca087df2aded6
4
+ data.tar.gz: 75559dccba1edf1d0f1a60ebf866726705fe48166d697a1859ec6da644e963f3
5
5
  SHA512:
6
- metadata.gz: effddb80b5b8093e572e65a587f5b7975cb20750aa39e7c329cb7358bd099d3bd12110648ee5b11c4dd45d791c1d050e6b0e7e82c7585c1678f9b1c532d54395
7
- data.tar.gz: 35e21330e4da4297adcf4cbfa67ce939b5894981b352acfbc7fe8c02b1083436d037e9f7be45445445ef621ec783c84ce866f8636379609d7ff8c62e7c9fd392
6
+ metadata.gz: 0d0d206e647386006b07e1a89e1a5596346a50894d0260d5def25876902abcbd398c07889a03ac43c88041343e576b2793eec8a1eecdcf47ff1f025587721a38
7
+ data.tar.gz: 62b2dbbeadf2c8076f2dee4ad1a95491552dfb0be3af646b03ffa449dd54009cfd940020fe4dfe3b69c2c5e7a707bdafdc2959baebe109ba28d288b31a086031
data/exe/wal CHANGED
@@ -7,7 +7,7 @@ begin
7
7
  Usage:
8
8
  wal watch --watcher <watcher-class>
9
9
  (--slot <replication-slot> | --tmp-slot)
10
- [--publication=<publication>...]
10
+ --publication <publication>...
11
11
  [--replicator <replicator-class>]
12
12
  wal start <config-file>
13
13
 
@@ -16,7 +16,7 @@ begin
16
16
  --watcher=<watcher-class> The watcher class to be used to listen for WAL changes.
17
17
  --slot=<replication-slot> The replication slot that will be used.
18
18
  --tmp-slot Use a temporary replication slot.
19
- [--publication=<publication>...] Force using the informed Postgres publications.
19
+ [--publication=<publication>...] Postgres publications to subscribe to.
20
20
  [--replicator=<replicatior_class>] Change the replication driver class
21
21
  DOCOPT
22
22
  rescue Docopt::Exit => err
@@ -28,24 +28,18 @@ require "./config/environment"
28
28
 
29
29
  db_config = ActiveRecord::Base.configurations.configs_for(name: "primary").configuration_hash
30
30
 
31
- def watch(db_config, cli)
32
- watcher = cli["--watcher"].constantize.new
33
- use_temporary_slot = cli["--tmp-slot"] || false
34
- replication_slot = cli["--slot"]
35
- replication_slot = replication_slot.presence || "wal_watcher_#{SecureRandom.alphanumeric(4)}" if use_temporary_slot
36
- publications = cli["--publication"] || []
37
- replicator_class = cli["--replicator"].presence&.constantize || Wal::Replicator
38
-
39
- puts "Watcher started for #{replication_slot} slot (#{publications.join(", ")})"
40
- replicator_class
41
- .new(replication_slot:, use_temporary_slot:, db_config:)
42
- .replicate_forever(Wal::LoggingWatcher.new(replication_slot, watcher), publications:)
43
- puts "Watcher finished for #{replication_slot}"
44
- end
45
-
46
31
  if cli["watch"]
47
- watch(db_config, cli)
32
+ config = {
33
+ "watcher" => cli["--watcher"],
34
+ "temporary" => cli["--tmp-slot"],
35
+ "publications" => cli["--publication"] || [],
36
+ "replicator" => cli["--replicator"],
37
+ }
38
+ slot = cli["--slot"].presence || "wal_watcher_#{SecureRandom.alphanumeric(4)}"
39
+ runner = Wal::Runner.new(config: { "slots" => { slot => config } }, db_config:)
40
+ runner.start
48
41
  elsif cli["start"]
49
- runner = Wal::Runner.new(config_file: cli["<config-file>"], db_config:)
42
+ config = YAML.load_file(cli["<config-file>"])
43
+ runner = Wal::Runner.new(config:, db_config:)
50
44
  runner.start
51
45
  end
@@ -5,6 +5,8 @@ module Wal
5
5
  def initialize(slot, watcher)
6
6
  @slot = slot
7
7
  @watcher = watcher
8
+ @actions = Set.new
9
+ @tables = Set.new
8
10
  end
9
11
 
10
12
  def should_watch_table?(table)
@@ -18,25 +20,40 @@ module Wal
18
20
  def on_event(event)
19
21
  case event
20
22
  when Wal::BeginTransactionEvent
21
- @start = Time.now
23
+ @start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
22
24
  @count = 0
23
- if event.estimated_size > 0
24
- Wal.logger&.debug("[#{@slot}] Begin transaction=#{event.transaction_id} size=#{event.estimated_size}")
25
- end
25
+ Wal.logger&.debug("[#{@slot}] Begin transaction=#{event.transaction_id} size=#{event.estimated_size}")
26
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}")
27
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start) * 1000).round(3)
28
+ message = "[#{@slot}] Commit transaction=#{event.transaction_id} elapsed=#{elapsed} events=#{@count}"
29
+ message << " actions=#{@actions.sort.join(",")}" unless @actions.empty?
30
+ message << " tables=#{@tables.sort.join(",")}" unless @tables.empty?
31
+ if @count > 1
32
+ Wal.logger&.info(message)
33
+ else
34
+ Wal.logger&.debug(message)
30
35
  end
36
+ @actions.clear
37
+ @tables.clear
31
38
  when Wal::InsertEvent
32
39
  Wal.logger&.debug("[#{@slot}] Insert transaction=#{event.transaction_id} table=#{event.table} primary_key=#{event.primary_key}")
33
40
  @count += 1
41
+ @actions << :insert
42
+ @tables << event.table
34
43
  when Wal::UpdateEvent
35
- Wal.logger&.debug("[#{@slot}] Update transaction=#{event.transaction_id} table=#{event.table} primary_key=#{event.primary_key}")
44
+ if Wal.logger&.level >= Logger::DEBUG
45
+ message = "[#{@slot}] Update transaction=#{event.transaction_id} table=#{event.table} primary_key=#{event.primary_key}"
46
+ message << " changed=#{event.diff.keys.join(",")}" if event.diff && !event.diff.empty?
47
+ Wal.logger&.debug(message)
48
+ end
36
49
  @count += 1
50
+ @actions << :update
51
+ @tables << event.table
37
52
  when Wal::DeleteEvent
38
53
  Wal.logger&.debug("[#{@slot}] Delete transaction=#{event.transaction_id} table=#{event.table} primary_key=#{event.primary_key}")
39
54
  @count += 1
55
+ @actions << :delete
56
+ @tables << event.table
40
57
  else
41
58
  @count += 1
42
59
  end
data/lib/wal/runner.rb CHANGED
@@ -32,30 +32,38 @@ module Wal
32
32
  Thread.new(slot, watcher, temporary, publications) do |replication_slot, watcher, use_temporary_slot, publications|
33
33
  retries = 0
34
34
  replication_slot = "#{replication_slot}_#{SecureRandom.alphanumeric(4)}" if use_temporary_slot
35
- puts "Watcher started for #{replication_slot} slot (#{publications.join(", ")})"
35
+ puts "[#{replication_slot}] Watcher started for #{replication_slot} slot (#{publications.join(", ")})"
36
36
 
37
37
  begin
38
+ Wal.hooks[:on_slot_start]&.call(slot, config)
39
+
38
40
  replicator_class
39
41
  .new(db_config:, **replicator_params, replication_slot:, use_temporary_slot:)
40
42
  .replicate_forever(Wal::LoggingWatcher.new(replication_slot, watcher), publications:)
43
+
44
+ Wal.hooks[:on_slot_finish]&.call(slot, config)
45
+
41
46
  if auto_restart
42
47
  backoff_time = backoff_exponent ? (backoff * retries) ** backoff_exponent : backoff
43
- puts "Watcher finished for #{replication_slot}, auto restarting in #{backoff_time.floor(2)}..."
48
+ puts "[#{replication_slot}] Watcher finished for #{replication_slot}, auto restarting in #{backoff_time.floor(2)}..."
44
49
  sleep backoff_time
45
- puts "Restarting #{replication_slot}"
50
+ puts "[#{replication_slot}] Restarting"
46
51
  redo
47
52
  end
48
53
  rescue ArgumentError
49
54
  raise
50
55
  rescue StandardError => err
56
+ Wal.hooks[:on_slot_error]&.call(err, slot, config)
57
+
58
+ Wal.logger&.error("[#{replication_slot}] Error #{err}")
59
+ Wal.logger&.error([err.message, *err.backtrace].join("\n"))
60
+
51
61
  if retries < max_retries
52
- Wal.logger&.error("[#{replication_slot}] Error #{err}")
53
- Wal.logger&.error([err.message, *err.backtrace].join("\n"))
54
62
  retries += 1
55
63
  backoff_time = backoff_exponent ? (backoff * retries) ** backoff_exponent : backoff
56
- puts "Restarting #{replication_slot} in #{backoff_time.floor(2)}s..."
64
+ puts "#{replication_slot}] Restarting #{replication_slot} in #{backoff_time.floor(2)}s..."
57
65
  sleep backoff_time
58
- puts "Restarting #{replication_slot}"
66
+ puts "#{replication_slot}] Restarting #{replication_slot}"
59
67
  retry
60
68
  end
61
69
  raise
@@ -79,24 +87,38 @@ module Wal
79
87
  end
80
88
  end
81
89
 
82
- attr_reader :config_file, :db_config
90
+ attr_reader :config, :db_config
83
91
 
84
- def initialize(config_file:, db_config:)
85
- @config_file = config_file
92
+ def initialize(config:, db_config:)
93
+ @config = config
86
94
  @db_config = db_config
87
95
  @child_pids = []
88
96
  end
89
97
 
90
98
  def start
91
- slots = YAML.load_file(config_file)["slots"]
92
- workers_slots = slots.group_by { |_slot, config| config["worker"] || "default" }
99
+ workers_slots = config["slots"].group_by { |_slot, slot_config| slot_config["worker"] || "default" }
100
+
101
+ if workers_slots.size == 1
102
+ run_single_worker(workers_slots.first)
103
+ else
104
+ run_forked_workers(workers_slots)
105
+ end
106
+ end
107
+
108
+ def run_single_worker((worker_name, slot_configs))
109
+ @ping_thread = start_ping_thread
110
+ puts "[#{worker_name}] Starting worker process (PID: #{Process.pid})"
111
+ worker = Worker.new(name: worker_name, slot_configs: slot_configs, db_config: db_config)
112
+ worker.run
113
+ end
93
114
 
94
- Wal.fork_hooks[:before_fork]&.call
115
+ def run_forked_workers(workers_slots)
116
+ Wal.hooks[:before_fork]&.call(workers_slots)
95
117
 
96
118
  workers_slots.each do |worker_name, slot_configs|
97
119
  pid = fork_worker(worker_name, slot_configs)
98
120
  @child_pids << pid
99
- puts "Spawned worker '#{worker_name}' with PID #{pid}"
121
+ puts "[#{worker_name}] Spawned worker '#{worker_name}' with PID #{pid}"
100
122
  end
101
123
 
102
124
  @ping_thread = start_ping_thread
@@ -108,7 +130,7 @@ module Wal
108
130
 
109
131
  def fork_worker(worker_name, slot_configs)
110
132
  Process.fork do
111
- Wal.fork_hooks[:after_fork]&.call
133
+ Wal.hooks[:after_fork]&.call(worker_name, slot_configs)
112
134
  puts "[#{worker_name}] Starting worker process (PID: #{Process.pid})"
113
135
  worker = Worker.new(name: worker_name, slot_configs: slot_configs, db_config: db_config)
114
136
  worker.run
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.32"
4
+ VERSION = "0.0.34"
5
5
  end
data/lib/wal.rb CHANGED
@@ -16,22 +16,30 @@ require_relative "wal/version"
16
16
  module Wal
17
17
  class << self
18
18
  attr_accessor :logger
19
- attr_accessor :fork_hooks
19
+ attr_accessor :hooks
20
20
 
21
21
  def logger
22
22
  @logger ||= Logger.new($stdout, level: :info)
23
23
  end
24
24
 
25
- def fork_hooks
26
- @fork_hooks ||= {}
25
+ def hooks
26
+ @hooks ||= {}
27
+ end
28
+
29
+ def on_slot_finished(&block)
30
+ hooks[:on_slot_finished] = block
31
+ end
32
+
33
+ def on_slot_error(&block)
34
+ hooks[:on_slot_error] = block
27
35
  end
28
36
 
29
37
  def before_fork(&block)
30
- fork_hooks[:before_fork] = block
38
+ hooks[:before_fork] = block
31
39
  end
32
40
 
33
41
  def after_fork(&block)
34
- fork_hooks[:after_fork] = block
42
+ hooks[:after_fork] = block
35
43
  end
36
44
  end
37
45
 
data/rbi/wal.rbi CHANGED
@@ -7,17 +7,26 @@ module Wal
7
7
  UpdateEvent,
8
8
  DeleteEvent,
9
9
  ) }
10
- VERSION = "0.0.32"
10
+ VERSION = "0.0.34"
11
11
 
12
12
  class << self
13
13
  sig { returns(T.class_of(Logger)) }
14
14
  attr_accessor :logger
15
15
 
16
- sig { params(block: T.proc.void).void }
16
+ sig { params(block: T.proc.params(worker_name: T.untyped).void).void }
17
17
  def before_fork(&block); end
18
18
 
19
- sig { params(block: T.proc.void).void }
19
+ sig { params(block: T.proc.params(worker_name: String, slot_configs: T.untyped).void).void }
20
20
  def after_fork(&block); end
21
+
22
+ sig { params(block: T.proc.params(slot: String, config: T.untyped).void).void }
23
+ def on_slot_start(&block); end
24
+
25
+ sig { params(block: T.proc.params(error: StandardError, slot: String, config: T.untyped).void).void }
26
+ def on_slot_error(&block); end
27
+
28
+ sig { params(block: T.proc.params(slot: String, config: T.untyped).void).void }
29
+ def on_slot_finish(&block); end
21
30
  end
22
31
 
23
32
  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.32
4
+ version: 0.0.34
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Navarro
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-01-28 00:00:00.000000000 Z
10
+ date: 2026-02-11 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: pg