mammoth 0.5.1 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c04c055c5cf5faedef6100513bc63a9ada3c69b99b904f701d0be93501aa4ebd
4
- data.tar.gz: 15e75ed7ed39723f5f0864253196a830ae0413d6609ae504640be2eca7d6b2da
3
+ metadata.gz: 5fe6f64a39d214c62e11a29fccb091f58cd2a1b8038a0bb803c39c4e018b09b3
4
+ data.tar.gz: f557c7288df617029808c327b656a8b7cb1f7689eb7f4b6d99032b01bcc0535e
5
5
  SHA512:
6
- metadata.gz: 1d0b5446e4b5edd10f5d82007dd8ae85e7e9e40f056725ef1faa5e090c2fa844a9007c94c7ac47b41cb7eeec6fe8d9be3eb0dec7685a757da749ff7dbf85ccbd
7
- data.tar.gz: 3f3185bc9743f100bab26a62f2fa45f94f04aa3243e787ab05d0b13aafc697a22ab432a84da1e40dfd7214e880bafe727e17c8ca4e15236ca97547f55166687c
6
+ metadata.gz: fe49156b57ff2355fd0e78368cd613fa3fa5dcf5d0a5ebf423ec799558b61a70da7529a123f5b254b904a93e7c7355a049b115308eb051e211894b84443f3444
7
+ data.tar.gz: 7b24591aa6f3996e51b5762022f680f212cbb1e8412e0e90ff42e34c3485d9f18fb2f4f4c9b9ca4420a697767400a2d37b6ebc600d203e7c4f5f0fbe5e904da2
data/CHANGELOG.md CHANGED
@@ -1,6 +1,34 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.7.0 Unreleased
4
+
5
+ ### Added
6
+
7
+ - Added explicit adapter registries for operational state, destinations, and delivery runtimes.
8
+ - Added the built-in SQLite operational state adapter.
9
+ - Added the built-in webhook destination adapter registration.
10
+ - Added inline and concurrent runtime adapter registration.
11
+ - Added optional node identity configuration for future control-plane integration.
12
+ - Added local capability reporting for state, destination, runtime, and relay features.
13
+ - Added a reusable status command object behind the CLI status command.
14
+
15
+ ### Changed
16
+
17
+ - Bumped Mammoth version to `0.7.0`.
18
+
19
+ ## 0.6.0
20
+
21
+ ### Added
22
+
23
+ - Added fanout route filters by schema, table, and operation for webhook destinations.
24
+ - Added destination `enabled` controls for config-driven delivery cutovers.
25
+ - Added per-destination retry policy overrides for `max_attempts` and `schedule_seconds`.
26
+ - Added dead-letter replay filters by destination, status, and failed-at time window.
27
+ - Added destination-labeled Prometheus metrics for dead-letter and delivered-envelope counts.
28
+
29
+ ### Changed
30
+
31
+ - Bumped Mammoth version to `0.6.0`.
4
32
 
5
33
  ## 0.5.1
6
34
 
data/README.md CHANGED
@@ -47,14 +47,18 @@ https://kanutocd.github.io/mammoth/Mammoth.html
47
47
  - dead letter persistence
48
48
  - webhook delivery sink
49
49
  - webhook fanout to multiple destinations
50
+ - fanout route filters by schema, table, and operation
51
+ - per-destination enable/disable and retry policy controls
50
52
  - delivery worker with retry, checkpoint, and DLQ handling
51
- - dead-letter inspection and replay commands
53
+ - dead-letter inspection and filtered replay commands
52
54
  - CDC-core event serialization boundary
53
55
  - CDC Ecosystem source-adapter integration boundary
54
56
  - Docker image support
55
57
  - public Helm chart support
56
58
  - unit and e2e test tasks
57
59
  - health and metrics endpoints
60
+ - explicit extension registries for state, destination, and runtime adapters
61
+ - node identity and local capability reporting
58
62
 
59
63
  ## Boundary
60
64
 
@@ -64,6 +68,16 @@ Mammoth does not own pgoutput protocol parsing, value decoding, source
64
68
  normalization, ordering policy, or runtime execution. Those belong to the
65
69
  upstream CDC Ecosystem components.
66
70
 
71
+ ## Extensions
72
+
73
+ Mammoth OSS exposes small adapter registries for future extensions:
74
+
75
+ - operational state adapters
76
+ - destination adapters
77
+ - runtime adapters
78
+
79
+ See [`docs/EXTENSIONS.md`](docs/EXTENSIONS.md).
80
+
67
81
  ## Configuration
68
82
 
