wal 0.0.0 → 0.0.2
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/.rspec +1 -0
- data/README.md +49 -0
- data/Rakefile +9 -1
- data/exe/wal +64 -0
- data/lib/wal/active_record_context_extension.rb +15 -0
- data/lib/wal/noop_watcher.rb +12 -0
- data/lib/wal/record_watcher.rb +389 -0
- data/lib/wal/replicator.rb +205 -0
- data/lib/wal/streaming_watcher.rb +74 -0
- data/lib/wal/version.rb +2 -1
- data/lib/wal/watcher.rb +95 -0
- data/lib/wal.rb +128 -1
- data/rbi/wal.rbi +295 -0
- data/sig/wal.rbs +184 -2
- data/sorbet/config +7 -0
- data/sorbet/rbi/annotations/.gitattributes +1 -0
- data/sorbet/rbi/annotations/activemodel.rbi +89 -0
- data/sorbet/rbi/annotations/activerecord.rbi +98 -0
- data/sorbet/rbi/annotations/activesupport.rbi +463 -0
- data/sorbet/rbi/annotations/minitest.rbi +119 -0
- data/sorbet/rbi/annotations/rainbow.rbi +269 -0
- data/sorbet/rbi/gems/.gitattributes +1 -0
- data/sorbet/rbi/gems/actioncable@8.0.2.rbi +9 -0
- data/sorbet/rbi/gems/actionmailbox@8.0.2.rbi +9 -0
- data/sorbet/rbi/gems/actionmailer@8.0.2.rbi +9 -0
- data/sorbet/rbi/gems/actionpack@8.0.2.rbi +21122 -0
- data/sorbet/rbi/gems/actiontext@8.0.2.rbi +9 -0
- data/sorbet/rbi/gems/actionview@8.0.2.rbi +16423 -0
- data/sorbet/rbi/gems/activejob@8.0.2.rbi +9 -0
- data/sorbet/rbi/gems/activemodel@8.0.2.rbi +6866 -0
- data/sorbet/rbi/gems/activerecord@8.0.2.rbi +43227 -0
- data/sorbet/rbi/gems/activestorage@8.0.2.rbi +9 -0
- data/sorbet/rbi/gems/activesupport@8.0.2.rbi +21110 -0
- data/sorbet/rbi/gems/ast@2.4.3.rbi +585 -0
- data/sorbet/rbi/gems/base64@0.3.0.rbi +545 -0
- data/sorbet/rbi/gems/benchmark@0.4.1.rbi +619 -0
- data/sorbet/rbi/gems/bigdecimal@3.2.2.rbi +78 -0
- data/sorbet/rbi/gems/builder@3.3.0.rbi +9 -0
- data/sorbet/rbi/gems/commander@5.0.0.rbi +9 -0
- data/sorbet/rbi/gems/concurrent-ruby@1.3.5.rbi +11657 -0
- data/sorbet/rbi/gems/connection_pool@2.5.3.rbi +9 -0
- data/sorbet/rbi/gems/crass@1.0.6.rbi +623 -0
- data/sorbet/rbi/gems/date@3.4.1.rbi +75 -0
- data/sorbet/rbi/gems/diff-lcs@1.6.2.rbi +1134 -0
- data/sorbet/rbi/gems/docker-api@2.4.0.rbi +1719 -0
- data/sorbet/rbi/gems/docopt@0.6.1.rbi +9 -0
- data/sorbet/rbi/gems/drb@2.2.3.rbi +1661 -0
- data/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
- data/sorbet/rbi/gems/excon@1.2.7.rbi +1514 -0
- data/sorbet/rbi/gems/globalid@1.2.1.rbi +9 -0
- data/sorbet/rbi/gems/highline@3.0.1.rbi +9 -0
- data/sorbet/rbi/gems/i18n@1.14.7.rbi +2359 -0
- data/sorbet/rbi/gems/io-console@0.8.0.rbi +9 -0
- data/sorbet/rbi/gems/logger@1.7.0.rbi +963 -0
- data/sorbet/rbi/gems/loofah@2.24.1.rbi +1105 -0
- data/sorbet/rbi/gems/mail@2.8.1.rbi +9 -0
- data/sorbet/rbi/gems/marcel@1.0.4.rbi +9 -0
- data/sorbet/rbi/gems/mini_mime@1.1.5.rbi +9 -0
- data/sorbet/rbi/gems/minitest@5.25.5.rbi +1704 -0
- data/sorbet/rbi/gems/multi_json@1.15.0.rbi +268 -0
- data/sorbet/rbi/gems/net-imap@0.5.9.rbi +9 -0
- data/sorbet/rbi/gems/net-pop@0.1.2.rbi +9 -0
- data/sorbet/rbi/gems/net-protocol@0.2.2.rbi +292 -0
- data/sorbet/rbi/gems/net-smtp@0.5.1.rbi +9 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +159 -0
- data/sorbet/rbi/gems/nio4r@2.7.4.rbi +9 -0
- data/sorbet/rbi/gems/nokogiri@1.18.8.rbi +8206 -0
- data/sorbet/rbi/gems/ostruct@0.6.2.rbi +354 -0
- data/sorbet/rbi/gems/parallel@1.27.0.rbi +291 -0
- data/sorbet/rbi/gems/parlour@9.1.1.rbi +3071 -0
- data/sorbet/rbi/gems/parser@3.3.8.0.rbi +7338 -0
- data/sorbet/rbi/gems/pg-replication-protocol@0.0.7.rbi +633 -0
- data/sorbet/rbi/gems/pg@1.5.9.rbi +2806 -0
- data/sorbet/rbi/gems/pp@0.6.2.rbi +368 -0
- data/sorbet/rbi/gems/prettyprint@0.2.0.rbi +477 -0
- data/sorbet/rbi/gems/prism@1.4.0.rbi +41732 -0
- data/sorbet/rbi/gems/psych@5.2.3.rbi +2435 -0
- data/sorbet/rbi/gems/racc@1.8.1.rbi +160 -0
- data/sorbet/rbi/gems/rack-session@2.1.1.rbi +727 -0
- data/sorbet/rbi/gems/rack-test@2.2.0.rbi +734 -0
- data/sorbet/rbi/gems/rack@3.1.16.rbi +4940 -0
- data/sorbet/rbi/gems/rackup@2.2.1.rbi +230 -0
- data/sorbet/rbi/gems/rails-dom-testing@2.3.0.rbi +858 -0
- data/sorbet/rbi/gems/rails-html-sanitizer@1.6.2.rbi +785 -0
- data/sorbet/rbi/gems/rails@8.0.2.rbi +9 -0
- data/sorbet/rbi/gems/railties@8.0.2.rbi +3865 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +403 -0
- data/sorbet/rbi/gems/rake@13.2.1.rbi +3120 -0
- data/sorbet/rbi/gems/rbi@0.3.6.rbi +6893 -0
- data/sorbet/rbi/gems/rbs@3.9.4.rbi +6978 -0
- data/sorbet/rbi/gems/rdoc@6.12.0.rbi +12760 -0
- data/sorbet/rbi/gems/reline@0.6.0.rbi +2451 -0
- data/sorbet/rbi/gems/rexml@3.4.1.rbi +5240 -0
- data/sorbet/rbi/gems/rspec-core@3.13.4.rbi +11348 -0
- data/sorbet/rbi/gems/rspec-expectations@3.13.5.rbi +8189 -0
- data/sorbet/rbi/gems/rspec-mocks@3.13.5.rbi +5350 -0
- data/sorbet/rbi/gems/rspec-sorbet@1.9.2.rbi +164 -0
- data/sorbet/rbi/gems/rspec-support@3.13.4.rbi +1630 -0
- data/sorbet/rbi/gems/rspec@3.13.1.rbi +83 -0
- data/sorbet/rbi/gems/securerandom@0.4.1.rbi +75 -0
- data/sorbet/rbi/gems/spoom@1.6.3.rbi +6985 -0
- data/sorbet/rbi/gems/stringio@3.1.5.rbi +9 -0
- data/sorbet/rbi/gems/tapioca@0.16.11.rbi +3628 -0
- data/sorbet/rbi/gems/testcontainers-core@0.2.0.rbi +1005 -0
- data/sorbet/rbi/gems/testcontainers-postgres@0.2.0.rbi +145 -0
- data/sorbet/rbi/gems/thor@1.3.2.rbi +4378 -0
- data/sorbet/rbi/gems/timeout@0.4.3.rbi +157 -0
- data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +5918 -0
- data/sorbet/rbi/gems/uri@1.0.3.rbi +2349 -0
- data/sorbet/rbi/gems/useragent@0.16.11.rbi +9 -0
- data/sorbet/rbi/gems/websocket-driver@0.8.0.rbi +9 -0
- data/sorbet/rbi/gems/websocket-extensions@0.1.5.rbi +9 -0
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
- data/sorbet/rbi/gems/yard@0.9.37.rbi +18379 -0
- data/sorbet/rbi/gems/zeitwerk@2.7.3.rbi +9 -0
- data/sorbet/tapioca/config.yml +5 -0
- data/sorbet/tapioca/require.rb +12 -0
- metadata +231 -6
@@ -0,0 +1,205 @@
|
|
1
|
+
# typed: false
|
2
|
+
|
3
|
+
require "ostruct"
|
4
|
+
|
5
|
+
module Wal
|
6
|
+
# Responsible to hook into a Postgres logical replication slot and stream the changes to a specific `Watcher`.
|
7
|
+
# Also it supports inject "contexts" into the replication events.
|
8
|
+
class Replicator
|
9
|
+
extend T::Sig
|
10
|
+
include PG::Replication::Protocol
|
11
|
+
|
12
|
+
sig do
|
13
|
+
params(
|
14
|
+
replication_slot: String,
|
15
|
+
use_temporary_slot: T::Boolean,
|
16
|
+
db_config: T::Hash[Symbol, T.untyped],
|
17
|
+
).void
|
18
|
+
end
|
19
|
+
def initialize(
|
20
|
+
replication_slot:,
|
21
|
+
use_temporary_slot: false,
|
22
|
+
db_config: ActiveRecord::Base.configurations.configs_for(name: "primary").configuration_hash
|
23
|
+
)
|
24
|
+
@db_config = db_config
|
25
|
+
@replication_slot = replication_slot
|
26
|
+
@use_temporary_slot = use_temporary_slot
|
27
|
+
end
|
28
|
+
|
29
|
+
sig { params(watcher: Watcher, publications: T::Array[String]).void }
|
30
|
+
def replicate_forever(watcher, publications:)
|
31
|
+
replication = replicate(watcher, publications:)
|
32
|
+
loop { replication.next }
|
33
|
+
rescue StopIteration
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
sig { params(watcher: Watcher, publications: T::Array[String]).returns(T::Enumerator::Lazy[Event]) }
|
38
|
+
def replicate(watcher, publications:)
|
39
|
+
watch_conn = PG.connect(
|
40
|
+
dbname: @db_config[:database],
|
41
|
+
host: @db_config[:host],
|
42
|
+
user: @db_config[:username],
|
43
|
+
password: @db_config[:password].presence,
|
44
|
+
port: @db_config[:port].presence,
|
45
|
+
replication: "database",
|
46
|
+
)
|
47
|
+
|
48
|
+
begin
|
49
|
+
watch_conn.query(<<~SQL)
|
50
|
+
CREATE_REPLICATION_SLOT #{@replication_slot} #{@use_temporary_slot ? "TEMPORARY" : ""} LOGICAL "pgoutput"
|
51
|
+
SQL
|
52
|
+
rescue PG::DuplicateObject
|
53
|
+
# We are fine, we already have created this slot in a previous run
|
54
|
+
end
|
55
|
+
|
56
|
+
tables = {}
|
57
|
+
context = {}
|
58
|
+
transaction_id = nil
|
59
|
+
|
60
|
+
watch_conn.start_pgoutput_replication_slot(@replication_slot, publications, messages: true).filter_map do |msg|
|
61
|
+
case msg
|
62
|
+
in XLogData(data: PG::Replication::PGOutput::Relation(oid:, name:, columns:))
|
63
|
+
tables[oid] = Table.new(
|
64
|
+
# TODO: for now we are forcing an id column here, but that is not really correct
|
65
|
+
primary_key_colums: columns.any? { |col| col.name == "id" } ? ["id"] : [],
|
66
|
+
name:,
|
67
|
+
columns: columns.map do |col|
|
68
|
+
Column.new(
|
69
|
+
name: col.name,
|
70
|
+
decoder: ActiveRecord::Base.connection.lookup_cast_type_from_column(
|
71
|
+
# We have to create this OpenStruct because weird AR API reasons...
|
72
|
+
# And the `sql_type` param luckly doesn't really matter for our use case
|
73
|
+
::OpenStruct.new(oid: col.oid, fmod: col.modifier, sql_type: "")
|
74
|
+
),
|
75
|
+
)
|
76
|
+
end
|
77
|
+
)
|
78
|
+
next
|
79
|
+
|
80
|
+
in XLogData(lsn:, data: PG::Replication::PGOutput::Begin(xid:, timestamp:, final_lsn:))
|
81
|
+
transaction_id = xid
|
82
|
+
context = {}
|
83
|
+
BeginTransactionEvent.new(
|
84
|
+
transaction_id:,
|
85
|
+
lsn:,
|
86
|
+
final_lsn:,
|
87
|
+
timestamp:,
|
88
|
+
).tap { |event| watcher.on_event(event) }
|
89
|
+
|
90
|
+
in XLogData(lsn:, data: PG::Replication::PGOutput::Commit(timestamp:))
|
91
|
+
CommitTransactionEvent.new(
|
92
|
+
transaction_id:,
|
93
|
+
lsn:,
|
94
|
+
context:,
|
95
|
+
timestamp:,
|
96
|
+
).tap do |event|
|
97
|
+
watcher.on_event(event)
|
98
|
+
watch_conn.standby_status_update(write_lsn: lsn)
|
99
|
+
end
|
100
|
+
|
101
|
+
in XLogData(data: PG::Replication::PGOutput::Message(prefix:, content:)) if watcher.valid_context_prefix? prefix
|
102
|
+
begin
|
103
|
+
context = JSON.parse(content).presence || {}
|
104
|
+
next
|
105
|
+
rescue JSON::ParserError
|
106
|
+
# Invalid context received, just ignore
|
107
|
+
end
|
108
|
+
|
109
|
+
in XLogData(lsn:, data: PG::Replication::PGOutput::Insert(oid:, new:))
|
110
|
+
table = tables[oid]
|
111
|
+
next unless watcher.should_watch_table? table.name
|
112
|
+
new_data = table.decode_row(new)
|
113
|
+
record_id = table.primary_key(new_data)
|
114
|
+
next unless record_id
|
115
|
+
|
116
|
+
InsertEvent.new(
|
117
|
+
transaction_id:,
|
118
|
+
lsn:,
|
119
|
+
context:,
|
120
|
+
table: table.name,
|
121
|
+
primary_key: record_id,
|
122
|
+
new: new_data,
|
123
|
+
).tap { |event| watcher.on_event(event) }
|
124
|
+
|
125
|
+
in XLogData(lsn:, data: PG::Replication::PGOutput::Update(oid:, new:, old:))
|
126
|
+
table = tables[oid]
|
127
|
+
next unless watcher.should_watch_table? table.name
|
128
|
+
old_data = table.decode_row(old)
|
129
|
+
new_data = table.decode_row(new)
|
130
|
+
record_id = table.primary_key(new_data)
|
131
|
+
next unless record_id
|
132
|
+
|
133
|
+
UpdateEvent.new(
|
134
|
+
transaction_id:,
|
135
|
+
lsn:,
|
136
|
+
context:,
|
137
|
+
table: table.name,
|
138
|
+
primary_key: record_id,
|
139
|
+
old: old_data,
|
140
|
+
new: new_data,
|
141
|
+
).tap { |event| watcher.on_event(event) }
|
142
|
+
|
143
|
+
in XLogData(lsn:, data: PG::Replication::PGOutput::Delete(oid:, old:, key:))
|
144
|
+
table = tables[oid]
|
145
|
+
next unless watcher.should_watch_table? table.name
|
146
|
+
old_data = table.decode_row(old.presence || key)
|
147
|
+
record_id = table.primary_key(old_data)
|
148
|
+
next unless record_id
|
149
|
+
|
150
|
+
DeleteEvent.new(
|
151
|
+
transaction_id:,
|
152
|
+
lsn:,
|
153
|
+
context:,
|
154
|
+
table: table.name,
|
155
|
+
primary_key: record_id,
|
156
|
+
old: old_data,
|
157
|
+
).tap { |event| watcher.on_event(event) }
|
158
|
+
|
159
|
+
else
|
160
|
+
next
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
class Column < T::Struct
|
166
|
+
const :name, String
|
167
|
+
const :decoder, T.untyped
|
168
|
+
|
169
|
+
def decode(value)
|
170
|
+
decoder.deserialize(value)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class Table < T::Struct
|
175
|
+
const :name, String
|
176
|
+
const :primary_key_colums, T::Array[String]
|
177
|
+
const :columns, T::Array[Column]
|
178
|
+
|
179
|
+
def primary_key(decoded_row)
|
180
|
+
case primary_key_colums
|
181
|
+
in [key]
|
182
|
+
case decoded_row[key]
|
183
|
+
in Integer => id
|
184
|
+
id
|
185
|
+
in String => id
|
186
|
+
id
|
187
|
+
else
|
188
|
+
# Only supporting string and integer primary keys for now
|
189
|
+
nil
|
190
|
+
end
|
191
|
+
else
|
192
|
+
# Not supporting coumpound primary keys
|
193
|
+
nil
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def decode_row(values)
|
198
|
+
values
|
199
|
+
.zip(columns)
|
200
|
+
.map { |tuple, col| [col.name, col.decode(tuple.data)] }
|
201
|
+
.to_h
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
module Wal
|
4
|
+
# A watcher that streams all the events of each WAL transaction on a separate thread.
|
5
|
+
#
|
6
|
+
# Useful to improve the throughput, as it will allow you to process events while fetching for more in parallel.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# Watcher that persists all delete events as it arrives using a single database transaction, and without waiting
|
11
|
+
# for the full WAL log transaction to be finished.
|
12
|
+
#
|
13
|
+
# ```ruby
|
14
|
+
# class RegisterDeletesWalWatcher < Wal::StreamingWalWatcher
|
15
|
+
# sig { override.params(events: T::Enumerator[Event]).void }
|
16
|
+
# def on_transaction_events(events)
|
17
|
+
# DeletedApplicationRecord.transaction do
|
18
|
+
# events
|
19
|
+
# .lazy
|
20
|
+
# .filter { |event| event.is_a? DeleteEvent }
|
21
|
+
# .each { |event| DeletedApplicationRecord.create_from_event(event) }
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
# ```
|
26
|
+
class StreamingWatcher
|
27
|
+
extend T::Sig
|
28
|
+
extend T::Helpers
|
29
|
+
include Wal::Watcher
|
30
|
+
abstract!
|
31
|
+
|
32
|
+
sig { abstract.params(events: T::Enumerator[Event]).void }
|
33
|
+
def on_transaction_events(events); end
|
34
|
+
|
35
|
+
sig { params(event: BeginTransactionEvent).returns(Integer) }
|
36
|
+
def queue_size(event)
|
37
|
+
5_000
|
38
|
+
end
|
39
|
+
|
40
|
+
sig { override.params(event: Event).void }
|
41
|
+
def on_event(event)
|
42
|
+
case event
|
43
|
+
when BeginTransactionEvent
|
44
|
+
@queue = SizedQueue.new(queue_size(event))
|
45
|
+
|
46
|
+
event_stream = Enumerator.new do |y|
|
47
|
+
while (item = @queue.pop)
|
48
|
+
case item
|
49
|
+
when CommitTransactionEvent
|
50
|
+
y << item
|
51
|
+
break
|
52
|
+
else
|
53
|
+
y << item
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
@worker = Thread.new { on_transaction_events(event_stream) }
|
58
|
+
|
59
|
+
@queue << event
|
60
|
+
|
61
|
+
when CommitTransactionEvent
|
62
|
+
@queue << event
|
63
|
+
@worker.join
|
64
|
+
|
65
|
+
# We are cleaning this up to hint to Ruby GC that this can be freed before the next begin transaction arrives
|
66
|
+
@queue.clear
|
67
|
+
@queue = nil
|
68
|
+
|
69
|
+
else
|
70
|
+
@queue << event
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/wal/version.rb
CHANGED
data/lib/wal/watcher.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Wal
|
4
|
+
# Watcher is the core API used to hook into Postgres WAL log.
|
5
|
+
# The only required method on the API is the `on_event`, which will receive a WAL entry of the following events:
|
6
|
+
# - Transaction started: `Wal::BeginTransactionEvent`
|
7
|
+
# - Row inserted: `Wal::InsertEvent`
|
8
|
+
# - Row updated: `Wal::UpdateEvent`
|
9
|
+
# - Row deleted: `Wal::DeleteEvent`
|
10
|
+
# - Transaction committed: `Wal::CommitTransactionEvent`
|
11
|
+
#
|
12
|
+
# The `on_event` method will be called without any buffering, so it is up to implementators to aggregate them if
|
13
|
+
# desired. In practice, it is rarelly useful to implement this module directly for application level business logic,
|
14
|
+
# and instead it is more recomended using more specific ones, such as the `RecordWatcher` and `StreamingWalWatcher`.
|
15
|
+
module Watcher
|
16
|
+
extend T::Sig
|
17
|
+
extend T::Helpers
|
18
|
+
include Wal
|
19
|
+
abstract!
|
20
|
+
|
21
|
+
sig { abstract.params(event: Event).void }
|
22
|
+
def on_event(event); end
|
23
|
+
|
24
|
+
# Allows dropping the processing of any table
|
25
|
+
sig { params(table: String).returns(T::Boolean) }
|
26
|
+
def should_watch_table?(table)
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
# Check if the given context prefix should be allowed for this watcher
|
31
|
+
sig { params(prefix: String).returns(T::Boolean) }
|
32
|
+
def valid_context_prefix?(prefix)
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
# Include this module if you prefer to work with each event having its own method.
|
37
|
+
# This might be useful when you always want to process each type of event in a different way.
|
38
|
+
#
|
39
|
+
# Example:
|
40
|
+
#
|
41
|
+
# Watcher that calculates how much time passed between the begin and commit of a WAL transaction.
|
42
|
+
#
|
43
|
+
# ```ruby
|
44
|
+
# class MeasureTransactionTimeWatcher
|
45
|
+
# extend T::Sig
|
46
|
+
# include Wal::Watcher
|
47
|
+
# include Wal::Watcher::SeparatedEvents
|
48
|
+
#
|
49
|
+
# sig { params(event: BeginTransactionEvent).void }
|
50
|
+
# def on_begin(event)
|
51
|
+
# @start_time = Time.current
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# sig { params(event: CommitTransactionEvent).void }
|
55
|
+
# def on_commit(event)
|
56
|
+
# puts "Transaction processing time: #{Time.current - @start_time}"
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
# ```
|
60
|
+
module SeparatedEvents
|
61
|
+
extend T::Sig
|
62
|
+
|
63
|
+
sig { params(event: Event).void }
|
64
|
+
def on_event(event)
|
65
|
+
case event
|
66
|
+
when BeginTransactionEvent
|
67
|
+
on_begin(event)
|
68
|
+
when CommitTransactionEvent
|
69
|
+
on_commit(event)
|
70
|
+
when InsertEvent
|
71
|
+
on_insert(event)
|
72
|
+
when UpdateEvent
|
73
|
+
on_update(event)
|
74
|
+
when DeleteEvent
|
75
|
+
on_delete(event)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { params(event: BeginTransactionEvent).void }
|
80
|
+
def on_begin(event); end
|
81
|
+
|
82
|
+
sig { params(event: InsertEvent).void }
|
83
|
+
def on_insert(event); end
|
84
|
+
|
85
|
+
sig { params(event: UpdateEvent).void }
|
86
|
+
def on_update(event); end
|
87
|
+
|
88
|
+
sig { params(event: DeleteEvent).void }
|
89
|
+
def on_delete(event); end
|
90
|
+
|
91
|
+
sig { params(event: CommitTransactionEvent).void }
|
92
|
+
def on_commit(event); end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/wal.rb
CHANGED
@@ -1,6 +1,133 @@
|
|
1
|
-
#
|
1
|
+
# typed: strict
|
2
2
|
|
3
|
+
require "pg"
|
4
|
+
require "pg/replication"
|
5
|
+
require "active_support"
|
6
|
+
require "active_record"
|
7
|
+
require_relative "wal/watcher"
|
8
|
+
require_relative "wal/noop_watcher"
|
9
|
+
require_relative "wal/record_watcher"
|
10
|
+
require_relative "wal/streaming_watcher"
|
11
|
+
require_relative "wal/replicator"
|
12
|
+
require_relative "wal/active_record_context_extension"
|
3
13
|
require_relative "wal/version"
|
4
14
|
|
5
15
|
module Wal
|
16
|
+
Event = T.type_alias do
|
17
|
+
T.any(
|
18
|
+
BeginTransactionEvent,
|
19
|
+
CommitTransactionEvent,
|
20
|
+
InsertEvent,
|
21
|
+
UpdateEvent,
|
22
|
+
DeleteEvent,
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
class BeginTransactionEvent < T::Struct
|
27
|
+
extend T::Sig
|
28
|
+
|
29
|
+
const :transaction_id, Integer
|
30
|
+
const :lsn, Integer
|
31
|
+
const :final_lsn, Integer
|
32
|
+
const :timestamp, Time
|
33
|
+
|
34
|
+
sig { returns(Integer) }
|
35
|
+
def estimated_size
|
36
|
+
final_lsn - lsn
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class CommitTransactionEvent < T::Struct
|
41
|
+
const :transaction_id, Integer
|
42
|
+
const :lsn, Integer
|
43
|
+
const :context, T::Hash[String, T.untyped]
|
44
|
+
const :timestamp, Time
|
45
|
+
end
|
46
|
+
|
47
|
+
module ChangeEvent
|
48
|
+
extend T::Sig
|
49
|
+
|
50
|
+
sig { returns(T::Hash[String, [T.untyped, T.untyped]]) }
|
51
|
+
def diff
|
52
|
+
{}
|
53
|
+
end
|
54
|
+
|
55
|
+
sig { params(attribute: T.any(Symbol, String)).returns(T::Boolean) }
|
56
|
+
def changed_attribute?(attribute)
|
57
|
+
diff.key? attribute.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
sig { params(attribute: T.any(Symbol, String)).returns(T.untyped) }
|
61
|
+
def attribute(attribute)
|
62
|
+
if (changes = diff[attribute.to_s])
|
63
|
+
changes[1]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
sig { params(attribute: T.any(Symbol, String)).returns(T.nilable([T.untyped, T.untyped])) }
|
68
|
+
def attribute_changes(attribute)
|
69
|
+
diff[attribute.to_s]
|
70
|
+
end
|
71
|
+
|
72
|
+
sig { params(attribute: T.any(Symbol, String)).returns(T.untyped) }
|
73
|
+
def attribute_was(attribute)
|
74
|
+
if (changes = diff[attribute.to_s])
|
75
|
+
changes[0]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class InsertEvent < T::Struct
|
81
|
+
extend T::Sig
|
82
|
+
include ::Wal::ChangeEvent
|
83
|
+
|
84
|
+
const :transaction_id, Integer
|
85
|
+
const :lsn, Integer
|
86
|
+
const :context, T::Hash[String, T.untyped]
|
87
|
+
const :table, String
|
88
|
+
const :primary_key, T.untyped
|
89
|
+
const :new, T::Hash[String, T.untyped]
|
90
|
+
|
91
|
+
sig { returns(T::Hash[String, [T.untyped, T.untyped]]) }
|
92
|
+
def diff
|
93
|
+
new.transform_values { |val| [nil, val] }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class UpdateEvent < T::Struct
|
98
|
+
extend T::Sig
|
99
|
+
include ::Wal::ChangeEvent
|
100
|
+
|
101
|
+
const :transaction_id, Integer
|
102
|
+
const :lsn, Integer
|
103
|
+
const :context, T::Hash[String, T.untyped]
|
104
|
+
const :table, String
|
105
|
+
const :primary_key, T.untyped
|
106
|
+
const :old, T::Hash[String, T.untyped]
|
107
|
+
const :new, T::Hash[String, T.untyped]
|
108
|
+
|
109
|
+
sig { returns(T::Hash[String, [T.untyped, T.untyped]]) }
|
110
|
+
def diff
|
111
|
+
(old.keys | new.keys).reduce({}) do |diff, key|
|
112
|
+
old[key] != new[key] ? diff.merge(key => [old[key], new[key]]) : diff
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class DeleteEvent < T::Struct
|
118
|
+
extend T::Sig
|
119
|
+
include ::Wal::ChangeEvent
|
120
|
+
|
121
|
+
const :transaction_id, Integer
|
122
|
+
const :lsn, Integer
|
123
|
+
const :context, T::Hash[String, T.untyped]
|
124
|
+
const :table, String
|
125
|
+
const :primary_key, T.untyped
|
126
|
+
const :old, T::Hash[String, T.untyped]
|
127
|
+
|
128
|
+
sig { returns(T::Hash[String, [T.untyped, T.untyped]]) }
|
129
|
+
def diff
|
130
|
+
old.transform_values { |val| [val, nil] }
|
131
|
+
end
|
132
|
+
end
|
6
133
|
end
|