mammoth 0.8.0 → 1.0.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 +104 -0
- data/README.md +93 -10
- data/config/mammoth.example.yml +19 -9
- data/config/mammoth.schema.json +3 -3
- data/lib/mammoth/application.rb +54 -43
- data/lib/mammoth/cli.rb +1 -13
- data/lib/mammoth/commands/bootstrap_command.rb +11 -7
- data/lib/mammoth/commands/dead_letters_command.rb +5 -3
- data/lib/mammoth/commands/deliver_sample_command.rb +1 -1
- data/lib/mammoth/commands/status_command.rb +5 -5
- data/lib/mammoth/concurrent_delivery_runtime.rb +15 -3
- data/lib/mammoth/dead_letter_commands.rb +16 -25
- data/lib/mammoth/dead_letter_store.rb +1 -1
- data/lib/mammoth/delivery_processor.rb +51 -9
- data/lib/mammoth/delivery_progress_coordinator.rb +153 -0
- data/lib/mammoth/delivery_worker.rb +19 -33
- data/lib/mammoth/dispatch_metrics.rb +55 -0
- data/lib/mammoth/event_serializer.rb +23 -12
- data/lib/mammoth/fanout_delivery_worker.rb +4 -4
- data/lib/mammoth/metrics_observer.rb +52 -0
- data/lib/mammoth/observability_metrics.rb +71 -0
- data/lib/mammoth/observability_server.rb +8 -6
- data/lib/mammoth/observability_snapshot.rb +118 -66
- data/lib/mammoth/operational_state/adapter.rb +16 -0
- data/lib/mammoth/operational_state/registry.rb +8 -0
- data/lib/mammoth/operational_state/sqlite_adapter.rb +18 -2
- data/lib/mammoth/persisted_payload_deserializer.rb +85 -0
- data/lib/mammoth/postgres_observability_metrics.rb +80 -0
- data/lib/mammoth/replication_consumer.rb +41 -52
- data/lib/mammoth/runtimes/batching_runtime.rb +59 -0
- data/lib/mammoth/runtimes/concurrent_adapter.rb +4 -2
- data/lib/mammoth/runtimes/inline_adapter.rb +19 -5
- data/lib/mammoth/runtimes/registry.rb +3 -2
- data/lib/mammoth/sources/postgres.rb +261 -4
- data/lib/mammoth/sources/postgres_publication_inspector.rb +214 -0
- data/lib/mammoth/sources/postgres_slot_health.rb +90 -0
- data/lib/mammoth/status.rb +22 -18
- data/lib/mammoth/transaction_envelope_serializer.rb +13 -15
- data/lib/mammoth/version.rb +1 -1
- data/lib/mammoth/webhook_sink.rb +2 -2
- data/lib/mammoth.rb +12 -1
- metadata +38 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc13868a4aa6d845e63c655af36b9bbb69604be8bfb5b7ab6beda7d1a765c145
|
|
4
|
+
data.tar.gz: 9a3c9514953f68db2d5287903e29980ffb147460691817f5de1a19a8123f1727
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 552aa45078fa0ed209890bbfe8eb2dc0672234d4e008c8cc0edd954a284e0b40bffafe16a2a8c2df8dd0219aee3eed742ba2a38531cd3d2acd7b13b35bb48b11
|
|
7
|
+
data.tar.gz: 69f509480ecdd46aa50a7879c3dbc1589becce9a651b1697e1f164ea0d6be781edb9f54238940a32553b59956c8cba05cd2b0caaffca57812823cb32e6fa481b
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,110 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.0.0 - 2026-07-17
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added PostgreSQL slot readiness diagnostics and Prometheus gauges for
|
|
10
|
+
inspection availability, presence, activity, retained WAL bytes, safe WAL
|
|
11
|
+
size, WAL status, invalidation, inactivity time, and restart/flush LSNs.
|
|
12
|
+
- `/readyz` now fails closed when the configured slot is missing, inactive,
|
|
13
|
+
invalidated, conflicting, WAL-lost, or cannot be inspected.
|
|
14
|
+
- Added startup validation for every configured publication table that publishes
|
|
15
|
+
`UPDATE` or `DELETE`, accepting a usable primary key, selected replica-identity
|
|
16
|
+
index, or `REPLICA IDENTITY FULL` and reporting all invalid tables together.
|
|
17
|
+
- Added catalog-derived, ordered replica-identity mappings for relation OIDs and
|
|
18
|
+
schema-qualified tables.
|
|
19
|
+
- Added a self-verifying live PostgreSQL example for composite, non-`id`
|
|
20
|
+
replica identities across `INSERT`, `UPDATE`, and `DELETE`.
|
|
21
|
+
- Added a live PostgreSQL example that shows fail-closed restart after slot
|
|
22
|
+
invalidation and the explicit operator reconciliation required to resume.
|
|
23
|
+
- Added a PostgreSQL observability example correlating Mammoth readiness and
|
|
24
|
+
Prometheus slot gauges with native replication and publication catalogs.
|
|
25
|
+
- Added a live additive schema-evolution example demonstrating consumer-first
|
|
26
|
+
compatibility, relation-metadata refresh, and the non-delivery of DDL.
|
|
27
|
+
- Added a destination idempotency example proving atomic receiver-side
|
|
28
|
+
duplicate suppression across isolated Mammoth delivery ledgers.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Added PostgreSQL slot preflight before streaming and fail-closed handling for
|
|
33
|
+
missing, active, lost, invalidated, incompatible, and
|
|
34
|
+
checkpoint-unreachable slots.
|
|
35
|
+
- Disabled automatic slot recreation whenever a configured or persisted resume
|
|
36
|
+
LSN requires WAL continuity.
|
|
37
|
+
- Rejected temporary-slot recovery from durable checkpoints.
|
|
38
|
+
- Preserved pgoutput-client's acknowledgement-compatible transport LSN
|
|
39
|
+
separately from normalized CDC-core `commit_lsn` payload context.
|
|
40
|
+
- Checkpoints and PostgreSQL feedback now use the source-owned transport
|
|
41
|
+
watermark, preventing decimal transaction commit values from reaching
|
|
42
|
+
`runner.ack`.
|
|
43
|
+
- Event-mode delivery resolves an envelope's transport position for each exact
|
|
44
|
+
child event without mutating or rebuilding CDC-core work.
|
|
45
|
+
- Wired publication identity mappings into
|
|
46
|
+
`Pgoutput::SourceAdapter::ReplicaIdentityResolver` so composite and non-`id`
|
|
47
|
+
keys are normalized without partial identity data.
|
|
48
|
+
|
|
49
|
+
### Quality
|
|
50
|
+
|
|
51
|
+
- Upgraded pgoutput-client to 0.4.0 and covered slot identity, health,
|
|
52
|
+
retained-WAL reporting, continuity boundaries, and safe first-time creation.
|
|
53
|
+
- Upgraded pgoutput-source-adapter to 0.3.0 and covered catalog-to-resolver
|
|
54
|
+
composition for composite replica identities.
|
|
55
|
+
- Upgraded pgoutput-decoder to 0.2.0 so key-only old tuples preserve their
|
|
56
|
+
exact decoded columns instead of synthesizing absent non-key values.
|
|
57
|
+
- Added end-to-end source/coordinator coverage proving that a transaction with
|
|
58
|
+
decimal `commit_lsn` checkpoints and acknowledges its formatted transport LSN.
|
|
59
|
+
- Added opt-in PostgreSQL 17 integration coverage for transactional `INSERT`,
|
|
60
|
+
`UPDATE`, and `DELETE` with composite identity, replication reconnects,
|
|
61
|
+
concurrent out-of-order completion, contiguous slot acknowledgement, and
|
|
62
|
+
fail-closed slot invalidation; CI and release gates run the suite against a
|
|
63
|
+
logical-replication service.
|
|
64
|
+
- Strengthened boundary coverage for source-owned position resolution and
|
|
65
|
+
publication-catalog inspection, and synchronized PostgreSQL, runtime,
|
|
66
|
+
operational-state, configuration, and transaction-example documentation.
|
|
67
|
+
- Documented PostgreSQL WAL guardrails, slot/catalog monitoring, DDL and
|
|
68
|
+
sequence limitations, destination conflict ownership, upgrade recovery,
|
|
69
|
+
troubleshooting, example caveats, and Kubernetes deployment policy.
|
|
70
|
+
|
|
71
|
+
## 0.9.0
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- Refactored observability, status, bootstrap, and dead-letter command
|
|
76
|
+
composition to consume `OperationalState::Adapter` instead of opening SQLite
|
|
77
|
+
or constructing concrete stores.
|
|
78
|
+
- Added adapter-owned `bootstrap!`, `ready?`, and configured registry
|
|
79
|
+
composition for backend-neutral operational tooling.
|
|
80
|
+
- Inject the configured operational-state delivered-envelope store into every
|
|
81
|
+
delivery worker instead of deriving a SQLite ledger from the checkpoint store.
|
|
82
|
+
- Made `Mammoth::DeliveryProcessor` implement `CDC::Core::Processor` and return
|
|
83
|
+
`CDC::Core::ProcessorResult` for successful, skipped, and failed deliveries.
|
|
84
|
+
- Wired `CDC::Core::Observer` notifications through inline and concurrent
|
|
85
|
+
runtimes and translated canonical dispatch metrics into Mammoth Prometheus
|
|
86
|
+
counters.
|
|
87
|
+
- Replaced duck-typed CDC lookalikes with exact `CDC::Core::ChangeEvent` and
|
|
88
|
+
`CDC::Core::TransactionEnvelope` objects.
|
|
89
|
+
- Enforced the PostgreSQL source output contract by rejecting non-core work
|
|
90
|
+
yielded by injected source adapters.
|
|
91
|
+
- Added an explicit persisted-payload deserialization boundary for sample JSON
|
|
92
|
+
and dead-letter replay.
|
|
93
|
+
- Moved configured batch accumulation and final partial-batch submission out of
|
|
94
|
+
`Mammoth::Application` and into the delivery runtime layer.
|
|
95
|
+
|
|
96
|
+
### Quality
|
|
97
|
+
|
|
98
|
+
- Added processor contract, observer, runtime notification, dispatch counter,
|
|
99
|
+
and Prometheus exposition coverage.
|
|
100
|
+
- Added exact core-type and persisted-payload round-trip coverage.
|
|
101
|
+
- Strengthened architecture boundary coverage with executable pgoutput adapter
|
|
102
|
+
output checks, downstream dependency guards, and an exact core-output RBS
|
|
103
|
+
contract for the PostgreSQL source.
|
|
104
|
+
- Added runtime batching ownership coverage so application orchestration cannot
|
|
105
|
+
reintroduce buffering or batch partitioning.
|
|
106
|
+
- Updated RBS signatures, YARD API documentation, examples, and benchmark
|
|
107
|
+
descriptions for the corrected processor/observer boundary.
|
|
108
|
+
|
|
5
109
|
## 0.8.0
|
|
6
110
|
|
|
7
111
|
### Changed
|
data/README.md
CHANGED
|
@@ -39,45 +39,105 @@ API documentation:
|
|
|
39
39
|
https://kanutocd.github.io/mammoth/Mammoth.html
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## v1.0 Release Scope
|
|
43
43
|
|
|
44
|
-
Mammoth
|
|
44
|
+
Mammoth 1.0 includes:
|
|
45
45
|
|
|
46
|
-
- CLI
|
|
46
|
+
- operator CLI for validation, bootstrap, status, delivery, observability, and
|
|
47
|
+
dead-letter workflows
|
|
47
48
|
- YAML configuration loading
|
|
48
49
|
- JSON Schema-backed configuration validation
|
|
49
50
|
- SQLite operational memory bootstrap
|
|
50
51
|
- checkpoint persistence
|
|
51
52
|
- dead letter persistence
|
|
53
|
+
- delivered-envelope ledger persistence
|
|
52
54
|
- webhook delivery sink
|
|
53
55
|
- webhook fanout to multiple destinations
|
|
54
56
|
- fanout route filters by schema, table, and operation
|
|
55
57
|
- per-destination enable/disable and retry policy controls
|
|
56
|
-
- delivery worker with retry,
|
|
58
|
+
- delivery worker with retry, delivered-ledger, and DLQ handling
|
|
59
|
+
- contiguous delivery watermark for checkpoint and PostgreSQL acknowledgement
|
|
60
|
+
- source-owned transport LSN preservation independent of payload `commit_lsn`
|
|
61
|
+
- fail-closed PostgreSQL slot and checkpoint continuity preflight
|
|
62
|
+
- fail-closed publication replica-identity preflight for `UPDATE` and `DELETE`
|
|
57
63
|
- dead-letter inspection and filtered replay commands
|
|
58
64
|
- CDC-core event serialization boundary
|
|
59
65
|
- CDC Ecosystem source-adapter integration boundary
|
|
60
66
|
- Docker image support
|
|
61
67
|
- public Helm chart support
|
|
62
68
|
- unit and e2e test tasks
|
|
63
|
-
- health and metrics endpoints
|
|
69
|
+
- health, PostgreSQL slot readiness, and retained-WAL metrics endpoints
|
|
70
|
+
- canonical CDC dispatch counters through a `CDC::Core::Observer`
|
|
64
71
|
- explicit extension registries for state, destination, and runtime adapters
|
|
65
72
|
- node identity and local capability reporting
|
|
66
73
|
- lifecycle hooks, configuration providers, and reusable local command objects
|
|
67
74
|
|
|
75
|
+
## Feature Examples
|
|
76
|
+
|
|
77
|
+
The runnable examples are organized around production behaviors and failure
|
|
78
|
+
modes, not isolated API snippets.
|
|
79
|
+
|
|
80
|
+
| Example | v1 capability demonstrated |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| [`live_postgres_webhook`](examples/live_postgres_webhook) | End-to-end PostgreSQL logical replication into webhook delivery. |
|
|
83
|
+
| [`transaction_webhook`](examples/transaction_webhook) | TransactionEnvelope preservation through the concurrent runtime. |
|
|
84
|
+
| [`webhook_fanout`](examples/webhook_fanout) | Routed multi-destination fanout, environment-backed headers, signing, and independent retry policies. |
|
|
85
|
+
| [`ordering`](examples/ordering) | Ordered and throughput-oriented transaction scheduling. |
|
|
86
|
+
| [`checkpoint_recovery`](examples/checkpoint_recovery) | Durable restart recovery, replay suppression, checkpointing, and acknowledgement. |
|
|
87
|
+
| [`slot_invalidation_recovery`](examples/slot_invalidation_recovery) | Fail-closed slot invalidation and explicit operator reconciliation. |
|
|
88
|
+
| [`composite_replica_identity`](examples/composite_replica_identity) | Composite, non-`id` replica identity across `INSERT`, `UPDATE`, and `DELETE`. |
|
|
89
|
+
| [`postgres_observability`](examples/postgres_observability) | Slot readiness and Prometheus metrics correlated with PostgreSQL catalogs. |
|
|
90
|
+
| [`schema_evolution`](examples/schema_evolution) | Consumer-first additive schema evolution without implying DDL delivery. |
|
|
91
|
+
| [`destination_idempotency`](examples/destination_idempotency) | Atomic destination-side duplicate suppression across isolated relay ledgers. |
|
|
92
|
+
| [`failing_webhook_retry`](examples/failing_webhook_retry) | Retry exhaustion and durable dead-letter persistence. |
|
|
93
|
+
| [`operational_state`](examples/operational_state) | Inspectable checkpoints, delivered ledgers, and dead letters. |
|
|
94
|
+
| [`kubernetes_helm`](examples/kubernetes_helm) | Single-consumer Kubernetes deployment using the public Helm chart. |
|
|
95
|
+
|
|
96
|
+
See [`examples/README.md`](examples/README.md) for the complete index, boundary
|
|
97
|
+
notes, and commands.
|
|
98
|
+
|
|
99
|
+
## v1 Compatibility
|
|
100
|
+
|
|
101
|
+
Mammoth 1.x treats its validated configuration, serialized webhook envelopes,
|
|
102
|
+
documented CLI command behavior, and forward operational-state migrations as
|
|
103
|
+
supported contracts. Compatible minor releases may add optional configuration
|
|
104
|
+
or payload fields, but do not remove or reinterpret existing fields.
|
|
105
|
+
|
|
106
|
+
Human-readable CLI formatting and PostgreSQL-derived row columns are not frozen:
|
|
107
|
+
scripts should rely on documented exit behavior, while receivers must tolerate
|
|
108
|
+
additive fields and coordinate source schema changes. See
|
|
109
|
+
[`docs/COMPATIBILITY.md`](docs/COMPATIBILITY.md) for the complete promise and
|
|
110
|
+
major-version boundaries.
|
|
111
|
+
|
|
68
112
|
## Boundary
|
|
69
113
|
|
|
70
114
|
Mammoth begins at CDC-core work items and ends at webhook fanout delivery.
|
|
71
115
|
|
|
72
116
|
Mammoth does not own pgoutput protocol parsing, value decoding, source
|
|
73
|
-
normalization,
|
|
74
|
-
|
|
117
|
+
normalization, or core dispatch vocabulary. Those belong to upstream CDC
|
|
118
|
+
Ecosystem components. Mammoth selects and composes a delivery runtime while
|
|
119
|
+
delegating its scheduling mechanics to the runtime layer. The runtime registry
|
|
120
|
+
wraps the selected adapter with configured batch accumulation; `Application`
|
|
121
|
+
only streams core work and coordinates lifecycle flush and shutdown calls.
|
|
75
122
|
|
|
76
123
|
For the live PostgreSQL stream, `pgoutput-source-adapter` incrementally owns
|
|
77
124
|
`Begin`/`Commit` buffering and emits exact `CDC::Core::ChangeEvent` or
|
|
78
125
|
`CDC::Core::TransactionEnvelope` work items. Mammoth only composes the
|
|
79
126
|
transport, parser, decoder, and source adapter and forwards the resulting core
|
|
80
|
-
work to delivery.
|
|
127
|
+
work to delivery. Mammoth's publication preflight supplies ordered,
|
|
128
|
+
catalog-derived replica-identity columns to the adapter, which owns composite
|
|
129
|
+
and non-`id` key extraction.
|
|
130
|
+
|
|
131
|
+
At the downstream boundary, `Mammoth::DeliveryProcessor` implements
|
|
132
|
+
`CDC::Core::Processor` and returns `CDC::Core::ProcessorResult`. Inline and
|
|
133
|
+
concurrent runtimes notify a `CDC::Core::Observer`; Mammoth's default observer
|
|
134
|
+
maps the canonical started, succeeded, failed, and skipped notifications to
|
|
135
|
+
Prometheus counters.
|
|
136
|
+
|
|
137
|
+
`Mammoth::ReplicationConsumer` accepts only exact core events and transaction
|
|
138
|
+
envelopes. Operator-facing JSON, such as `deliver-sample` input and persisted
|
|
139
|
+
dead letters, is reconstructed by `PersistedPayloadDeserializer` before it
|
|
140
|
+
re-enters delivery; stored hashes do not masquerade as live CDC work.
|
|
81
141
|
|
|
82
142
|
## Extensions
|
|
83
143
|
|
|
@@ -135,7 +195,8 @@ bundle exec ./exe/mammoth start config/mammoth.example.yml
|
|
|
135
195
|
bundle exec ./exe/mammoth observability config/mammoth.example.yml
|
|
136
196
|
```
|
|
137
197
|
|
|
138
|
-
|
|
198
|
+
Reconstruct and deliver a single persisted event JSON file through Mammoth's
|
|
199
|
+
core delivery path:
|
|
139
200
|
|
|
140
201
|
```bash
|
|
141
202
|
bundle exec ./exe/mammoth deliver-sample \
|
|
@@ -152,6 +213,10 @@ Mammoth stores operational memory in SQLite:
|
|
|
152
213
|
- `dead_letters`
|
|
153
214
|
- `delivered_envelopes`
|
|
154
215
|
|
|
216
|
+
SQLite is the built-in default behind `operational_state.adapter`. Bootstrap,
|
|
217
|
+
status, observability, and dead-letter commands consume the adapter contract
|
|
218
|
+
rather than opening SQLite directly.
|
|
219
|
+
|
|
155
220
|
## Performance
|
|
156
221
|
|
|
157
222
|
Mammoth includes local benchmarks for the product surfaces operators tune in
|
|
@@ -185,7 +250,17 @@ script/test-e2e
|
|
|
185
250
|
```
|
|
186
251
|
|
|
187
252
|
The e2e task uses a real HTTP receiver, real SQLite database, and real
|
|
188
|
-
filesystem paths.
|
|
253
|
+
filesystem paths. Set `MAMMOTH_E2E_POSTGRES_URL` to include the real PostgreSQL
|
|
254
|
+
logical-replication scenarios:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
MAMMOTH_E2E_POSTGRES_URL=postgres://postgres:postgres@127.0.0.1:5432/mammoth_e2e \
|
|
258
|
+
bundle exec rake test:e2e
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The PostgreSQL fixture must have `wal_level=logical`; its test role must be able
|
|
262
|
+
to create publications and logical replication slots, terminate replication
|
|
263
|
+
backends, and change `max_slot_wal_keep_size`.
|
|
189
264
|
|
|
190
265
|
## Kubernetes
|
|
191
266
|
|
|
@@ -204,6 +279,14 @@ helm install mammoth charts/mammoth
|
|
|
204
279
|
The chart uses one replica and `Recreate` strategy to respect PostgreSQL's
|
|
205
280
|
logical replication slot constraint: one slot, one active subscriber.
|
|
206
281
|
|
|
282
|
+
Production operators should also monitor retained WAL and slot readiness,
|
|
283
|
+
configure PostgreSQL retention guardrails, and alert on database disk and
|
|
284
|
+
catalog health. DDL and sequence state are not replicated; coordinate schema
|
|
285
|
+
changes with webhook consumers and synchronize sequences externally when
|
|
286
|
+
building a writable database copy. See
|
|
287
|
+
[`docs/POSTGRESQL.md`](docs/POSTGRESQL.md) and
|
|
288
|
+
[`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md).
|
|
289
|
+
|
|
207
290
|
## License
|
|
208
291
|
|
|
209
292
|
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",
|
|
@@ -513,7 +513,7 @@
|
|
|
513
513
|
"batch_size": {
|
|
514
514
|
"type": "integer",
|
|
515
515
|
"minimum": 1,
|
|
516
|
-
"description": "Number of work units
|
|
516
|
+
"description": "Number of work units accumulated by the runtime layer before submission to the selected adapter. Values greater than 1 allow ordering-insensitive deliveries to overlap more visibly."
|
|
517
517
|
},
|
|
518
518
|
"preserve_order": {
|
|
519
519
|
"type": "boolean",
|
data/lib/mammoth/application.rb
CHANGED
|
@@ -4,25 +4,32 @@ module Mammoth
|
|
|
4
4
|
# Top-level Mammoth application runtime.
|
|
5
5
|
#
|
|
6
6
|
# Application wires Mammoth's delivery-side runtime pieces: configuration,
|
|
7
|
-
#
|
|
8
|
-
# store, dead
|
|
7
|
+
# operational-state adapter, replication consumer, delivery worker, checkpoint
|
|
8
|
+
# store, dead-letter store, and webhook sink. Upstream PostgreSQL transport
|
|
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
|
|
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
|
|
17
19
|
# @param sink [#deliver, nil] optional destination sink
|
|
20
|
+
# @param state_adapter [Mammoth::OperationalState::Adapter, nil] operational state dependency
|
|
18
21
|
# @param sleeper [#call] retry sleep strategy
|
|
19
22
|
# @param lifecycle_hooks [Mammoth::LifecycleHooks, Hash] local lifecycle callbacks
|
|
20
|
-
|
|
23
|
+
# @param observer [CDC::Core::Observer, nil] dispatch lifecycle observer
|
|
24
|
+
def initialize(config, source: nil, sink: nil, state_adapter: nil, sleeper: Kernel.method(:sleep),
|
|
25
|
+
lifecycle_hooks: LifecycleHooks.new, observer: nil)
|
|
21
26
|
@config = config
|
|
22
27
|
@lifecycle_hooks = build_lifecycle_hooks(lifecycle_hooks)
|
|
23
|
-
@
|
|
24
|
-
@
|
|
28
|
+
@observer = observer || MetricsObserver.new
|
|
29
|
+
@state_adapter = state_adapter || build_state_adapter
|
|
30
|
+
@checkpoint_store = @state_adapter.checkpoint_store
|
|
25
31
|
@consumer = ReplicationConsumer.new(source: source || build_source, delivery_unit: delivery_unit)
|
|
32
|
+
@progress_coordinator = build_progress_coordinator
|
|
26
33
|
@delivery_worker = sink ? build_delivery_worker(sink: sink, sleeper: sleeper) : build_configured_delivery_worker(sleeper:)
|
|
27
34
|
end
|
|
28
35
|
|
|
@@ -67,50 +74,35 @@ module Mammoth
|
|
|
67
74
|
|
|
68
75
|
def process_consumer(runtime)
|
|
69
76
|
processed = 0
|
|
70
|
-
batch = [nil].compact
|
|
71
|
-
|
|
72
|
-
consumer.start do |work|
|
|
73
|
-
if runtime_batching?(runtime)
|
|
74
|
-
batch << work
|
|
75
|
-
next unless batch.size >= runtime_batch_size
|
|
76
|
-
|
|
77
|
-
processed += process_batch(runtime, batch)
|
|
78
|
-
batch = []
|
|
79
|
-
else
|
|
80
|
-
process_work(runtime, work)
|
|
81
|
-
processed += 1
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
processed + flush_batch(runtime, batch)
|
|
86
|
-
end
|
|
87
77
|
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
consumer.start_with_boundaries do |work, group_end|
|
|
79
|
+
progress_coordinator.register(work, group_end:)
|
|
80
|
+
process_work(runtime, work)
|
|
81
|
+
processed += 1
|
|
82
|
+
end
|
|
90
83
|
|
|
91
|
-
|
|
84
|
+
runtime.flush
|
|
85
|
+
progress_coordinator.finalize
|
|
86
|
+
processed
|
|
92
87
|
end
|
|
93
88
|
|
|
94
89
|
def process_work(runtime, work)
|
|
95
|
-
runtime.
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def process_batch(runtime, batch)
|
|
99
|
-
runtime.process_many(batch)
|
|
100
|
-
batch.size
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def runtime_batching?(runtime)
|
|
104
|
-
runtime && runtime_batch_size > 1
|
|
90
|
+
runtime.process(work)
|
|
105
91
|
end
|
|
106
92
|
|
|
107
93
|
def build_runtime
|
|
108
94
|
Runtimes::Registry.build(
|
|
109
95
|
runtime_adapter,
|
|
110
|
-
processor: DeliveryProcessor.new(
|
|
96
|
+
processor: DeliveryProcessor.new(
|
|
97
|
+
delivery_worker:,
|
|
98
|
+
delivery_unit: delivery_unit,
|
|
99
|
+
progress_coordinator: progress_coordinator
|
|
100
|
+
),
|
|
111
101
|
concurrency: runtime_concurrency,
|
|
112
102
|
timeout: runtime_timeout,
|
|
113
|
-
preserve_order: runtime_preserve_order
|
|
103
|
+
preserve_order: runtime_preserve_order?,
|
|
104
|
+
batch_size: runtime_batch_size,
|
|
105
|
+
observer: observer
|
|
114
106
|
)
|
|
115
107
|
end
|
|
116
108
|
|
|
@@ -118,12 +110,34 @@ module Mammoth
|
|
|
118
110
|
Sources::Postgres.new(config, checkpoint_store: checkpoint_store)
|
|
119
111
|
end
|
|
120
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
|
+
|
|
121
134
|
def build_delivery_worker(sink:, sleeper:, delivery_policy: {})
|
|
122
135
|
DeliveryWorker.from_config(
|
|
123
136
|
config,
|
|
124
137
|
sink: sink,
|
|
125
138
|
checkpoint_store: checkpoint_store,
|
|
126
139
|
dead_letter_store: state_adapter.dead_letter_store,
|
|
140
|
+
delivered_envelope_store: state_adapter.delivered_envelope_store,
|
|
127
141
|
sleeper: sleeper,
|
|
128
142
|
delivery_policy: delivery_policy
|
|
129
143
|
)
|
|
@@ -164,11 +178,7 @@ module Mammoth
|
|
|
164
178
|
end
|
|
165
179
|
|
|
166
180
|
def build_state_adapter
|
|
167
|
-
OperationalState::Registry.
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def operational_state_adapter
|
|
171
|
-
config.dig("operational_state", "adapter") || "sqlite"
|
|
181
|
+
OperationalState::Registry.build_configured(config)
|
|
172
182
|
end
|
|
173
183
|
|
|
174
184
|
def delivery_unit
|
|
@@ -196,4 +206,5 @@ module Mammoth
|
|
|
196
206
|
value.nil? || value
|
|
197
207
|
end
|
|
198
208
|
end
|
|
209
|
+
# rubocop:enable Metrics/ClassLength
|
|
199
210
|
end
|
data/lib/mammoth/cli.rb
CHANGED
|
@@ -5,16 +5,6 @@ require "json"
|
|
|
5
5
|
module Mammoth
|
|
6
6
|
# Small command dispatcher for Mammoth's operator-facing CLI.
|
|
7
7
|
class CLI
|
|
8
|
-
# Internal replay envelope used for transaction dead-letter recovery.
|
|
9
|
-
DEAD_LETTER_TRANSACTION_ENVELOPE = Data.define(
|
|
10
|
-
:events,
|
|
11
|
-
:transaction_id,
|
|
12
|
-
:source_position,
|
|
13
|
-
:commit_lsn,
|
|
14
|
-
:committed_at,
|
|
15
|
-
:metadata
|
|
16
|
-
)
|
|
17
|
-
|
|
18
8
|
# Human-readable command usage printed for invalid or incomplete invocations.
|
|
19
9
|
USAGE = [
|
|
20
10
|
"Usage:",
|
|
@@ -94,9 +84,7 @@ module Mammoth
|
|
|
94
84
|
end
|
|
95
85
|
|
|
96
86
|
def status
|
|
97
|
-
|
|
98
|
-
store = SQLiteStore.connect(config.dig("sqlite", "path"))
|
|
99
|
-
Commands::StatusCommand.new(config, sqlite_store: store).call
|
|
87
|
+
Commands::StatusCommand.new(load_config).call
|
|
100
88
|
end
|
|
101
89
|
|
|
102
90
|
def start
|
|
@@ -2,23 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module Mammoth
|
|
4
4
|
module Commands
|
|
5
|
-
# Initializes the configured operational
|
|
5
|
+
# Initializes the configured operational-state backend.
|
|
6
6
|
class BootstrapCommand
|
|
7
|
-
attr_reader :config, :output
|
|
7
|
+
attr_reader :config, :state_adapter, :output
|
|
8
8
|
|
|
9
9
|
# @param config [Mammoth::Configuration] loaded configuration
|
|
10
|
+
# @param state_adapter [Mammoth::OperationalState::Adapter, nil] operational state dependency
|
|
10
11
|
# @param output [#puts] output stream
|
|
11
|
-
def initialize(config, output: $stdout)
|
|
12
|
+
def initialize(config, state_adapter: nil, output: $stdout)
|
|
12
13
|
@config = config
|
|
14
|
+
@state_adapter = state_adapter || OperationalState::Registry.build_configured(config)
|
|
13
15
|
@output = output
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
# @return [Integer] process-style status code
|
|
17
19
|
def call
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
output.puts "
|
|
21
|
-
output.puts "
|
|
20
|
+
state_adapter.bootstrap!
|
|
21
|
+
summary = state_adapter.summary
|
|
22
|
+
output.puts "Operational state initialized"
|
|
23
|
+
output.puts "Adapter: #{summary.fetch(:adapter)}"
|
|
24
|
+
output.puts "Path: #{summary.fetch(:path)}" if summary.key?(:path)
|
|
25
|
+
output.puts "Tables: #{Array(summary.fetch(:tables)).join(", ")}" if summary.key?(:tables)
|
|
22
26
|
0
|
|
23
27
|
end
|
|
24
28
|
end
|
|
@@ -4,18 +4,20 @@ module Mammoth
|
|
|
4
4
|
module Commands
|
|
5
5
|
# Reusable local command object for dead-letter inspection and replay.
|
|
6
6
|
class DeadLettersCommand
|
|
7
|
-
attr_reader :argv, :lifecycle_hooks
|
|
7
|
+
attr_reader :argv, :state_adapter, :lifecycle_hooks
|
|
8
8
|
|
|
9
9
|
# @param argv [Array<String>] dead-letters CLI-style arguments
|
|
10
|
+
# @param state_adapter [Mammoth::OperationalState::Adapter, nil] operational state dependency
|
|
10
11
|
# @param lifecycle_hooks [Mammoth::LifecycleHooks, Hash] local lifecycle callbacks
|
|
11
|
-
def initialize(argv, lifecycle_hooks: LifecycleHooks.new)
|
|
12
|
+
def initialize(argv, state_adapter: nil, lifecycle_hooks: LifecycleHooks.new)
|
|
12
13
|
@argv = argv
|
|
14
|
+
@state_adapter = state_adapter
|
|
13
15
|
@lifecycle_hooks = lifecycle_hooks
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
# @return [Integer] process-style status code
|
|
17
19
|
def call
|
|
18
|
-
DeadLetterCommands.call(argv, lifecycle_hooks: lifecycle_hooks)
|
|
20
|
+
DeadLetterCommands.call(argv, state_adapter: state_adapter, lifecycle_hooks: lifecycle_hooks)
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
end
|
|
@@ -23,7 +23,7 @@ module Mammoth
|
|
|
23
23
|
def call
|
|
24
24
|
raise ConfigurationError, "event JSON file not found: #{event_path}" unless File.file?(event_path)
|
|
25
25
|
|
|
26
|
-
event = JSON.parse(File.read(event_path))
|
|
26
|
+
event = PersistedPayloadDeserializer.event(JSON.parse(File.read(event_path)))
|
|
27
27
|
processed = Application.new(config, source: [event], lifecycle_hooks: lifecycle_hooks).start
|
|
28
28
|
output.puts "Processed sample events: #{processed}"
|
|
29
29
|
0
|