69
83
  Mammoth configuration is YAML-backed and IDE-friendly.
@@ -78,6 +92,24 @@ Validate configuration:
78
92
  bundle exec ./exe/mammoth validate config/mammoth.example.yml
79
93
  ```
80
94
 
95
+ Fanout destinations can be routed and tuned independently:
96
+
97
+ ```yaml
98
+ destinations:
99
+ - name: audit_webhook
100
+ type: webhook
101
+ enabled: true
102
+ url: https://audit.example.com/cdc
103
+ timeout_seconds: 5
104
+ route:
105
+ schemas: [public]
106
+ tables: [orders]
107
+ operations: [insert, update]
108
+ retry:
109
+ max_attempts: 3
110
+ schedule_seconds: [1, 10]
111
+ ```
112
+
81
113
  ## CLI
82
114
 
83
115
  ```bash
@@ -34,6 +34,19 @@ mammoth:
34
34
  # production_mammoth
35
35
  name: local_mammoth
36
36
 
37
+ node:
38
+ # Optional local node identity for status output and future control-plane
39
+ # agents. Omit this section to default node_id to the host name and node_name
40
+ # to mammoth.name.
41
+ node_id: local-mammoth-1
42
+ node_name: local-mammoth-dev
43
+ fleet_id: local-dev
44
+ environment: development
45
+ labels:
46
+ region: local
47
+ metadata:
48
+ owner: platform
49
+
37
50
  postgres:
38
51
  # PostgreSQL host reachable from the Mammoth process or pod.
39
52
  #
@@ -275,6 +288,7 @@ webhook:
275
288
  # destinations:
276
289
  # - name: primary_webhook
277
290
  # type: webhook
291
+ # enabled: true
278
292
  # url: https://example.com/webhooks/postgres
279
293
  # timeout_seconds: 5
280
294
  # header_env:
@@ -284,8 +298,28 @@ webhook:
284
298
  # secret_env: MAMMOTH_PRIMARY_WEBHOOK_SIGNING_SECRET
285
299
  # - name: audit_webhook
286
300
  # type: webhook
301
+ # enabled: true
287
302
  # url: https://audit.example.com/cdc
288
303
  # timeout_seconds: 5
304
+ #
305
+ # # Optional route filter. Omit route to receive every event/envelope.
306
+ # # For transaction delivery, a destination matches when any event in the
307
+ # # transaction matches; Mammoth still delivers the full transaction envelope.
308
+ # route:
309
+ # schemas:
310
+ # - public
311
+ # tables:
312
+ # - orders
313
+ # operations:
314
+ # - insert
315
+ # - update
316
+ #
317
+ # # Optional per-destination retry override. Omit to use top-level retry.
318
+ # retry:
319
+ # max_attempts: 3
320
+ # schedule_seconds:
321
+ # - 1
322
+ # - 10
289
323
 
290
324
  retry:
291
325
  # Maximum delivery attempts before the event/envelope is moved to the dead
@@ -310,6 +344,10 @@ sqlite:
310
344
  # records here. In Kubernetes, back this path with a PersistentVolumeClaim.
311
345
  path: data/mammoth.db
312
346
 
347
+ operational_state:
348
+ # Mammoth OSS 0.7.0 ships the sqlite operational state adapter.
349
+ adapter: sqlite
350
+
313
351
  observability:
314
352
  # Bind host for the optional health, readiness, and metrics server.
315
353
  host: 0.0.0.0
@@ -40,6 +40,47 @@
40
40
  },
41
41
  "description": "Mammoth instance identity and operator-facing metadata."
42
42
  },
43
+ "node": {
44
+ "type": "object",
45
+ "additionalProperties": false,
46
+ "properties": {
47
+ "node_id": {
48
+ "type": "string",
49
+ "minLength": 1,
50
+ "description": "Stable node identifier used by status output and future control-plane agents."
51
+ },
52
+ "node_name": {
53
+ "type": "string",
54
+ "minLength": 1,
55
+ "description": "Human-readable node name."
56
+ },
57
+ "fleet_id": {
58
+ "type": "string",
59
+ "minLength": 1,
60
+ "description": "Optional fleet identifier for grouping Mammoth nodes."
61
+ },
62
+ "environment": {
63
+ "type": "string",
64
+ "minLength": 1,
65
+ "description": "Optional deployment environment such as development, staging, or production."
66
+ },
67
+ "labels": {
68
+ "type": "object",
69
+ "additionalProperties": {
70
+ "type": "string"
71
+ },
72
+ "description": "Operator-defined node labels."
73
+ },
74
+ "metadata": {
75
+ "type": "object",
76
+ "additionalProperties": {
77
+ "type": "string"
78
+ },
79
+ "description": "Operator-defined node metadata."
80
+ }
81
+ },
82
+ "description": "Optional local node identity. Mammoth OSS uses this for status and future extension surfaces only."
83
+ },
43
84
  "postgres": {
44
85
  "type": "object",
45
86
  "additionalProperties": false,
@@ -223,7 +264,11 @@
223
264
  "enum": [
224
265
  "webhook"
225
266
  ],
226
- "description": "Destination adapter type. Mammoth OSS 0.5.1 supports webhook destinations."
267
+ "description": "Destination adapter type. Mammoth OSS 0.7.0 supports webhook destinations."
268
+ },
269
+ "enabled": {
270
+ "type": "boolean",
271
+ "description": "When false, Mammoth skips this destination, advances local progress for the work item, and does not attempt delivery."
227
272
  },
