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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -0
  3. data/README.md +93 -10
  4. data/config/mammoth.example.yml +19 -9
  5. data/config/mammoth.schema.json +3 -3
  6. data/lib/mammoth/application.rb +54 -43
  7. data/lib/mammoth/cli.rb +1 -13
  8. data/lib/mammoth/commands/bootstrap_command.rb +11 -7
  9. data/lib/mammoth/commands/dead_letters_command.rb +5 -3
  10. data/lib/mammoth/commands/deliver_sample_command.rb +1 -1
  11. data/lib/mammoth/commands/status_command.rb +5 -5
  12. data/lib/mammoth/concurrent_delivery_runtime.rb +15 -3
  13. data/lib/mammoth/dead_letter_commands.rb +16 -25
  14. data/lib/mammoth/dead_letter_store.rb +1 -1
  15. data/lib/mammoth/delivery_processor.rb +51 -9
  16. data/lib/mammoth/delivery_progress_coordinator.rb +153 -0
  17. data/lib/mammoth/delivery_worker.rb +19 -33
  18. data/lib/mammoth/dispatch_metrics.rb +55 -0
  19. data/lib/mammoth/event_serializer.rb +23 -12
  20. data/lib/mammoth/fanout_delivery_worker.rb +4 -4
  21. data/lib/mammoth/metrics_observer.rb +52 -0
  22. data/lib/mammoth/observability_metrics.rb +71 -0
  23. data/lib/mammoth/observability_server.rb +8 -6
  24. data/lib/mammoth/observability_snapshot.rb +118 -66
  25. data/lib/mammoth/operational_state/adapter.rb +16 -0
  26. data/lib/mammoth/operational_state/registry.rb +8 -0
  27. data/lib/mammoth/operational_state/sqlite_adapter.rb +18 -2
  28. data/lib/mammoth/persisted_payload_deserializer.rb +85 -0
  29. data/lib/mammoth/postgres_observability_metrics.rb +80 -0
  30. data/lib/mammoth/replication_consumer.rb +41 -52
  31. data/lib/mammoth/runtimes/batching_runtime.rb +59 -0
  32. data/lib/mammoth/runtimes/concurrent_adapter.rb +4 -2
  33. data/lib/mammoth/runtimes/inline_adapter.rb +19 -5
  34. data/lib/mammoth/runtimes/registry.rb +3 -2
  35. data/lib/mammoth/sources/postgres.rb +261 -4
  36. data/lib/mammoth/sources/postgres_publication_inspector.rb +214 -0
  37. data/lib/mammoth/sources/postgres_slot_health.rb +90 -0
  38. data/lib/mammoth/status.rb +22 -18
  39. data/lib/mammoth/transaction_envelope_serializer.rb +13 -15
  40. data/lib/mammoth/version.rb +1 -1
  41. data/lib/mammoth/webhook_sink.rb +2 -2
  42. data/lib/mammoth.rb +12 -1
  43. metadata +38 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bca055c346ee0a0b651861f60fca15910708ea772b1cfb661cbc8a46040a1e0
4
- data.tar.gz: 03d9953cec8a9d82859cbfe446736b4cdc216d46e36244bb5fca4abe455fb0ef
3
+ metadata.gz: fc13868a4aa6d845e63c655af36b9bbb69604be8bfb5b7ab6beda7d1a765c145
4
+ data.tar.gz: 9a3c9514953f68db2d5287903e29980ffb147460691817f5de1a19a8123f1727
5
5
  SHA512:
6
- metadata.gz: 3394c678ad2135d34d9d5429599d33d02b8390d7d366821f71356fb0ed50bee71892594901372b0083646f4b8f2332e80977933651102c2e72c3a9309770bee2
7
- data.tar.gz: 3b67803444f90aa6951f6c554d70b0d7b6e88d7b020a905edbda6357a3f54a7eacbd5b3087a66fd1c002f740596cdcf9515f5ae565e6eb7f00cf66ecba648791
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
- ## OSS MVP
42
+ ## v1.0 Release Scope
43
43
 
44
- Mammoth OSS includes:
44
+ Mammoth 1.0 includes:
45
45
 
46
- - CLI foundation
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, checkpoint, and DLQ handling
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, ordering policy, or runtime execution. Those belong to the
74
- upstream CDC Ecosystem components.
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
- Deliver a single normalized event JSON file through Mammoth's delivery path:
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).
@@ -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 so PostgreSQL knows the
131
- # consumer is alive and can advance slot state safely.
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 weaker
149
- # transaction-level semantics.
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 the safer
153
- # default because checkpointing can advance only after the whole transaction
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
@@ -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 the replication slot if it does not exist. Convenient for development; DBA-managed slots are recommended for production."
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 submitted to the concurrent runtime together. Values greater than 1 allow ordering-insensitive deliveries to overlap more visibly."
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",
@@ -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
- # SQLite operational memory, replication consumer, delivery worker, checkpoint
8
- # store, dead letter store, and webhook sink. Upstream PostgreSQL transport
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
- def initialize(config, source: nil, sink: nil, sleeper: Kernel.method(:sleep), lifecycle_hooks: LifecycleHooks.new)
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
- @state_adapter = build_state_adapter
24
- @checkpoint_store = state_adapter.checkpoint_store
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
- def flush_batch(runtime, batch)
89
- return 0 unless runtime_batching?(runtime) && batch.any?
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
- process_batch(runtime, batch)
84
+ runtime.flush
85
+ progress_coordinator.finalize
86
+ processed
92
87
  end
93
88
 
94
89
  def process_work(runtime, work)
95
- runtime.process_many([work])
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(delivery_worker:, delivery_unit: delivery_unit),
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.build(operational_state_adapter, config)
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
- config = load_config
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 SQLite database.
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
- store = SQLiteStore.connect(config.dig("sqlite", "path")).bootstrap!
19
- output.puts "SQLite database initialized"
20
- output.puts "Path: #{store.path}"
21
- output.puts "Tables: #{store.tables.join(", ")}"
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