wal 0.0.16 → 0.0.19
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/README.md +29 -19
- data/exe/wal +63 -8
- data/lib/wal/replicator.rb +1 -0
- data/lib/wal/version.rb +1 -1
- data/lib/wal.rb +8 -0
- data/rbi/wal.rbi +9 -1
- 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: 8547683e990bc915e382feecac5220a3b6b31a3d7e59c0891511f30c2eec5d19
|
4
|
+
data.tar.gz: e1bbac497d67a1cf76c8e56a002f45bf12849fe4fdfb4415ea3d8ee573e44e81
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f119216774509c4056ed84ccd29d43c9d743d67c3365289b2b18be0b92dd2117f543bf282fb6071abdf27d3774a2e1326238ab73e37c4c7e55bd5267c6583b96
|
7
|
+
data.tar.gz: d938d438d7d7b79457dba62a4460fba9e9523f953a618eacf4706ac1cf01293aafdbbf71ec35cf8a0a5156ea4b38a4e138c0cc7a373c38f6208f6136d926bd17
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@ Wal is a framework that lets you hook into Postgres WAL events directly from you
|
|
4
4
|
|
5
5
|
Unlike using database triggers, Wal allows you to keep your logic in your application code while still reacting to persistence events coming from the database.
|
6
6
|
|
7
|
-
Also, unlike ActiveRecord callbacks, these events are guaranteed by
|
7
|
+
Also, unlike ActiveRecord callbacks, these events are guaranteed by Postgres to be 100% consistent, ensuring you never miss one.
|
8
8
|
|
9
9
|
# Getting started
|
10
10
|
|
@@ -45,42 +45,52 @@ class DenormalizePostWatcher < Wal::RecordWatcher
|
|
45
45
|
|
46
46
|
# When a `Post` title or body is changed, we update its `DenormalizedPost` record
|
47
47
|
on_update Post, changed: [:title, :body] do |event|
|
48
|
-
DenormalizedPost
|
49
|
-
|
50
|
-
|
51
|
-
|
48
|
+
DenormalizedPost
|
49
|
+
.where(post_id: event.primary_key)
|
50
|
+
.update_all(
|
51
|
+
title: event.new["title"],
|
52
|
+
body: event.new["body"],
|
53
|
+
)
|
52
54
|
end
|
53
55
|
|
54
56
|
# When a `Post` category changes, we also update its `DenormalizedPost` record
|
55
57
|
on_update Post, changed: [:category_id] do |event|
|
56
|
-
DenormalizedPost
|
57
|
-
|
58
|
-
|
59
|
-
|
58
|
+
DenormalizedPost
|
59
|
+
.where(post_id: event.primary_key)
|
60
|
+
.update_all(
|
61
|
+
category_id: event.new["category_id"],
|
62
|
+
category_name: Category.find_by(id: event.new["category_id"])&.name,
|
63
|
+
)
|
60
64
|
end
|
61
65
|
|
62
66
|
# When a `Category` changes, we update all the `DenormalizedPosts` referencing it
|
63
67
|
on_update Category, changed: [:name] do |event|
|
64
|
-
DenormalizedPost
|
65
|
-
|
66
|
-
|
68
|
+
DenormalizedPost
|
69
|
+
.where(category_id: event.primary_key)
|
70
|
+
.update_all(
|
71
|
+
category_name: event.new["name"],
|
72
|
+
)
|
67
73
|
end
|
68
74
|
|
69
75
|
# Finally when a `Category` is deleted, we clear all the `DenormalizedPosts` referencing it
|
70
|
-
|
71
|
-
DenormalizedPost
|
72
|
-
category_id:
|
73
|
-
|
74
|
-
|
76
|
+
on_delete Category do |event|
|
77
|
+
DenormalizedPost
|
78
|
+
.where(category_id: event.primary_key)
|
79
|
+
.update_all(
|
80
|
+
category_id: nil,
|
81
|
+
category_name: nil,
|
82
|
+
)
|
75
83
|
end
|
76
84
|
end
|
77
85
|
```
|
78
86
|
|
79
87
|
You might wonder: *Why not just use ActiveRecord callbacks for this?*
|
80
88
|
|
81
|
-
|
89
|
+
And while it is hard to justify that for our simple example, ActiveRecord callbacks are not guaranteed to always run. Depending on the methods you use to perform the changes, they can be skipped.
|
90
|
+
|
91
|
+
Wal ensures every single change is captured. *Even when updates happen directly in the database and bypass Rails entirely*. That's the main reason to use it: when you need 100% consistency.
|
82
92
|
|
83
|
-
|
93
|
+
Usually one could resort into database triggers when full consistency is required, but running and maintaining application level code on the database tends to be painful. Wal let's you do the same but at the application level.
|
84
94
|
|
85
95
|
## Configuring the Watcher
|
86
96
|
|
data/exe/wal
CHANGED
@@ -28,18 +28,59 @@ 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
|
+
|
31
72
|
if cli["watch"]
|
32
73
|
watcher = cli["--watcher"].constantize.new
|
33
74
|
use_temporary_slot = cli["--tmp-slot"] || false
|
34
75
|
replication_slot = cli["--slot"]
|
35
76
|
replication_slot = replication_slot.presence || "wal_watcher_#{SecureRandom.alphanumeric(4)}" if use_temporary_slot
|
36
77
|
publications = cli["--publication"] || []
|
37
|
-
|
78
|
+
replicator_class = cli["--replicator"].presence&.constantize || Wal::Replicator
|
38
79
|
|
39
80
|
puts "Watcher started for #{replication_slot} slot (#{publications.join(", ")})"
|
40
|
-
replicator
|
41
|
-
|
42
|
-
|
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:)
|
43
84
|
puts "Watcher finished for #{replication_slot}"
|
44
85
|
|
45
86
|
elsif cli["start"]
|
@@ -49,17 +90,31 @@ elsif cli["start"]
|
|
49
90
|
watcher = config["watcher"].constantize.new
|
50
91
|
temporary = config["temporary"] || false
|
51
92
|
publications = config["publications"] || []
|
52
|
-
|
93
|
+
replicator_class = config["replicator"].presence&.constantize || Wal::Replicator
|
94
|
+
retries = config["retries"]&.to_i || 5
|
53
95
|
|
54
96
|
Thread.new(slot, watcher, temporary, publications) do |replication_slot, watcher, use_temporary_slot, publications|
|
55
97
|
replication_slot = "#{replication_slot}_#{SecureRandom.alphanumeric(4)}" if use_temporary_slot
|
56
98
|
puts "Watcher started for #{replication_slot} slot (#{publications.join(", ")})"
|
57
99
|
|
58
|
-
replicator
|
59
|
-
|
60
|
-
|
100
|
+
replicator = replicator_class.new(replication_slot:, use_temporary_slot:, db_config:)
|
101
|
+
replicator = Wal::LoggingReplicator.new(replication_slot, replicator)
|
102
|
+
|
103
|
+
begin
|
104
|
+
replicator.replicate_forever(watcher, publications:)
|
105
|
+
rescue StandardError => err
|
106
|
+
if retries > 0
|
107
|
+
Wal.logger&.error("[#{replication_slot}] Error #{err}")
|
108
|
+
retries -= 1
|
109
|
+
sleep 2 ** retries
|
110
|
+
retry
|
111
|
+
end
|
112
|
+
raise
|
113
|
+
end
|
61
114
|
|
62
115
|
puts "Watcher finished for #{replication_slot}"
|
116
|
+
|
117
|
+
Process.kill("TERM", Process.pid)
|
63
118
|
end
|
64
119
|
end
|
65
120
|
|
data/lib/wal/replicator.rb
CHANGED
@@ -88,6 +88,7 @@ module Wal
|
|
88
88
|
|
89
89
|
in XLogData(lsn:, data: PG::Replication::PGOutput::Message(prefix: "wal_ping"))
|
90
90
|
watch_conn.standby_status_update(write_lsn: [watch_conn.last_confirmed_lsn, lsn].compact.max)
|
91
|
+
next
|
91
92
|
|
92
93
|
in XLogData(data: PG::Replication::PGOutput::Message(prefix:, content:)) if watcher.valid_context_prefix? prefix
|
93
94
|
begin
|
data/lib/wal/version.rb
CHANGED
data/lib/wal.rb
CHANGED
@@ -12,6 +12,14 @@ require_relative "wal/railtie"
|
|
12
12
|
require_relative "wal/version"
|
13
13
|
|
14
14
|
module Wal
|
15
|
+
class << self
|
16
|
+
attr_accessor :logger
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.configure(&block)
|
20
|
+
yield self
|
21
|
+
end
|
22
|
+
|
15
23
|
class BeginTransactionEvent < Data.define(:transaction_id, :lsn, :final_lsn, :timestamp)
|
16
24
|
def estimated_size
|
17
25
|
final_lsn - lsn
|
data/rbi/wal.rbi
CHANGED
@@ -7,7 +7,15 @@ module Wal
|
|
7
7
|
UpdateEvent,
|
8
8
|
DeleteEvent,
|
9
9
|
) }
|
10
|
-
VERSION = "0.0.
|
10
|
+
VERSION = "0.0.19"
|
11
|
+
|
12
|
+
class << self
|
13
|
+
sig { returns(T.class_of(Logger)) }
|
14
|
+
attr_accessor :logger
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { params(block: T.proc.params(config: T.class_of(Wal)).void).void }
|
18
|
+
def self.configure(&block); end
|
11
19
|
|
12
20
|
class BeginTransactionEvent < T::Struct
|
13
21
|
prop :transaction_id, Integer, immutable: true
|
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.
|
4
|
+
version: 0.0.19
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rodrigo Navarro
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-09-
|
10
|
+
date: 2025-09-20 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: pg
|