228
273
  "url": {
229
274
  "type": "string",
@@ -281,6 +326,61 @@
281
326
  }
282
327
  },
283
328
  "description": "Optional HMAC-SHA256 request signing for this webhook destination."
329
+ },
330
+ "route": {
331
+ "type": "object",
332
+ "additionalProperties": false,
333
+ "properties": {
334
+ "schemas": {
335
+ "type": "array",
336
+ "minItems": 1,
337
+ "items": {
338
+ "type": "string",
339
+ "minLength": 1
340
+ },
341
+ "description": "PostgreSQL schemas this destination should receive. Omit to match every schema."
342
+ },
343
+ "tables": {
344
+ "type": "array",
345
+ "minItems": 1,
346
+ "items": {
347
+ "type": "string",
348
+ "minLength": 1
349
+ },
350
+ "description": "PostgreSQL tables this destination should receive. Omit to match every table."
351
+ },
352
+ "operations": {
353
+ "type": "array",
354
+ "minItems": 1,
355
+ "items": {
356
+ "type": "string",
357
+ "minLength": 1
358
+ },
359
+ "description": "CDC operations this destination should receive, such as insert, update, or delete. Omit to match every operation."
360
+ }
361
+ },
362
+ "description": "Optional destination route filter. Event delivery matches one event; transaction delivery matches when any event in the transaction matches and delivers the full envelope."
363
+ },
364
+ "retry": {
365
+ "type": "object",
366
+ "additionalProperties": false,
367
+ "properties": {
368
+ "max_attempts": {
369
+ "type": "integer",
370
+ "minimum": 1,
371
+ "description": "Destination-specific maximum delivery attempts before dead-lettering. Defaults to the top-level retry.max_attempts."
372
+ },
373
+ "schedule_seconds": {
374
+ "type": "array",
375
+ "minItems": 1,
376
+ "items": {
377
+ "type": "integer",
378
+ "minimum": 1
379
+ },
380
+ "description": "Destination-specific retry backoff schedule in seconds. Defaults to the top-level retry.schedule_seconds."
381
+ }
382
+ },
383
+ "description": "Optional retry policy override for this destination."
284
384
  }
285
385
  }
286
386
  },
@@ -326,6 +426,20 @@
326
426
  },
327
427
  "description": "SQLite operational store settings for checkpoints, attempts, and dead-letter records."
328
428
  },
