mammoth 1.0.0 → 1.2.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 +33 -0
- data/README.md +24 -3
- data/lib/mammoth/event_serializer.rb +39 -7
- data/lib/mammoth/transaction_envelope_serializer.rb +12 -2
- data/lib/mammoth/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9169e3ff1f9fad3a07df87cee57891aea0ca6815f356f7ef998e94572a432ea4
|
|
4
|
+
data.tar.gz: 6ee2140075cb22b1fc0aea3977a33adfd1aa49918d184a823ee3cbcab9b87bac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37191be4257dbab756e5e2ba81bae60905df5a921792001daf12b5ece1bcacbf60c353967b6da5c0de4e71311a8b95311ed37e7f8f2587bcb49e7dfbae6a57bd
|
|
7
|
+
data.tar.gz: 603479a5b0e7e067743eb6dcb8802ea2077954df50e23a3b29b97bdefb161cb1c47f493dfac919aa27d6a0552f018fcc1a7116e876acca50f71bc716f0d8c539
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.2.0 - 2026-07-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added an opt-in monitoring profile to `webhooks-quickstart` with provisioned
|
|
10
|
+
Prometheus and Grafana services, meaningful seeded traffic, a Mammoth
|
|
11
|
+
operations dashboard, curated Prometheus queries, recording rules, and
|
|
12
|
+
example Prometheus and Grafana alerting rules.
|
|
13
|
+
|
|
14
|
+
## 1.1.0 - 2026-07-19
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Added a serializer benchmark comparing explicit and deterministic fallback
|
|
19
|
+
event IDs for event and transaction payload projection.
|
|
20
|
+
- Added `webhooks-quickstart`, a one-command PostgreSQL-to-Mammoth demo with an
|
|
21
|
+
order application; inspectable signed webhook deliveries and retries;
|
|
22
|
+
`INSERT`, `UPDATE`, and `DELETE` events; multi-event payment and reversal
|
|
23
|
+
transactions; adaptation guidance; WAL-retention safeguards; and end-to-end
|
|
24
|
+
verification.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Generate deterministic fallback event and transaction-envelope IDs so retries
|
|
29
|
+
and post-crash redelivery preserve destination idempotency when upstream
|
|
30
|
+
metadata does not provide an event ID.
|
|
31
|
+
- Serialize `CDC::Core::ChangeEvent` column differences when complete old and
|
|
32
|
+
new rows are available, while avoiding false differences from key-only old
|
|
33
|
+
rows.
|
|
34
|
+
- Require pgoutput-source-adapter 0.3.1 and use its stable transaction-local
|
|
35
|
+
event sequence to prevent deterministic ID collisions between otherwise
|
|
36
|
+
identical changes while preserving replay stability.
|
|
37
|
+
|
|
5
38
|
## 1.0.0 - 2026-07-17
|
|
6
39
|
|
|
7
40
|
### Added
|
data/README.md
CHANGED
|
@@ -38,10 +38,28 @@ API documentation:
|
|
|
38
38
|
|
|
39
39
|
https://kanutocd.github.io/mammoth/Mammoth.html
|
|
40
40
|
|
|
41
|
+
## Start Here
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
The recommended first-run experience is the
|
|
44
|
+
[`webhooks-quickstart`](webhooks-quickstart) stack. One Compose command starts a
|
|
45
|
+
demo application, logical-replication-enabled PostgreSQL, Mammoth, a signed
|
|
46
|
+
webhook receiver with visible retries, and Mammoth health endpoints:
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
```bash
|
|
49
|
+
cd webhooks-quickstart
|
|
50
|
+
docker compose up --build --wait
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Once the flow is visible, follow its
|
|
54
|
+
[`ADAPTING.md`](webhooks-quickstart/ADAPTING.md) guide to connect Mammoth to an
|
|
55
|
+
existing PostgreSQL application. Its optional `monitoring` profile adds seeded
|
|
56
|
+
traffic, a provisioned Grafana dashboard and alert rules, and a curated
|
|
57
|
+
Prometheus query library; see the
|
|
58
|
+
[observability guide](docs/OBSERVABILITY.md#runnable-monitoring-showcase).
|
|
59
|
+
|
|
60
|
+
## v1 Release Scope
|
|
61
|
+
|
|
62
|
+
Mammoth 1.x includes:
|
|
45
63
|
|
|
46
64
|
- operator CLI for validation, bootstrap, status, delivery, observability, and
|
|
47
65
|
dead-letter workflows
|
|
@@ -107,7 +125,9 @@ Human-readable CLI formatting and PostgreSQL-derived row columns are not frozen:
|
|
|
107
125
|
scripts should rely on documented exit behavior, while receivers must tolerate
|
|
108
126
|
additive fields and coordinate source schema changes. See
|
|
109
127
|
[`docs/COMPATIBILITY.md`](docs/COMPATIBILITY.md) for the complete promise and
|
|
110
|
-
major-version boundaries.
|
|
128
|
+
major-version boundaries. See
|
|
129
|
+
[`docs/WEBHOOK-PAYLOADS.md`](docs/WEBHOOK-PAYLOADS.md) for the canonical event
|
|
130
|
+
and transaction JSON contracts, column-change semantics, and event-ID behavior.
|
|
111
131
|
|
|
112
132
|
## Boundary
|
|
113
133
|
|
|
@@ -222,6 +242,7 @@ rather than opening SQLite directly.
|
|
|
222
242
|
Mammoth includes local benchmarks for the product surfaces operators tune in
|
|
223
243
|
production:
|
|
224
244
|
|
|
245
|
+
- event and transaction serialization, including deterministic fallback IDs
|
|
225
246
|
- concurrent delivery runtime
|
|
226
247
|
- real webhook delivery
|
|
227
248
|
- multi-destination webhook fanout
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
require "
|
|
4
|
+
require "digest"
|
|
5
5
|
require "time"
|
|
6
6
|
|
|
7
7
|
module Mammoth
|
|
@@ -27,17 +27,17 @@ module Mammoth
|
|
|
27
27
|
def initialize(event)
|
|
28
28
|
raise ArgumentError, "event must be a CDC::Core::ChangeEvent" unless event.is_a?(CDC::Core::ChangeEvent)
|
|
29
29
|
|
|
30
|
-
@event = event
|
|
30
|
+
@event = event
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Return the webhook payload.
|
|
34
34
|
#
|
|
35
35
|
# @return [Hash] webhook payload
|
|
36
36
|
def call
|
|
37
|
-
event_hash = stringify_keys(@event)
|
|
37
|
+
event_hash = stringify_keys(@event.to_h)
|
|
38
38
|
metadata = stringify_keys(event_hash["metadata"] || {})
|
|
39
39
|
{
|
|
40
|
-
"event_id" => event_id(metadata),
|
|
40
|
+
"event_id" => event_id(event_hash, metadata),
|
|
41
41
|
"source" => source(metadata),
|
|
42
42
|
"operation" => normalize_operation(event_hash.fetch("operation")),
|
|
43
43
|
"namespace" => event_hash["schema"],
|
|
@@ -47,7 +47,7 @@ module Mammoth
|
|
|
47
47
|
"transaction_id" => event_hash["transaction_id"],
|
|
48
48
|
"occurred_at" => occurred_at(event_hash),
|
|
49
49
|
"data" => event_data(event_hash),
|
|
50
|
-
"changes" => metadata
|
|
50
|
+
"changes" => serialized_changes(event_hash, metadata),
|
|
51
51
|
"metadata" => metadata
|
|
52
52
|
}
|
|
53
53
|
end
|
|
@@ -69,8 +69,25 @@ module Mammoth
|
|
|
69
69
|
operation.to_s
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
def event_id(metadata)
|
|
73
|
-
metadata["event_id"] ||
|
|
72
|
+
def event_id(event_hash, metadata)
|
|
73
|
+
metadata["event_id"] || deterministic_event_id(event_hash, metadata)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def deterministic_event_id(event_hash, metadata)
|
|
77
|
+
identity = {
|
|
78
|
+
source: source(metadata),
|
|
79
|
+
transaction_id: event_hash["transaction_id"],
|
|
80
|
+
source_position: event_hash["commit_lsn"],
|
|
81
|
+
sequence_number: event_hash["sequence_number"],
|
|
82
|
+
occurred_at: event_hash["occurred_at"],
|
|
83
|
+
namespace: event_hash["schema"],
|
|
84
|
+
entity: event_hash["table"],
|
|
85
|
+
operation: normalize_operation(event_hash.fetch("operation")),
|
|
86
|
+
primary_key: event_hash["primary_key"],
|
|
87
|
+
old_values: event_hash["old_values"],
|
|
88
|
+
new_values: event_hash["new_values"]
|
|
89
|
+
}
|
|
90
|
+
"evt_#{Digest::SHA256.hexdigest(JSON.generate(identity))}"
|
|
74
91
|
end
|
|
75
92
|
|
|
76
93
|
def source(metadata)
|
|
@@ -87,5 +104,20 @@ module Mammoth
|
|
|
87
104
|
def event_data(event_hash)
|
|
88
105
|
event_hash["data"] || event_hash["new_values"] || event_hash["old_values"] || {}
|
|
89
106
|
end
|
|
107
|
+
|
|
108
|
+
def serialized_changes(event_hash, metadata)
|
|
109
|
+
return metadata["changes"] || [] if metadata.key?("changes")
|
|
110
|
+
return [] unless changes_available?(event_hash)
|
|
111
|
+
|
|
112
|
+
@event.changes.map(&:to_h)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def changes_available?(event_hash)
|
|
116
|
+
return true unless normalize_operation(event_hash.fetch("operation")) == "update"
|
|
117
|
+
|
|
118
|
+
old_values = event_hash["old_values"]
|
|
119
|
+
new_values = event_hash["new_values"]
|
|
120
|
+
old_values.is_a?(Hash) && new_values.is_a?(Hash) && old_values.keys.sort == new_values.keys.sort
|
|
121
|
+
end
|
|
90
122
|
end
|
|
91
123
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
require "
|
|
4
|
+
require "digest"
|
|
5
5
|
require "time"
|
|
6
6
|
|
|
7
7
|
module Mammoth
|
|
@@ -38,7 +38,7 @@ module Mammoth
|
|
|
38
38
|
def call
|
|
39
39
|
event_payloads = envelope.events.map { |event| EventSerializer.call(event) }
|
|
40
40
|
{
|
|
41
|
-
"event_id" => envelope_metadata["event_id"] ||
|
|
41
|
+
"event_id" => envelope_metadata["event_id"] || deterministic_event_id(event_payloads),
|
|
42
42
|
"type" => PAYLOAD_TYPE,
|
|
43
43
|
"source" => first_event_value(event_payloads, "source") || EventSerializer::DEFAULT_SOURCE,
|
|
44
44
|
"transaction_id" => envelope.transaction_id,
|
|
@@ -70,6 +70,16 @@ module Mammoth
|
|
|
70
70
|
@envelope_metadata ||= stringify_keys(envelope.metadata)
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
def deterministic_event_id(event_payloads)
|
|
74
|
+
identity = {
|
|
75
|
+
source: first_event_value(event_payloads, "source") || EventSerializer::DEFAULT_SOURCE,
|
|
76
|
+
transaction_id: envelope.transaction_id,
|
|
77
|
+
source_position: source_position(event_payloads),
|
|
78
|
+
event_ids: event_payloads.map { |payload| payload.fetch("event_id") }
|
|
79
|
+
}
|
|
80
|
+
"txn_#{Digest::SHA256.hexdigest(JSON.generate(identity))}"
|
|
81
|
+
end
|
|
82
|
+
|
|
73
83
|
def source_position(event_payloads)
|
|
74
84
|
envelope.commit_lsn ||
|
|
75
85
|
first_event_value(event_payloads.reverse, "source_position")
|
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: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
@@ -122,7 +122,7 @@ dependencies:
|
|
|
122
122
|
version: '0.3'
|
|
123
123
|
- - ">="
|
|
124
124
|
- !ruby/object:Gem::Version
|
|
125
|
-
version: 0.3.
|
|
125
|
+
version: 0.3.1
|
|
126
126
|
type: :runtime
|
|
127
127
|
prerelease: false
|
|
128
128
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -132,7 +132,7 @@ dependencies:
|
|
|
132
132
|
version: '0.3'
|
|
133
133
|
- - ">="
|
|
134
134
|
- !ruby/object:Gem::Version
|
|
135
|
-
version: 0.3.
|
|
135
|
+
version: 0.3.1
|
|
136
136
|
- !ruby/object:Gem::Dependency
|
|
137
137
|
name: sqlite3
|
|
138
138
|
requirement: !ruby/object:Gem::Requirement
|