mammoth 0.7.2 → 0.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +13 -3
- data/lib/mammoth/sources/postgres.rb +30 -129
- data/lib/mammoth/version.rb +1 -1
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6bca055c346ee0a0b651861f60fca15910708ea772b1cfb661cbc8a46040a1e0
|
|
4
|
+
data.tar.gz: 03d9953cec8a9d82859cbfe446736b4cdc216d46e36244bb5fca4abe455fb0ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3394c678ad2135d34d9d5429599d33d02b8390d7d366821f71356fb0ed50bee71892594901372b0083646f4b8f2332e80977933651102c2e72c3a9309770bee2
|
|
7
|
+
data.tar.gz: 3b67803444f90aa6951f6c554d70b0d7b6e88d7b020a905edbda6357a3f54a7eacbd5b3087a66fd1c002f740596cdcf9515f5ae565e6eb7f00cf66ecba648791
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.0
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Delegated incremental PostgreSQL transaction buffering and
|
|
10
|
+
`CDC::Core::TransactionEnvelope` construction to `pgoutput-source-adapter`.
|
|
11
|
+
- `Mammoth::Sources::Postgres` now streams decoded events and transport WAL
|
|
12
|
+
positions through the adapter's `each_normalized` API instead of maintaining
|
|
13
|
+
private transaction state or envelope lookalikes.
|
|
14
|
+
|
|
15
|
+
### Quality
|
|
16
|
+
|
|
17
|
+
- Added boundary and source integration coverage for streaming adapter
|
|
18
|
+
delegation, exact core envelope preservation, and WAL position forwarding.
|
|
19
|
+
- Updated API signatures, examples, benchmark descriptions, and architecture
|
|
20
|
+
documentation for the corrected source-adapter boundary.
|
|
21
|
+
|
|
5
22
|
## 0.7.2
|
|
6
23
|
|
|
7
24
|
### Changed
|
data/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/kanutocd/mammoth/main/docs/assets/logo/mammoth-horizontal.svg" alt="Mammoth" width="520">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# Mammoth
|
|
2
6
|
|
|
3
7
|
[](https://badge.fury.io/rb/mammoth)
|
|
@@ -5,7 +9,7 @@
|
|
|
5
9
|
[](https://www.ruby-lang.org/en/)
|
|
6
10
|
[](https://opensource.org/licenses/MIT)
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
Mammoth is a self-hosted PostgreSQL event relay focused on reliable delivery
|
|
9
13
|
of database change events.
|
|
10
14
|
|
|
11
15
|
```text
|
|
@@ -20,7 +24,7 @@ Mammoth
|
|
|
20
24
|
Webhook fanout
|
|
21
25
|
```
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
Mammoth is intentionally boring infrastructure. It uses YAML configuration,
|
|
24
28
|
JSON Schema validation, local SQLite operational state, and the CDC Ecosystem's
|
|
25
29
|
shared vocabulary so operators can inspect, recover, and reason about delivery.
|
|
26
30
|
|
|
@@ -37,7 +41,7 @@ https://kanutocd.github.io/mammoth/Mammoth.html
|
|
|
37
41
|
|
|
38
42
|
## OSS MVP
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
Mammoth OSS includes:
|
|
41
45
|
|
|
42
46
|
- CLI foundation
|
|
43
47
|
- YAML configuration loading
|
|
@@ -69,6 +73,12 @@ Mammoth does not own pgoutput protocol parsing, value decoding, source
|
|
|
69
73
|
normalization, ordering policy, or runtime execution. Those belong to the
|
|
70
74
|
upstream CDC Ecosystem components.
|
|
71
75
|
|
|
76
|
+
For the live PostgreSQL stream, `pgoutput-source-adapter` incrementally owns
|
|
77
|
+
`Begin`/`Commit` buffering and emits exact `CDC::Core::ChangeEvent` or
|
|
78
|
+
`CDC::Core::TransactionEnvelope` work items. Mammoth only composes the
|
|
79
|
+
transport, parser, decoder, and source adapter and forwards the resulting core
|
|
80
|
+
work to delivery.
|
|
81
|
+
|
|
72
82
|
## Extensions
|
|
73
83
|
|
|
74
84
|
Mammoth OSS exposes small adapter registries for future extensions:
|
|
@@ -8,7 +8,7 @@ module Mammoth
|
|
|
8
8
|
# Postgres realizes the CDC Ecosystem libraries for Mammoth's product
|
|
9
9
|
# boundary. It composes pgoutput-client, pgoutput-parser,
|
|
10
10
|
# pgoutput-decoder, and pgoutput-source-adapter into a single source that
|
|
11
|
-
# yields CDC::Core
|
|
11
|
+
# yields CDC::Core work to the delivery runtime.
|
|
12
12
|
#
|
|
13
13
|
# This class may mention pgoutput implementation details because it is the
|
|
14
14
|
# concrete PostgreSQL source adapter used by Mammoth. The rest of Mammoth
|
|
@@ -45,22 +45,26 @@ module Mammoth
|
|
|
45
45
|
@checkpoint_store = checkpoint_store
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# Stream CDC::Core
|
|
48
|
+
# Stream CDC::Core work from PostgreSQL logical replication.
|
|
49
49
|
#
|
|
50
50
|
# Calling this method starts the injected or configured pgoutput-client
|
|
51
51
|
# runner. The runner owns the PostgreSQL replication connection and slot
|
|
52
|
-
# lifecycle
|
|
53
|
-
#
|
|
52
|
+
# lifecycle, while pgoutput-source-adapter owns transaction buffering and
|
|
53
|
+
# normalization into CDC::Core work items. This class only composes those
|
|
54
|
+
# layers and forwards transport source positions to the adapter.
|
|
54
55
|
#
|
|
55
|
-
# @yieldparam work [
|
|
56
|
+
# @yieldparam work [CDC::Core::ChangeEvent, CDC::Core::TransactionEnvelope]
|
|
56
57
|
# @return [Enumerator, nil]
|
|
57
58
|
# @raise [Mammoth::ReplicationError] when the source cannot stream CDC work
|
|
58
59
|
def each(&block)
|
|
59
60
|
return enum_for(:each) unless block_given?
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
normalizer = effective_adapter
|
|
63
|
+
unless normalizer.respond_to?(:each_normalized)
|
|
64
|
+
raise ReplicationError, "pgoutput source adapter must respond to #each_normalized"
|
|
63
65
|
end
|
|
66
|
+
|
|
67
|
+
normalizer.each_normalized(decoded_stream, &block)
|
|
64
68
|
nil
|
|
65
69
|
rescue StandardError => e
|
|
66
70
|
raise e if e.is_a?(ReplicationError)
|
|
@@ -70,41 +74,37 @@ module Mammoth
|
|
|
70
74
|
|
|
71
75
|
private
|
|
72
76
|
|
|
77
|
+
def decoded_stream
|
|
78
|
+
Enumerator.new do |stream|
|
|
79
|
+
effective_runner.start do |payload, metadata = nil|
|
|
80
|
+
process_payload(payload, metadata) { |decoded| stream << stream_event(decoded, metadata) }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
73
85
|
def process_payload(payload, metadata, &block)
|
|
74
86
|
parsed = parse_payload(payload)
|
|
75
87
|
decoded = decode_message(parsed, metadata)
|
|
76
|
-
|
|
88
|
+
each_decoded(decoded, &block)
|
|
77
89
|
end
|
|
78
90
|
|
|
79
|
-
|
|
80
|
-
def process_decoded(decoded, metadata, &block)
|
|
91
|
+
def each_decoded(decoded, &block)
|
|
81
92
|
return if decoded.nil?
|
|
82
93
|
|
|
83
94
|
if decoded.is_a?(Array)
|
|
84
|
-
decoded.each { |item|
|
|
95
|
+
decoded.each { |item| each_decoded(item, &block) }
|
|
85
96
|
return
|
|
86
97
|
end
|
|
87
98
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return
|
|
91
|
-
end
|
|
99
|
+
block.call(decoded)
|
|
100
|
+
end
|
|
92
101
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
end
|
|
102
|
+
def stream_event(decoded, metadata)
|
|
103
|
+
normalizer = effective_adapter
|
|
104
|
+
return decoded unless normalizer.respond_to?(:stream_event)
|
|
97
105
|
|
|
98
|
-
|
|
99
|
-
work = enrich_work_position(work, metadata, decoded)
|
|
100
|
-
if transaction_buffer_active?
|
|
101
|
-
Array(@transaction_events) << work
|
|
102
|
-
else
|
|
103
|
-
block.call(work)
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
+
normalizer.stream_event(decoded, source_position: source_position(metadata))
|
|
106
107
|
end
|
|
107
|
-
# rubocop:enable Metrics/MethodLength
|
|
108
108
|
|
|
109
109
|
def parse_payload(payload)
|
|
110
110
|
parser = effective_parser
|
|
@@ -127,102 +127,6 @@ module Mammoth
|
|
|
127
127
|
raise ReplicationError, "pgoutput decoder must respond to #decode or #call"
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
-
def normalize_decoded(decoded)
|
|
131
|
-
return [] if decoded.nil?
|
|
132
|
-
return decoded.flat_map { |item| normalize_decoded(item) } if decoded.is_a?(Array)
|
|
133
|
-
|
|
134
|
-
adapter = effective_adapter
|
|
135
|
-
result = if adapter.respond_to?(:normalize)
|
|
136
|
-
adapter.normalize(decoded)
|
|
137
|
-
elsif adapter.respond_to?(:call)
|
|
138
|
-
adapter.call(decoded)
|
|
139
|
-
else
|
|
140
|
-
raise ReplicationError, "pgoutput source adapter must respond to #normalize or #call"
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
result.is_a?(Array) ? result.compact : [result].compact
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def begin_message?(decoded)
|
|
147
|
-
message_kind(decoded).include?("begin")
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def commit_message?(decoded)
|
|
151
|
-
message_kind(decoded).include?("commit")
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def message_kind(decoded)
|
|
155
|
-
(value_from(decoded, :message_type, :type, :kind) || decoded.class.name.to_s.split("::").last).to_s.downcase
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def start_transaction_buffer(decoded)
|
|
159
|
-
@transaction_events = []
|
|
160
|
-
@transaction_id = value_from(decoded, :transaction_id, :xid, :final_lsn)
|
|
161
|
-
@transaction_metadata = value_hash(decoded, :metadata) || {}
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def emit_transaction_buffer(decoded, metadata, &block)
|
|
165
|
-
return unless transaction_buffer_active?
|
|
166
|
-
|
|
167
|
-
block.call(
|
|
168
|
-
TransactionEnvelope.new(
|
|
169
|
-
@transaction_events,
|
|
170
|
-
transaction_id_for(decoded),
|
|
171
|
-
commit_lsn_for(decoded, metadata),
|
|
172
|
-
value_from(decoded, :commit_time, :committed_at, :timestamp),
|
|
173
|
-
@transaction_metadata
|
|
174
|
-
)
|
|
175
|
-
)
|
|
176
|
-
ensure
|
|
177
|
-
clear_transaction_buffer
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def transaction_id_for(decoded)
|
|
181
|
-
value_from(decoded, :transaction_id, :xid) || @transaction_id || first_event_value(:transaction_id, :xid)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def commit_lsn_for(decoded, metadata)
|
|
185
|
-
value_from(decoded, :commit_lsn, :source_position, :lsn, :end_lsn, :final_lsn) ||
|
|
186
|
-
value_from(metadata, :commit_lsn, :source_position, :lsn, :end_lsn, :final_lsn) ||
|
|
187
|
-
first_event_value(:commit_lsn, :source_position, :lsn)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def transaction_buffer_active?
|
|
191
|
-
!@transaction_events.nil?
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def clear_transaction_buffer
|
|
195
|
-
@transaction_events = nil
|
|
196
|
-
@transaction_id = nil
|
|
197
|
-
@transaction_metadata = nil
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
201
|
-
def enrich_work_position(work, metadata, decoded)
|
|
202
|
-
position = value_from(work, :source_position, :commit_lsn) ||
|
|
203
|
-
value_from(metadata, :source_position, :commit_lsn, :lsn) ||
|
|
204
|
-
value_from(decoded, :source_position, :commit_lsn, :lsn)
|
|
205
|
-
return work unless position
|
|
206
|
-
|
|
207
|
-
work_hash = work.respond_to?(:to_h) ? work.to_h : work
|
|
208
|
-
return work unless work_hash.is_a?(Hash)
|
|
209
|
-
|
|
210
|
-
key_style = work_hash.key?("operation") ? :string : :symbol
|
|
211
|
-
source_position_key = key_style == :string ? "source_position" : :source_position
|
|
212
|
-
commit_lsn_key = key_style == :string ? "commit_lsn" : :commit_lsn
|
|
213
|
-
return work if work_hash[source_position_key] || work_hash[commit_lsn_key]
|
|
214
|
-
|
|
215
|
-
work_hash.merge(source_position_key => position, commit_lsn_key => position)
|
|
216
|
-
end
|
|
217
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
218
|
-
|
|
219
|
-
def first_event_value(*keys)
|
|
220
|
-
event = Array(@transaction_events).find do |event|
|
|
221
|
-
keys.any? { |key| value_from(event, key) }
|
|
222
|
-
end
|
|
223
|
-
value_from(event, *keys) if event
|
|
224
|
-
end
|
|
225
|
-
|
|
226
130
|
def value_from(object, *keys)
|
|
227
131
|
return nil if object.nil?
|
|
228
132
|
|
|
@@ -239,9 +143,8 @@ module Mammoth
|
|
|
239
143
|
nil
|
|
240
144
|
end
|
|
241
145
|
|
|
242
|
-
def
|
|
243
|
-
|
|
244
|
-
value.is_a?(Hash) ? value : nil
|
|
146
|
+
def source_position(metadata)
|
|
147
|
+
value_from(metadata, :source_position, :commit_lsn, :lsn, :wal_end_lsn, :end_lsn, :final_lsn)
|
|
245
148
|
end
|
|
246
149
|
|
|
247
150
|
def effective_runner
|
|
@@ -375,8 +278,6 @@ module Mammoth
|
|
|
375
278
|
rescue LoadError => e
|
|
376
279
|
raise ReplicationError, "#{gem_name} is required for PostgreSQL CDC source integration: #{e.message}"
|
|
377
280
|
end
|
|
378
|
-
TransactionEnvelope = Data.define(:events, :transaction_id, :commit_lsn, :commit_time, :metadata)
|
|
379
|
-
private_constant :TransactionEnvelope
|
|
380
281
|
end
|
|
381
282
|
# rubocop:enable Metrics/ClassLength
|
|
382
283
|
end
|
data/lib/mammoth/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mammoth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
@@ -99,14 +99,20 @@ dependencies:
|
|
|
99
99
|
requirements:
|
|
100
100
|
- - "~>"
|
|
101
101
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: '0.
|
|
102
|
+
version: '0.2'
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: 0.2.0
|
|
103
106
|
type: :runtime
|
|
104
107
|
prerelease: false
|
|
105
108
|
version_requirements: !ruby/object:Gem::Requirement
|
|
106
109
|
requirements:
|
|
107
110
|
- - "~>"
|
|
108
111
|
- !ruby/object:Gem::Version
|
|
109
|
-
version: '0.
|
|
112
|
+
version: '0.2'
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: 0.2.0
|
|
110
116
|
- !ruby/object:Gem::Dependency
|
|
111
117
|
name: sqlite3
|
|
112
118
|
requirement: !ruby/object:Gem::Requirement
|