429
+ "operational_state": {
430
+ "type": "object",
431
+ "additionalProperties": false,
432
+ "properties": {
433
+ "adapter": {
434
+ "type": "string",
435
+ "enum": [
436
+ "sqlite"
437
+ ],
438
+ "description": "Operational state adapter. Mammoth OSS 0.7.0 ships the sqlite adapter."
439
+ }
440
+ },
441
+ "description": "Operational state adapter selection. SQLite remains the default OSS adapter."
442
+ },
329
443
  "logging": {
330
444
  "type": "object",
331
445
  "additionalProperties": false,
@@ -10,7 +10,7 @@ module Mammoth
10
10
  # injected CDC work source rather than owning upstream CDC source-adapter
11
11
  # lifecycle decisions.
12
12
  class Application
13
- attr_reader :config, :sqlite_store, :consumer, :delivery_worker, :checkpoint_store
13
+ attr_reader :config, :state_adapter, :consumer, :delivery_worker, :checkpoint_store
14
14
 
15
15
  # @param config [Mammoth::Configuration] loaded configuration
16
16
  # @param source [#each, nil] injectable event source for tests and demos
@@ -18,12 +18,17 @@ module Mammoth
18
18
  # @param sleeper [#call] retry sleep strategy
19
19
  def initialize(config, source: nil, sink: nil, sleeper: Kernel.method(:sleep))
20
20
  @config = config
21
- @sqlite_store = SQLiteStore.connect(config.dig("sqlite", "path")).bootstrap!
22
- @checkpoint_store = CheckpointStore.new(sqlite_store)
21
+ @state_adapter = build_state_adapter
22
+ @checkpoint_store = state_adapter.checkpoint_store
23
23
  @consumer = ReplicationConsumer.new(source: source || build_source, delivery_unit: delivery_unit)
24
24
  @delivery_worker = sink ? build_delivery_worker(sink: sink, sleeper: sleeper) : build_configured_delivery_worker(sleeper:)
25
25
  end
26
26
 
27
+ # @return [Mammoth::SQLiteStore] underlying SQLite store for compatibility
28
+ def sqlite_store
29
+ state_adapter.respond_to?(:sqlite_store) ? state_adapter.sqlite_store : nil
30
+ end
31
+
27
32
  # Start the application runtime and deliver consumed CDC work.
28
33
  #
29
34
  # @return [Integer] number of processed work units
@@ -54,13 +59,7 @@ module Mammoth
54
59
  private
55
60
 
56
61
  def process_work(runtime, work)
57
- if runtime
58
- runtime.process_many([work])
59
- elsif transaction_delivery?
60
- delivery_worker.deliver_transaction(work)
61
- else
62
- delivery_worker.deliver(work)
63
- end
62
+ runtime.process_many([work])
64
63
  end
65
64
 
66
65
  def process_batch(runtime, batch)
@@ -73,9 +72,8 @@ module Mammoth
73
72
  end
74
73
 
75
74
  def build_runtime
76
- return unless runtime_adapter == "concurrent"
77
-
78
- ConcurrentDeliveryRuntime.new(
75
+ Runtimes::Registry.build(
76
+ runtime_adapter,
79
77
  processor: DeliveryProcessor.new(delivery_worker:, delivery_unit: delivery_unit),
80
78
  concurrency: runtime_concurrency,
81
79
  timeout: runtime_timeout,
@@ -87,34 +85,57 @@ module Mammoth
87
85
  Sources::Postgres.new(config, checkpoint_store: checkpoint_store)
88
86
  end
89
87
 
90
- def build_delivery_worker(sink:, sleeper:)
88
+ def build_delivery_worker(sink:, sleeper:, delivery_policy: {})
91
89
  DeliveryWorker.from_config(
92
90
  config,
93
91
  sink: sink,
94
92
  checkpoint_store: checkpoint_store,
95
- dead_letter_store: DeadLetterStore.new(sqlite_store),
96
- sleeper: sleeper
93
+ dead_letter_store: state_adapter.dead_letter_store,
94
+ sleeper: sleeper,
95
+ delivery_policy: delivery_policy
97
96
  )
98
97
  end
99
98
 
100
99
  def build_configured_delivery_worker(sleeper:)
101
- workers = destination_sinks.map { |sink| build_delivery_worker(sink:, sleeper:) }
100
+ workers = destination_specs.map do |spec|
101
+ build_delivery_worker(sink: spec.fetch(:sink), sleeper:, delivery_policy: spec.fetch(:delivery_policy))
102
+ end
102
103
  return workers.fetch(0) if workers.one?
103
104
 
104
105
  FanoutDeliveryWorker.new(workers)
105
106
  end
106
107
 
107
- def destination_sinks
108
+ def destination_specs
108
109
  destinations = config.data["destinations"]
109
- return [WebhookSink.from_config(config)] unless destinations
110
+ unless destinations
111
+ delivery_policy = {} # : Hash[String, untyped]
112
+ return [{ sink: WebhookSink.from_config(config), delivery_policy: delivery_policy }]
113
+ end
110
114
 
111
115
  destinations.map.with_index(1) do |destination, index|
112
- WebhookSink.from_destination_config(destination, label: "destinations[#{index - 1}]")
116
+ {
117
+ sink: Destinations::Registry.build(destination, label: "destinations[#{index - 1}]"),
118
+ delivery_policy: destination_delivery_policy(destination)
119
+ }
113
120
  end
114
121
  end
115
122
 
116
- def transaction_delivery?
117
- delivery_unit == :transaction
123
+ def destination_delivery_policy(destination)
124
+ retry_config = destination["retry"] || {}
125
+ {
126
+ "enabled" => destination.fetch("enabled", true),
127
+ "max_attempts" => retry_config.fetch("max_attempts", config.dig("retry", "max_attempts")),
128
+ "schedule_seconds" => retry_config.fetch("schedule_seconds", config.dig("retry", "schedule_seconds")),
129
+ "route_filter" => RouteFilter.new(destination["route"])
130
+ }
131
+ end
132
+
133
+ def build_state_adapter
134
+ OperationalState::Registry.build(operational_state_adapter, config)
135
+ end
136
+
137
+ def operational_state_adapter
138
+ config.dig("operational_state", "adapter") || "sqlite"
118
139
  end
119
140
 
120
141
  def delivery_unit
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ # Reports local Mammoth capabilities without inspecting private internals.
5
+ class Capabilities
6
+ # Built-in OSS data-plane feature capabilities.
7
+ FEATURES = %w[
8
+ checkpointing
9
+ delivery_ledger
10
+ dead_letters
11
+ replay
12
+ health
13
+ metrics
14
+ routing
15
+ fanout
16
+ ].freeze
17
+
18
+ attr_reader :config
19
+
20
+ # @param config [Mammoth::Configuration] loaded configuration
21
+ def initialize(config)
22
+ @config = config
23
+ end
24
+
25
+ # @param config [Mammoth::Configuration] loaded configuration
26
+ # @return [Hash] JSON-friendly capabilities
27
+ def self.call(config)
28
+ new(config).to_h
29
+ end
30
+
31
+ # @return [Hash] JSON-friendly capabilities
32
+ def to_h
33
+ {
34
+ operational_state: operational_state_adapter,
35
+ destinations: destination_types,
36
+ runtimes: Runtimes::Registry.names,
37
+ runtime: runtime_adapter,
38
+ features: FEATURES
39
+ }
40
+ end
41
+
42
+ private
43
+
44
+ def operational_state_adapter
45
+ config.dig("operational_state", "adapter") || "sqlite"
46
+ end
47
+
48
+ def runtime_adapter
49
+ config.dig("runtime", "adapter") || "inline"
50
+ end
51
+
52
+ def destination_types
53
+ destinations = config.data["destinations"]
54
+ return [config.dig("webhook", "type") || "webhook"] unless destinations
55
+
56
+ destinations.map { |destination| destination.fetch("type", "webhook") }.uniq.sort
57
+ end
58
+ end
59
+ end
data/lib/mammoth/cli.rb CHANGED
@@ -24,9 +24,11 @@ module Mammoth
24
24
  " mammoth status CONFIG",
25
25
  " mammoth start CONFIG",
26
26
  " mammoth deliver-sample CONFIG EVENT_JSON",
27
- " mammoth dead-letters list CONFIG [--status STATUS] [--limit N]",
27
+ " mammoth dead-letters list CONFIG [--status STATUS] [--destination NAME] " \
28
+ "[--failed-after ISO8601] [--failed-before ISO8601] [--limit N]",
28
29
  " mammoth dead-letters show CONFIG ID",
29
- " mammoth dead-letters replay CONFIG [ID ...]",
30
+ " mammoth dead-letters replay CONFIG [ID ...] [--destination NAME] [--status STATUS] " \
31
+ "[--failed-after ISO8601] [--failed-before ISO8601] [--limit N]",
30
32
  " mammoth observability CONFIG"
31
33
  ].join("\n")
32
34
 
@@ -100,8 +102,7 @@ module Mammoth
100
102
  def status
101
103
  config = load_config
102
104
  store = SQLiteStore.connect(config.dig("sqlite", "path"))
103
- Status.call(config, sqlite_store: store)
104
- 0
105
+ Commands::StatusCommand.new(config, sqlite_store: store).call
105
106
  end
106
107
 
107
108
  def start
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ # Local command objects shared by CLI and future control-plane agents.
5
+ module Commands
6
+ # Reusable local command object for status inspection.
7
+ class StatusCommand
8
+ attr_reader :config, :sqlite_store, :output
9
+
10
+ # @param config [Mammoth::Configuration] loaded configuration
11
+ # @param sqlite_store [Mammoth::SQLiteStore, nil] optional operational store
12
+ # @param output [#puts] output stream
13
+ def initialize(config, sqlite_store: nil, output: $stdout)
14
+ @config = config
15
+ @sqlite_store = sqlite_store
16
+ @output = output
17
+ end
18
+
19
+ # Execute the status command.
20
+ #
21
+ # @return [Integer] process-style status code
22
+ def call
23
+ Status.call(config, sqlite_store: sqlite_store, output: output)
24
+ 0
25
+ end
26
+ end
27
+ end
28
+ end