mammoth 0.9.0 → 1.1.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 +89 -1
- data/README.md +87 -7
- data/config/mammoth.example.yml +19 -9
- data/config/mammoth.schema.json +2 -2
- data/lib/mammoth/application.rb +34 -3
- data/lib/mammoth/delivery_processor.rb +5 -2
- data/lib/mammoth/delivery_progress_coordinator.rb +153 -0
- data/lib/mammoth/delivery_worker.rb +9 -25
- data/lib/mammoth/event_serializer.rb +39 -7
- data/lib/mammoth/observability_metrics.rb +3 -1
- data/lib/mammoth/observability_server.rb +5 -3
- data/lib/mammoth/observability_snapshot.rb +63 -10
- data/lib/mammoth/postgres_observability_metrics.rb +80 -0
- data/lib/mammoth/replication_consumer.rb +28 -7
- data/lib/mammoth/sources/postgres.rb +252 -4
- data/lib/mammoth/sources/postgres_publication_inspector.rb +214 -0
- data/lib/mammoth/sources/postgres_slot_health.rb +90 -0
- data/lib/mammoth/transaction_envelope_serializer.rb +12 -2
- data/lib/mammoth/version.rb +1 -1
- data/lib/mammoth.rb +5 -1
- metadata +33 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 456cf70ebb1cb5a91e7e73a42f847298890e8d56f827ca1175ebe61b1d3bc09a
|
|
4
|
+
data.tar.gz: e726ba8a465f924b45f2f4055a0a6bf076b7b376681ff7db0a72355ddad241e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 848144fb545ae6040119e249820a1fd495887b8ff2f7bb3f62d47e2fc91de4869a8ec30eaa56cb4946dd50c2063fc7071a8307ae303b25ddbc40d396c7dd45c8
|
|
7
|
+
data.tar.gz: fabd87ecb39c6158ca730dc9d8926a4157b6e01d8ab48a90b6f305299309d3680a86834351a59fba40e0019cfb1d81de66d34aa9ccabacd45f47a8c3b10577dd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,94 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 1.1.0 - 2026-07-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added a serializer benchmark comparing explicit and deterministic fallback
|
|
8
|
+
event IDs for event and transaction payload projection.
|
|
9
|
+
- Added `webhooks-quickstart`, a one-command PostgreSQL-to-Mammoth demo with an
|
|
10
|
+
order application; inspectable signed webhook deliveries and retries;
|
|
11
|
+
`INSERT`, `UPDATE`, and `DELETE` events; multi-event payment and reversal
|
|
12
|
+
transactions; adaptation guidance; WAL-retention safeguards; and end-to-end
|
|
13
|
+
verification.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Generate deterministic fallback event and transaction-envelope IDs so retries
|
|
18
|
+
and post-crash redelivery preserve destination idempotency when upstream
|
|
19
|
+
metadata does not provide an event ID.
|
|
20
|
+
- Serialize `CDC::Core::ChangeEvent` column differences when complete old and
|
|
21
|
+
new rows are available, while avoiding false differences from key-only old
|
|
22
|
+
rows.
|
|
23
|
+
- Require pgoutput-source-adapter 0.3.1 and use its stable transaction-local
|
|
24
|
+
event sequence to prevent deterministic ID collisions between otherwise
|
|
25
|
+
identical changes while preserving replay stability.
|
|
26
|
+
|
|
27
|
+
## 1.0.0 - 2026-07-17
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Added PostgreSQL slot readiness diagnostics and Prometheus gauges for
|
|
32
|
+
inspection availability, presence, activity, retained WAL bytes, safe WAL
|
|
33
|
+
size, WAL status, invalidation, inactivity time, and restart/flush LSNs.
|
|
34
|
+
- `/readyz` now fails closed when the configured slot is missing, inactive,
|
|
35
|
+
invalidated, conflicting, WAL-lost, or cannot be inspected.
|
|
36
|
+
- Added startup validation for every configured publication table that publishes
|
|
37
|
+
`UPDATE` or `DELETE`, accepting a usable primary key, selected replica-identity
|
|
38
|
+
index, or `REPLICA IDENTITY FULL` and reporting all invalid tables together.
|
|
39
|
+
- Added catalog-derived, ordered replica-identity mappings for relation OIDs and
|
|
40
|
+
schema-qualified tables.
|
|
41
|
+
- Added a self-verifying live PostgreSQL example for composite, non-`id`
|
|
42
|
+
replica identities across `INSERT`, `UPDATE`, and `DELETE`.
|
|
43
|
+
- Added a live PostgreSQL example that shows fail-closed restart after slot
|
|
44
|
+
invalidation and the explicit operator reconciliation required to resume.
|
|
45
|
+
- Added a PostgreSQL observability example correlating Mammoth readiness and
|
|
46
|
+
Prometheus slot gauges with native replication and publication catalogs.
|
|
47
|
+
- Added a live additive schema-evolution example demonstrating consumer-first
|
|
48
|
+
compatibility, relation-metadata refresh, and the non-delivery of DDL.
|
|
49
|
+
- Added a destination idempotency example proving atomic receiver-side
|
|
50
|
+
duplicate suppression across isolated Mammoth delivery ledgers.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- Added PostgreSQL slot preflight before streaming and fail-closed handling for
|
|
55
|
+
missing, active, lost, invalidated, incompatible, and
|
|
56
|
+
checkpoint-unreachable slots.
|
|
57
|
+
- Disabled automatic slot recreation whenever a configured or persisted resume
|
|
58
|
+
LSN requires WAL continuity.
|
|
59
|
+
- Rejected temporary-slot recovery from durable checkpoints.
|
|
60
|
+
- Preserved pgoutput-client's acknowledgement-compatible transport LSN
|
|
61
|
+
separately from normalized CDC-core `commit_lsn` payload context.
|
|
62
|
+
- Checkpoints and PostgreSQL feedback now use the source-owned transport
|
|
63
|
+
watermark, preventing decimal transaction commit values from reaching
|
|
64
|
+
`runner.ack`.
|
|
65
|
+
- Event-mode delivery resolves an envelope's transport position for each exact
|
|
66
|
+
child event without mutating or rebuilding CDC-core work.
|
|
67
|
+
- Wired publication identity mappings into
|
|
68
|
+
`Pgoutput::SourceAdapter::ReplicaIdentityResolver` so composite and non-`id`
|
|
69
|
+
keys are normalized without partial identity data.
|
|
70
|
+
|
|
71
|
+
### Quality
|
|
72
|
+
|
|
73
|
+
- Upgraded pgoutput-client to 0.4.0 and covered slot identity, health,
|
|
74
|
+
retained-WAL reporting, continuity boundaries, and safe first-time creation.
|
|
75
|
+
- Upgraded pgoutput-source-adapter to 0.3.0 and covered catalog-to-resolver
|
|
76
|
+
composition for composite replica identities.
|
|
77
|
+
- Upgraded pgoutput-decoder to 0.2.0 so key-only old tuples preserve their
|
|
78
|
+
exact decoded columns instead of synthesizing absent non-key values.
|
|
79
|
+
- Added end-to-end source/coordinator coverage proving that a transaction with
|
|
80
|
+
decimal `commit_lsn` checkpoints and acknowledges its formatted transport LSN.
|
|
81
|
+
- Added opt-in PostgreSQL 17 integration coverage for transactional `INSERT`,
|
|
82
|
+
`UPDATE`, and `DELETE` with composite identity, replication reconnects,
|
|
83
|
+
concurrent out-of-order completion, contiguous slot acknowledgement, and
|
|
84
|
+
fail-closed slot invalidation; CI and release gates run the suite against a
|
|
85
|
+
logical-replication service.
|
|
86
|
+
- Strengthened boundary coverage for source-owned position resolution and
|
|
87
|
+
publication-catalog inspection, and synchronized PostgreSQL, runtime,
|
|
88
|
+
operational-state, configuration, and transaction-example documentation.
|
|
89
|
+
- Documented PostgreSQL WAL guardrails, slot/catalog monitoring, DDL and
|
|
90
|
+
sequence limitations, destination conflict ownership, upgrade recovery,
|
|
91
|
+
troubleshooting, example caveats, and Kubernetes deployment policy.
|
|
4
92
|
|
|
5
93
|
## 0.9.0
|
|
6
94
|
|
data/README.md
CHANGED
|
@@ -38,12 +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.
|
|
45
56
|
|
|
46
|
-
|
|
57
|
+
## v1 Release Scope
|
|
58
|
+
|
|
59
|
+
Mammoth 1.x includes:
|
|
60
|
+
|
|
61
|
+
- operator CLI for validation, bootstrap, status, delivery, observability, and
|
|
62
|
+
dead-letter workflows
|
|
47
63
|
- YAML configuration loading
|
|
48
64
|
- JSON Schema-backed configuration validation
|
|
49
65
|
- SQLite operational memory bootstrap
|
|
@@ -54,19 +70,62 @@ Mammoth OSS includes:
|
|
|
54
70
|
- webhook fanout to multiple destinations
|
|
55
71
|
- fanout route filters by schema, table, and operation
|
|
56
72
|
- per-destination enable/disable and retry policy controls
|
|
57
|
-
- delivery worker with retry, delivered-ledger,
|
|
73
|
+
- delivery worker with retry, delivered-ledger, and DLQ handling
|
|
74
|
+
- contiguous delivery watermark for checkpoint and PostgreSQL acknowledgement
|
|
75
|
+
- source-owned transport LSN preservation independent of payload `commit_lsn`
|
|
76
|
+
- fail-closed PostgreSQL slot and checkpoint continuity preflight
|
|
77
|
+
- fail-closed publication replica-identity preflight for `UPDATE` and `DELETE`
|
|
58
78
|
- dead-letter inspection and filtered replay commands
|
|
59
79
|
- CDC-core event serialization boundary
|
|
60
80
|
- CDC Ecosystem source-adapter integration boundary
|
|
61
81
|
- Docker image support
|
|
62
82
|
- public Helm chart support
|
|
63
83
|
- unit and e2e test tasks
|
|
64
|
-
- health and metrics endpoints
|
|
84
|
+
- health, PostgreSQL slot readiness, and retained-WAL metrics endpoints
|
|
65
85
|
- canonical CDC dispatch counters through a `CDC::Core::Observer`
|
|
66
86
|
- explicit extension registries for state, destination, and runtime adapters
|
|
67
87
|
- node identity and local capability reporting
|
|
68
88
|
- lifecycle hooks, configuration providers, and reusable local command objects
|
|
69
89
|
|
|
90
|
+
## Feature Examples
|
|
91
|
+
|
|
92
|
+
The runnable examples are organized around production behaviors and failure
|
|
93
|
+
modes, not isolated API snippets.
|
|
94
|
+
|
|
95
|
+
| Example | v1 capability demonstrated |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| [`live_postgres_webhook`](examples/live_postgres_webhook) | End-to-end PostgreSQL logical replication into webhook delivery. |
|
|
98
|
+
| [`transaction_webhook`](examples/transaction_webhook) | TransactionEnvelope preservation through the concurrent runtime. |
|
|
99
|
+
| [`webhook_fanout`](examples/webhook_fanout) | Routed multi-destination fanout, environment-backed headers, signing, and independent retry policies. |
|
|
100
|
+
| [`ordering`](examples/ordering) | Ordered and throughput-oriented transaction scheduling. |
|
|
101
|
+
| [`checkpoint_recovery`](examples/checkpoint_recovery) | Durable restart recovery, replay suppression, checkpointing, and acknowledgement. |
|
|
102
|
+
| [`slot_invalidation_recovery`](examples/slot_invalidation_recovery) | Fail-closed slot invalidation and explicit operator reconciliation. |
|
|
103
|
+
| [`composite_replica_identity`](examples/composite_replica_identity) | Composite, non-`id` replica identity across `INSERT`, `UPDATE`, and `DELETE`. |
|
|
104
|
+
| [`postgres_observability`](examples/postgres_observability) | Slot readiness and Prometheus metrics correlated with PostgreSQL catalogs. |
|
|
105
|
+
| [`schema_evolution`](examples/schema_evolution) | Consumer-first additive schema evolution without implying DDL delivery. |
|
|
106
|
+
| [`destination_idempotency`](examples/destination_idempotency) | Atomic destination-side duplicate suppression across isolated relay ledgers. |
|
|
107
|
+
| [`failing_webhook_retry`](examples/failing_webhook_retry) | Retry exhaustion and durable dead-letter persistence. |
|
|
108
|
+
| [`operational_state`](examples/operational_state) | Inspectable checkpoints, delivered ledgers, and dead letters. |
|
|
109
|
+
| [`kubernetes_helm`](examples/kubernetes_helm) | Single-consumer Kubernetes deployment using the public Helm chart. |
|
|
110
|
+
|
|
111
|
+
See [`examples/README.md`](examples/README.md) for the complete index, boundary
|
|
112
|
+
notes, and commands.
|
|
113
|
+
|
|
114
|
+
## v1 Compatibility
|
|
115
|
+
|
|
116
|
+
Mammoth 1.x treats its validated configuration, serialized webhook envelopes,
|
|
117
|
+
documented CLI command behavior, and forward operational-state migrations as
|
|
118
|
+
supported contracts. Compatible minor releases may add optional configuration
|
|
119
|
+
or payload fields, but do not remove or reinterpret existing fields.
|
|
120
|
+
|
|
121
|
+
Human-readable CLI formatting and PostgreSQL-derived row columns are not frozen:
|
|
122
|
+
scripts should rely on documented exit behavior, while receivers must tolerate
|
|
123
|
+
additive fields and coordinate source schema changes. See
|
|
124
|
+
[`docs/COMPATIBILITY.md`](docs/COMPATIBILITY.md) for the complete promise and
|
|
125
|
+
major-version boundaries. See
|
|
126
|
+
[`docs/WEBHOOK-PAYLOADS.md`](docs/WEBHOOK-PAYLOADS.md) for the canonical event
|
|
127
|
+
and transaction JSON contracts, column-change semantics, and event-ID behavior.
|
|
128
|
+
|
|
70
129
|
## Boundary
|
|
71
130
|
|
|
72
131
|
Mammoth begins at CDC-core work items and ends at webhook fanout delivery.
|
|
@@ -82,7 +141,9 @@ For the live PostgreSQL stream, `pgoutput-source-adapter` incrementally owns
|
|
|
82
141
|
`Begin`/`Commit` buffering and emits exact `CDC::Core::ChangeEvent` or
|
|
83
142
|
`CDC::Core::TransactionEnvelope` work items. Mammoth only composes the
|
|
84
143
|
transport, parser, decoder, and source adapter and forwards the resulting core
|
|
85
|
-
work to delivery.
|
|
144
|
+
work to delivery. Mammoth's publication preflight supplies ordered,
|
|
145
|
+
catalog-derived replica-identity columns to the adapter, which owns composite
|
|
146
|
+
and non-`id` key extraction.
|
|
86
147
|
|
|
87
148
|
At the downstream boundary, `Mammoth::DeliveryProcessor` implements
|
|
88
149
|
`CDC::Core::Processor` and returns `CDC::Core::ProcessorResult`. Inline and
|
|
@@ -178,6 +239,7 @@ rather than opening SQLite directly.
|
|
|
178
239
|
Mammoth includes local benchmarks for the product surfaces operators tune in
|
|
179
240
|
production:
|
|
180
241
|
|
|
242
|
+
- event and transaction serialization, including deterministic fallback IDs
|
|
181
243
|
- concurrent delivery runtime
|
|
182
244
|
- real webhook delivery
|
|
183
245
|
- multi-destination webhook fanout
|
|
@@ -206,7 +268,17 @@ script/test-e2e
|
|
|
206
268
|
```
|
|
207
269
|
|
|
208
270
|
The e2e task uses a real HTTP receiver, real SQLite database, and real
|
|
209
|
-
filesystem paths.
|
|
271
|
+
filesystem paths. Set `MAMMOTH_E2E_POSTGRES_URL` to include the real PostgreSQL
|
|
272
|
+
logical-replication scenarios:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
MAMMOTH_E2E_POSTGRES_URL=postgres://postgres:postgres@127.0.0.1:5432/mammoth_e2e \
|
|
276
|
+
bundle exec rake test:e2e
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The PostgreSQL fixture must have `wal_level=logical`; its test role must be able
|
|
280
|
+
to create publications and logical replication slots, terminate replication
|
|
281
|
+
backends, and change `max_slot_wal_keep_size`.
|
|
210
282
|
|
|
211
283
|
## Kubernetes
|
|
212
284
|
|
|
@@ -225,6 +297,14 @@ helm install mammoth charts/mammoth
|
|
|
225
297
|
The chart uses one replica and `Recreate` strategy to respect PostgreSQL's
|
|
226
298
|
logical replication slot constraint: one slot, one active subscriber.
|
|
227
299
|
|
|
300
|
+
Production operators should also monitor retained WAL and slot readiness,
|
|
301
|
+
configure PostgreSQL retention guardrails, and alert on database disk and
|
|
302
|
+
catalog health. DDL and sequence state are not replicated; coordinate schema
|
|
303
|
+
changes with webhook consumers and synchronize sequences externally when
|
|
304
|
+
building a writable database copy. See
|
|
305
|
+
[`docs/POSTGRESQL.md`](docs/POSTGRESQL.md) and
|
|
306
|
+
[`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md).
|
|
307
|
+
|
|
228
308
|
## License
|
|
229
309
|
|
|
230
310
|
Mammoth OSS is licensed under the [MIT License](LICENSE.txt).
|
data/config/mammoth.example.yml
CHANGED
|
@@ -89,6 +89,8 @@ replication:
|
|
|
89
89
|
#
|
|
90
90
|
# Publications define which table changes PostgreSQL sends to Mammoth.
|
|
91
91
|
# Create them explicitly in production so the database boundary is clear.
|
|
92
|
+
# Tables that publish UPDATE or DELETE must have a primary key, an eligible
|
|
93
|
+
# REPLICA IDENTITY USING INDEX, or REPLICA IDENTITY FULL.
|
|
92
94
|
#
|
|
93
95
|
# Example SQL:
|
|
94
96
|
# CREATE PUBLICATION mammoth_publication FOR TABLE users, orders;
|
|
@@ -98,7 +100,9 @@ replication:
|
|
|
98
100
|
# Optional start LSN for advanced recovery or controlled replay.
|
|
99
101
|
#
|
|
100
102
|
# Leave unset for normal operation. Mammoth will use its checkpoint store and
|
|
101
|
-
# the replication slot position.
|
|
103
|
+
# the replication slot position. Persisted PostgreSQL checkpoints use the
|
|
104
|
+
# acknowledgement-compatible transport LSN, not a normalized transaction's
|
|
105
|
+
# decimal commit_lsn.
|
|
102
106
|
#
|
|
103
107
|
# Example:
|
|
104
108
|
# start_lsn: "0/16B6C50"
|
|
@@ -108,10 +112,15 @@ replication:
|
|
|
108
112
|
# Automatically create the replication slot if it does not exist.
|
|
109
113
|
#
|
|
110
114
|
# true:
|
|
111
|
-
# Convenient for development, demos, and first-time bootstrap.
|
|
115
|
+
# Convenient for development, demos, and first-time bootstrap. Mammoth
|
|
116
|
+
# creates a missing slot only when no configured or persisted resume LSN
|
|
117
|
+
# requires continuity.
|
|
112
118
|
#
|
|
113
119
|
# false:
|
|
114
120
|
# Recommended for production when slots are DBA-managed infrastructure.
|
|
121
|
+
#
|
|
122
|
+
# A missing slot is never recreated while a checkpoint exists. Mammoth fails
|
|
123
|
+
# closed because a replacement slot cannot recover the lost WAL interval.
|
|
115
124
|
auto_create_slot: false
|
|
116
125
|
|
|
117
126
|
# Use a temporary replication slot.
|
|
@@ -127,8 +136,9 @@ replication:
|
|
|
127
136
|
|
|
128
137
|
# Standby feedback interval in seconds.
|
|
129
138
|
#
|
|
130
|
-
# Mammoth sends periodic feedback while streaming
|
|
131
|
-
#
|
|
139
|
+
# Mammoth sends periodic feedback while streaming. The feedback position
|
|
140
|
+
# advances only after every earlier delivery has a durable outcome and the
|
|
141
|
+
# same contiguous position has been written to Mammoth's checkpoint store.
|
|
132
142
|
#
|
|
133
143
|
# Lower values:
|
|
134
144
|
# faster liveness/feedback visibility, slightly more network chatter
|
|
@@ -145,13 +155,13 @@ delivery:
|
|
|
145
155
|
# Delivery granularity.
|
|
146
156
|
#
|
|
147
157
|
# event:
|
|
148
|
-
# Deliver each change event independently. This has lower latency but
|
|
149
|
-
# transaction
|
|
158
|
+
# Deliver each change event independently. This has lower latency but does
|
|
159
|
+
# not give consumers one atomic transaction payload. Checkpoint and WAL
|
|
160
|
+
# acknowledgement still wait for every event in the transaction.
|
|
150
161
|
#
|
|
151
162
|
# transaction:
|
|
152
|
-
# Deliver a complete TransactionEnvelope as one payload. This is
|
|
153
|
-
#
|
|
154
|
-
# has been delivered successfully.
|
|
163
|
+
# Deliver a complete TransactionEnvelope as one payload. This is recommended
|
|
164
|
+
# when downstream consumers need transaction-level context.
|
|
155
165
|
#
|
|
156
166
|
# Recommended:
|
|
157
167
|
# transaction
|
data/config/mammoth.schema.json
CHANGED
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
"type": "string",
|
|
142
142
|
"minLength": 1
|
|
143
143
|
},
|
|
144
|
-
"description": "PostgreSQL publications to subscribe to. Publications define which table changes PostgreSQL sends to Mammoth."
|
|
144
|
+
"description": "PostgreSQL publications to subscribe to. Publications define which table changes PostgreSQL sends to Mammoth. Tables publishing UPDATE or DELETE require a usable primary key, selected replica-identity index, or REPLICA IDENTITY FULL."
|
|
145
145
|
},
|
|
146
146
|
"start_lsn": {
|
|
147
147
|
"type": [
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
},
|
|
153
153
|
"auto_create_slot": {
|
|
154
154
|
"type": "boolean",
|
|
155
|
-
"description": "When true, Mammoth creates
|
|
155
|
+
"description": "When true, Mammoth creates a missing replication slot only for a fresh stream with no configured or persisted resume LSN. Mammoth never recreates a missing slot while checkpoint continuity is required."
|
|
156
156
|
},
|
|
157
157
|
"temporary_slot": {
|
|
158
158
|
"type": "boolean",
|
data/lib/mammoth/application.rb
CHANGED
|
@@ -9,8 +9,10 @@ module Mammoth
|
|
|
9
9
|
# composition stays outside this class so the application runtime consumes an
|
|
10
10
|
# injected CDC work source rather than owning upstream CDC source-adapter
|
|
11
11
|
# lifecycle decisions.
|
|
12
|
+
# rubocop:disable Metrics/ClassLength
|
|
12
13
|
class Application
|
|
13
|
-
attr_reader :config, :state_adapter, :consumer, :delivery_worker, :checkpoint_store, :lifecycle_hooks, :observer
|
|
14
|
+
attr_reader :config, :state_adapter, :consumer, :delivery_worker, :checkpoint_store, :lifecycle_hooks, :observer,
|
|
15
|
+
:progress_coordinator
|
|
14
16
|
|
|
15
17
|
# @param config [Mammoth::Configuration] loaded configuration
|
|
16
18
|
# @param source [#each, nil] injectable event source for tests and demos
|
|
@@ -27,6 +29,7 @@ module Mammoth
|
|
|
27
29
|
@state_adapter = state_adapter || build_state_adapter
|
|
28
30
|
@checkpoint_store = @state_adapter.checkpoint_store
|
|
29
31
|
@consumer = ReplicationConsumer.new(source: source || build_source, delivery_unit: delivery_unit)
|
|
32
|
+
@progress_coordinator = build_progress_coordinator
|
|
30
33
|
@delivery_worker = sink ? build_delivery_worker(sink: sink, sleeper: sleeper) : build_configured_delivery_worker(sleeper:)
|
|
31
34
|
end
|
|
32
35
|
|
|
@@ -72,12 +75,14 @@ module Mammoth
|
|
|
72
75
|
def process_consumer(runtime)
|
|
73
76
|
processed = 0
|
|
74
77
|
|
|
75
|
-
consumer.
|
|
78
|
+
consumer.start_with_boundaries do |work, group_end|
|
|
79
|
+
progress_coordinator.register(work, group_end:)
|
|
76
80
|
process_work(runtime, work)
|
|
77
81
|
processed += 1
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
runtime.flush
|
|
85
|
+
progress_coordinator.finalize
|
|
81
86
|
processed
|
|
82
87
|
end
|
|
83
88
|
|
|
@@ -88,7 +93,11 @@ module Mammoth
|
|
|
88
93
|
def build_runtime
|
|
89
94
|
Runtimes::Registry.build(
|
|
90
95
|
runtime_adapter,
|
|
91
|
-
processor: DeliveryProcessor.new(
|
|
96
|
+
processor: DeliveryProcessor.new(
|
|
97
|
+
delivery_worker:,
|
|
98
|
+
delivery_unit: delivery_unit,
|
|
99
|
+
progress_coordinator: progress_coordinator
|
|
100
|
+
),
|
|
92
101
|
concurrency: runtime_concurrency,
|
|
93
102
|
timeout: runtime_timeout,
|
|
94
103
|
preserve_order: runtime_preserve_order?,
|
|
@@ -101,6 +110,27 @@ module Mammoth
|
|
|
101
110
|
Sources::Postgres.new(config, checkpoint_store: checkpoint_store)
|
|
102
111
|
end
|
|
103
112
|
|
|
113
|
+
def build_progress_coordinator
|
|
114
|
+
DeliveryProgressCoordinator.new(
|
|
115
|
+
checkpoint_store: checkpoint_store,
|
|
116
|
+
source_name: config.dig("mammoth", "name"),
|
|
117
|
+
slot_name: config.dig("replication", "slot"),
|
|
118
|
+
publication_name: Array(config.dig("replication", "publications")).join(","),
|
|
119
|
+
acknowledger: source_acknowledger,
|
|
120
|
+
position_resolver: source_position_resolver
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def source_acknowledger
|
|
125
|
+
source = consumer.source
|
|
126
|
+
source.method(:acknowledge) if source.respond_to?(:acknowledge)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def source_position_resolver
|
|
130
|
+
source = consumer.source
|
|
131
|
+
source.method(:progress_position_for) if source.respond_to?(:progress_position_for)
|
|
132
|
+
end
|
|
133
|
+
|
|
104
134
|
def build_delivery_worker(sink:, sleeper:, delivery_policy: {})
|
|
105
135
|
DeliveryWorker.from_config(
|
|
106
136
|
config,
|
|
@@ -176,4 +206,5 @@ module Mammoth
|
|
|
176
206
|
value.nil? || value
|
|
177
207
|
end
|
|
178
208
|
end
|
|
209
|
+
# rubocop:enable Metrics/ClassLength
|
|
179
210
|
end
|
|
@@ -32,14 +32,16 @@ module Mammoth
|
|
|
32
32
|
|
|
33
33
|
concurrent_safe!
|
|
34
34
|
|
|
35
|
-
attr_reader :delivery_worker, :delivery_unit
|
|
35
|
+
attr_reader :delivery_worker, :delivery_unit, :progress_coordinator
|
|
36
36
|
|
|
37
37
|
# @param delivery_worker [Mammoth::DeliveryWorker] relay-aware delivery worker
|
|
38
38
|
# @param delivery_unit [String, Symbol] event or transaction
|
|
39
|
-
|
|
39
|
+
# @param progress_coordinator [Mammoth::DeliveryProgressCoordinator, nil] contiguous progress coordinator
|
|
40
|
+
def initialize(delivery_worker:, delivery_unit: :event, progress_coordinator: nil)
|
|
40
41
|
super()
|
|
41
42
|
@delivery_worker = delivery_worker
|
|
42
43
|
@delivery_unit = delivery_unit.to_sym
|
|
44
|
+
@progress_coordinator = progress_coordinator
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
# @return [Boolean] true when this processor instance is safe for concurrent execution.
|
|
@@ -63,6 +65,7 @@ module Mammoth
|
|
|
63
65
|
# @return [CDC::Core::ProcessorResult] normalized processor result
|
|
64
66
|
def process(work)
|
|
65
67
|
summary = deliver(work)
|
|
68
|
+
progress_coordinator&.complete(work)
|
|
66
69
|
build_result(work, summary)
|
|
67
70
|
rescue StandardError => e
|
|
68
71
|
failure_result(work, e, retryable: e.is_a?(DeliveryError))
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mammoth
|
|
4
|
+
# Coordinates durable delivery progress across out-of-order workers.
|
|
5
|
+
#
|
|
6
|
+
# Work is registered in source order before dispatch. Completion may arrive
|
|
7
|
+
# in any order, but a checkpoint and upstream acknowledgement advance only
|
|
8
|
+
# when every item in the oldest closed source group has a durable outcome.
|
|
9
|
+
# A source group is normally one PostgreSQL transaction.
|
|
10
|
+
class DeliveryProgressCoordinator
|
|
11
|
+
# Mutable source transaction tracked by the coordinator.
|
|
12
|
+
class Group
|
|
13
|
+
attr_reader :items
|
|
14
|
+
attr_accessor :closed
|
|
15
|
+
|
|
16
|
+
# @param items [Array<DeliveryProgressCoordinator::Entry>] registered work
|
|
17
|
+
# @param closed [Boolean] whether the source group is complete
|
|
18
|
+
def initialize(items:, closed:)
|
|
19
|
+
@items = items
|
|
20
|
+
@closed = closed
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Mutable completion state for one registered work item.
|
|
25
|
+
class Entry
|
|
26
|
+
attr_reader :work, :source_position
|
|
27
|
+
attr_accessor :completed
|
|
28
|
+
|
|
29
|
+
# @param work [Object] registered work
|
|
30
|
+
# @param source_position [String, Integer, nil] upstream position
|
|
31
|
+
# @param completed [Boolean] whether a durable outcome was recorded
|
|
32
|
+
def initialize(work:, source_position:, completed:)
|
|
33
|
+
@work = work
|
|
34
|
+
@source_position = source_position
|
|
35
|
+
@completed = completed
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_reader :checkpoint_store, :source_name, :slot_name, :publication_name
|
|
40
|
+
|
|
41
|
+
# @param checkpoint_store [Mammoth::CheckpointStore] durable checkpoint persistence
|
|
42
|
+
# @param source_name [String] logical source name
|
|
43
|
+
# @param slot_name [String] PostgreSQL replication slot name
|
|
44
|
+
# @param publication_name [String] publication name
|
|
45
|
+
# @param acknowledger [#call, nil] upstream durable-progress acknowledgement
|
|
46
|
+
# @param position_resolver [#call, nil] source-owned durable position resolver
|
|
47
|
+
def initialize(checkpoint_store:, source_name:, slot_name:, publication_name:, acknowledger: nil,
|
|
48
|
+
position_resolver: nil)
|
|
49
|
+
@checkpoint_store = checkpoint_store
|
|
50
|
+
@source_name = source_name
|
|
51
|
+
@slot_name = slot_name
|
|
52
|
+
@publication_name = publication_name
|
|
53
|
+
@acknowledger = acknowledger
|
|
54
|
+
@position_resolver = position_resolver
|
|
55
|
+
# rubocop:disable Layout/LeadingCommentSpace -- Steep inline type syntax requires `#:`.
|
|
56
|
+
@groups = [] #: Array[Group]
|
|
57
|
+
@entries_by_work = {} #: Hash[untyped, Array[Entry]]
|
|
58
|
+
# rubocop:enable Layout/LeadingCommentSpace
|
|
59
|
+
@entries_by_work.compare_by_identity
|
|
60
|
+
@mutex = Mutex.new
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Register work before dispatch.
|
|
64
|
+
#
|
|
65
|
+
# @param work [CDC::Core::ChangeEvent, CDC::Core::TransactionEnvelope] work item
|
|
66
|
+
# @param group_end [Boolean] whether this is the final item in its source group
|
|
67
|
+
# @return [void]
|
|
68
|
+
def register(work, group_end:)
|
|
69
|
+
@mutex.synchronize do
|
|
70
|
+
group = current_group
|
|
71
|
+
entry = Entry.new(work: work, source_position: source_position(work), completed: false)
|
|
72
|
+
group.items << entry
|
|
73
|
+
(@entries_by_work[work] ||= []) << entry # steep:ignore
|
|
74
|
+
group.closed = true if group_end
|
|
75
|
+
end
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Mark a work item durably resolved and advance all newly contiguous groups.
|
|
80
|
+
#
|
|
81
|
+
# @param work [CDC::Core::ChangeEvent, CDC::Core::TransactionEnvelope] work item
|
|
82
|
+
# @return [String, nil] latest source position advanced by this call
|
|
83
|
+
def complete(work)
|
|
84
|
+
@mutex.synchronize do
|
|
85
|
+
entry = pending_entry_for(work)
|
|
86
|
+
raise ReplicationError, "delivery progress completed before registration" unless entry
|
|
87
|
+
|
|
88
|
+
entry.completed = true
|
|
89
|
+
advance_contiguous_groups
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Close and advance a final source group after a clean end-of-stream.
|
|
94
|
+
#
|
|
95
|
+
# @return [String, nil] latest source position advanced by this call
|
|
96
|
+
def finalize
|
|
97
|
+
@mutex.synchronize do
|
|
98
|
+
@groups.last.closed = true if @groups.last
|
|
99
|
+
advance_contiguous_groups
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def current_group
|
|
106
|
+
return @groups.last if @groups.last && !@groups.last.closed
|
|
107
|
+
|
|
108
|
+
Group.new(items: [], closed: false).tap { |group| @groups << group }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def pending_entry_for(work)
|
|
112
|
+
@entries_by_work.fetch(work, []).find { |entry| !entry.completed } # steep:ignore
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def advance_contiguous_groups
|
|
116
|
+
advanced_position = nil
|
|
117
|
+
|
|
118
|
+
while (group = @groups.first) && group.closed && group.items.all?(&:completed)
|
|
119
|
+
position = group.items.reverse_each.lazy.map(&:source_position).find { |value| !value.nil? }
|
|
120
|
+
persist_and_acknowledge(position) if position
|
|
121
|
+
advanced_position = position || advanced_position
|
|
122
|
+
remove_group(group)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
advanced_position
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def persist_and_acknowledge(position)
|
|
129
|
+
checkpoint_store.write(
|
|
130
|
+
source_name: source_name,
|
|
131
|
+
slot_name: slot_name,
|
|
132
|
+
publication_name: publication_name,
|
|
133
|
+
last_lsn: position
|
|
134
|
+
)
|
|
135
|
+
@acknowledger&.call(position)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def remove_group(group)
|
|
139
|
+
@groups.shift
|
|
140
|
+
group.items.each do |entry|
|
|
141
|
+
entries = @entries_by_work[entry.work]
|
|
142
|
+
entries.delete(entry)
|
|
143
|
+
@entries_by_work.delete(entry.work) if entries.empty?
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def source_position(work)
|
|
148
|
+
return @position_resolver.call(work) if @position_resolver
|
|
149
|
+
|
|
150
|
+
work.commit_lsn if work.respond_to?(:commit_lsn)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|