mammoth 0.6.0 → 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: b1249f6166082904d6ecac8d74819442681eaac72ae1e23c80a2134c412e235a
4
- data.tar.gz: 292272e0696a218c3a75d970476efa3acaa71d54164da139aefa007bbd256ae5
3
+ metadata.gz: 5fe6f64a39d214c62e11a29fccb091f58cd2a1b8038a0bb803c39c4e018b09b3
4
+ data.tar.gz: f557c7288df617029808c327b656a8b7cb1f7689eb7f4b6d99032b01bcc0535e
5
5
  SHA512:
6
- metadata.gz: 4e401e3a8a1778ac6b63338636cc15d5c06ccbeea77e2179af1189ba8d60d1005a2281884e3cc3a09208ac4836f37d94132ab16c3f00b08edfb1c3347fe0e944
7
- data.tar.gz: fe8efe54a12f5dae1960eb4cf34c91b1eacd1d91a8609c082349b2a4e900ebda7f5160052b8f7d3bf062b09d16bb84438712f0f7d514ff23b4b16606da8b279a
6
+ metadata.gz: fe49156b57ff2355fd0e78368cd613fa3fa5dcf5d0a5ebf423ec799558b61a70da7529a123f5b254b904a93e7c7355a049b115308eb051e211894b84443f3444
7
+ data.tar.gz: 7b24591aa6f3996e51b5762022f680f212cbb1e8412e0e90ff42e34c3485d9f18fb2f4f4c9b9ca4420a697767400a2d37b6ebc600d203e7c4f5f0fbe5e904da2
data/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
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`.
4
18
 
5
19
  ## 0.6.0
6
20
 
data/README.md CHANGED
@@ -57,6 +57,8 @@ https://kanutocd.github.io/mammoth/Mammoth.html
57
57
  - public Helm chart support
58
58
  - unit and e2e test tasks
59
59
  - health and metrics endpoints
60
+ - explicit extension registries for state, destination, and runtime adapters
61
+ - node identity and local capability reporting
60
62
 
61
63
  ## Boundary
62
64
 
@@ -66,6 +68,16 @@ Mammoth does not own pgoutput protocol parsing, value decoding, source
66
68
  normalization, ordering policy, or runtime execution. Those belong to the
67
69
  upstream CDC Ecosystem components.
68
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
+
69
81
  ## Configuration
70
82
 
71
83
  Mammoth configuration is YAML-backed and IDE-friendly.
@@ -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
  #
@@ -331,6 +344,10 @@ sqlite:
331
344
  # records here. In Kubernetes, back this path with a PersistentVolumeClaim.
332
345
  path: data/mammoth.db
333
346
 
347
+ operational_state:
348
+ # Mammoth OSS 0.7.0 ships the sqlite operational state adapter.
349
+ adapter: sqlite
350
+
334
351
  observability:
335
352
  # Bind host for the optional health, readiness, and metrics server.
336
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,7 @@
223
264
  "enum": [
224
265
  "webhook"
225
266
  ],
226
- "description": "Destination adapter type. Mammoth OSS 0.6.0 supports webhook destinations."
267
+ "description": "Destination adapter type. Mammoth OSS 0.7.0 supports webhook destinations."
227
268
  },
228
269
  "enabled": {
229
270
  "type": "boolean",
@@ -385,6 +426,20 @@
385
426
  },
386
427
  "description": "SQLite operational store settings for checkpoints, attempts, and dead-letter records."
387
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
+ },
388
443
  "logging": {
389
444
  "type": "object",
390
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,
@@ -92,7 +90,7 @@ module Mammoth
92
90
  config,
93
91
  sink: sink,
94
92
  checkpoint_store: checkpoint_store,
95
- dead_letter_store: DeadLetterStore.new(sqlite_store),
93
+ dead_letter_store: state_adapter.dead_letter_store,
96
94
  sleeper: sleeper,
97
95
  delivery_policy: delivery_policy
98
96
  )
@@ -116,7 +114,7 @@ module Mammoth
116
114
 
117
115
  destinations.map.with_index(1) do |destination, index|
118
116
  {
119
- sink: WebhookSink.from_destination_config(destination, label: "destinations[#{index - 1}]"),
117
+ sink: Destinations::Registry.build(destination, label: "destinations[#{index - 1}]"),
120
118
  delivery_policy: destination_delivery_policy(destination)
121
119
  }
122
120
  end
@@ -132,8 +130,12 @@ module Mammoth
132
130
  }
133
131
  end
134
132
 
135
- def transaction_delivery?
136
- delivery_unit == :transaction
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"
137
139
  end
138
140
 
139
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
@@ -102,8 +102,7 @@ module Mammoth
102
102
  def status
103
103
  config = load_config
104
104
  store = SQLiteStore.connect(config.dig("sqlite", "path"))
105
- Status.call(config, sqlite_store: store)
106
- 0
105
+ Commands::StatusCommand.new(config, sqlite_store: store).call
107
106
  end
108
107
 
109
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
@@ -144,8 +144,6 @@ module Mammoth
144
144
 
145
145
  raise ConfigurationError, "unexpected argument #{args[index]}\n#{CLI::USAGE}"
146
146
  end
147
- rescue ArgumentError
148
- raise ConfigurationError, "invalid dead letter list option"
149
147
  rescue IndexError
150
148
  raise ConfigurationError, "missing value for dead letter option"
151
149
  end
@@ -188,13 +186,11 @@ module Mammoth
188
186
  end
189
187
 
190
188
  ids.map do |raw_id|
191
- id = Integer(raw_id)
189
+ id = raw_id
192
190
  row = dead_letter_store.fetch(id)
193
191
  raise ConfigurationError, "dead letter not found: #{id}" unless row
194
192
 
195
193
  row
196
- rescue ArgumentError
197
- raise ConfigurationError, "dead letter id must be an integer"
198
194
  end
199
195
  end
200
196
 
@@ -100,6 +100,10 @@ module Mammoth
100
100
  database.get_first_value("SELECT COUNT(*) FROM dead_letters#{where}", values)
101
101
  end
102
102
 
103
+ # Count dead letters grouped by destination.
104
+ #
105
+ # @param status [String, nil] optional status filter
106
+ # @return [Array<Hash>] rows with destination_name and count
103
107
  def counts_by_destination(status: nil)
104
108
  where, values = row_filters(status:)
105
109
  database.execute(
@@ -93,6 +93,9 @@ module Mammoth
93
93
  end
94
94
  end
95
95
 
96
+ # Count delivered envelopes grouped by destination.
97
+ #
98
+ # @return [Array<Hash>] rows with destination_name and count
96
99
  def counts_by_destination
97
100
  database.execute(
98
101
  "SELECT destination_name, COUNT(*) AS count FROM delivered_envelopes GROUP BY destination_name"
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ # Destination adapter contracts and registry.
5
+ module Destinations
6
+ # Base contract for destination adapters.
7
+ class Adapter
8
+ # @return [Hash] JSON-friendly adapter capabilities
9
+ def self.capabilities
10
+ { type: adapter_type }
11
+ end
12
+
13
+ # @return [String] adapter type name
14
+ def self.adapter_type
15
+ type = name.split("::").last.to_s.delete_suffix("Adapter").downcase
16
+ type.empty? ? "adapter" : type
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ module Destinations
5
+ # Registry for destination adapters.
6
+ module Registry
7
+ class << self
8
+ # Register a destination adapter.
9
+ #
10
+ # @param name [String, Symbol] adapter name
11
+ # @param adapter [Object] adapter class
12
+ # @return [Object] registered adapter
13
+ def register(name, adapter)
14
+ registry.register(name, adapter)
15
+ end
16
+
17
+ # Fetch a destination adapter.
18
+ #
19
+ # @param name [String, Symbol] adapter name
20
+ # @return [Object] adapter class
21
+ def fetch(name)
22
+ registry.fetch(name)
23
+ end
24
+
25
+ # Build a destination sink from config.
26
+ #
27
+ # @param destination [Hash] destination config
28
+ # @param label [String] config label for errors
29
+ # @return [Object] delivery sink
30
+ def build(destination, label:)
31
+ fetch(destination.fetch("type", "webhook")).build(destination, label: label)
32
+ end
33
+
34
+ # @return [Array<String>] registered destination adapter names
35
+ def names
36
+ registry.names
37
+ end
38
+
39
+ # @return [Mammoth::Registry] underlying registry
40
+ def registry
41
+ @registry ||= Mammoth::Registry.new("destination")
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ module Destinations
5
+ # Built-in webhook destination adapter.
6
+ class WebhookAdapter < Adapter
7
+ class << self
8
+ # @return [String] adapter type name
9
+ def adapter_type
10
+ "webhook"
11
+ end
12
+
13
+ # Build a webhook sink from destination configuration.
14
+ #
15
+ # @param destination [Hash] destination configuration
16
+ # @param label [String] config path label for errors
17
+ # @return [Mammoth::WebhookSink]
18
+ def build(destination, label:)
19
+ WebhookSink.from_destination_config(destination, label: label)
20
+ end
21
+
22
+ # @return [Hash] JSON-friendly capabilities
23
+ def capabilities
24
+ { type: adapter_type, delivery_units: %w[event transaction], signing: true, header_env: true }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module Mammoth
6
+ # Local Mammoth node identity used by status and future control-plane agents.
7
+ class NodeIdentity
8
+ attr_reader :node_id, :node_name, :fleet_id, :environment, :labels, :metadata
9
+
10
+ # @param node_id [String] stable node identifier
11
+ # @param node_name [String] human-readable node name
12
+ # @param fleet_id [String, nil] optional fleet identifier
13
+ # @param environment [String, nil] optional environment name
14
+ # @param labels [Hash] operator-defined labels
15
+ # @param metadata [Hash] operator-defined metadata
16
+ def initialize(node_id:, node_name:, fleet_id: nil, environment: nil, labels: {}, metadata: {})
17
+ @node_id = node_id
18
+ @node_name = node_name
19
+ @fleet_id = fleet_id
20
+ @environment = environment
21
+ @labels = labels || {}
22
+ @metadata = metadata || {}
23
+ end
24
+
25
+ # Build identity from Mammoth configuration.
26
+ #
27
+ # @param config [Mammoth::Configuration] loaded configuration
28
+ # @return [Mammoth::NodeIdentity]
29
+ def self.from_config(config)
30
+ identity = config.data["node"] || {}
31
+ hostname = Socket.gethostname
32
+ new(
33
+ node_id: identity["node_id"] || hostname,
34
+ node_name: identity["node_name"] || config.dig("mammoth", "name") || hostname,
35
+ fleet_id: identity["fleet_id"],
36
+ environment: identity["environment"],
37
+ labels: identity["labels"] || {},
38
+ metadata: identity["metadata"] || {}
39
+ )
40
+ end
41
+
42
+ # @return [Hash] JSON-friendly node identity
43
+ def to_h
44
+ {
45
+ node_id: node_id,
46
+ node_name: node_name,
47
+ fleet_id: fleet_id,
48
+ environment: environment,
49
+ labels: labels,
50
+ metadata: metadata
51
+ }
52
+ end
53
+ end
54
+ end
@@ -10,7 +10,9 @@ module Mammoth
10
10
  # may run it as a sidecar-like process or in a separate process that points at
11
11
  # the same SQLite operational database.
12
12
  class ObservabilityServer
13
+ # Default bind host for the observability server.
13
14
  DEFAULT_HOST = "0.0.0.0"
15
+ # Default TCP port for the observability server.
14
16
  DEFAULT_PORT = 9393
15
17
 
16
18
  attr_reader :config, :host, :port, :sqlite_store, :logger, :server
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ # Operational state adapter contracts and registry.
5
+ module OperationalState
6
+ # Base contract for operational state adapters.
7
+ class Adapter
8
+ # @return [void]
9
+ def initialize; end
10
+
11
+ # @return [Mammoth::CheckpointStore]
12
+ def checkpoint_store
13
+ raise NotImplementedError, "#{self.class} must implement #checkpoint_store"
14
+ end
15
+
16
+ # @return [Mammoth::DeadLetterStore]
17
+ def dead_letter_store
18
+ raise NotImplementedError, "#{self.class} must implement #dead_letter_store"
19
+ end
20
+
21
+ # @return [Mammoth::DeliveredEnvelopeStore]
22
+ def delivered_envelope_store
23
+ raise NotImplementedError, "#{self.class} must implement #delivered_envelope_store"
24
+ end
25
+
26
+ # @return [Hash] JSON-friendly state summary
27
+ def summary
28
+ {
29
+ adapter: "unknown",
30
+ checkpoints: checkpoint_store.count,
31
+ dead_letters: dead_letter_store.count,
32
+ delivered_envelopes: delivered_envelope_store.count
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ module OperationalState
5
+ # Registry for operational state adapters.
6
+ module Registry
7
+ class << self
8
+ # Register an operational state adapter.
9
+ #
10
+ # @param name [String, Symbol] adapter name
11
+ # @param adapter [Object] adapter class
12
+ # @return [Object] registered adapter
13
+ def register(name, adapter)
14
+ registry.register(name, adapter)
15
+ end
16
+
17
+ # Fetch an operational state adapter.
18
+ #
19
+ # @param name [String, Symbol] adapter name
20
+ # @return [Object] adapter class
21
+ def fetch(name)
22
+ registry.fetch(name)
23
+ end
24
+
25
+ # Build an operational state adapter from config.
26
+ #
27
+ # @param name [String, Symbol] adapter name
28
+ # @param config [Mammoth::Configuration] loaded configuration
29
+ # @return [Mammoth::OperationalState::Adapter] adapter instance
30
+ def build(name, config)
31
+ fetch(name).from_config(config)
32
+ end
33
+
34
+ # @return [Array<String>] registered operational state adapter names
35
+ def names
36
+ registry.names
37
+ end
38
+
39
+ # @return [Mammoth::Registry] underlying registry
40
+ def registry
41
+ @registry ||= Mammoth::Registry.new("operational_state")
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ module OperationalState
5
+ # SQLite-backed operational state adapter used by Mammoth OSS.
6
+ class SQLiteAdapter < Adapter
7
+ attr_reader :sqlite_store
8
+
9
+ # @param sqlite_store [Mammoth::SQLiteStore] bootstrapped SQLite store
10
+ def initialize(sqlite_store)
11
+ super()
12
+ @sqlite_store = sqlite_store.bootstrap!
13
+ end
14
+
15
+ # Build a SQLite state adapter from Mammoth configuration.
16
+ #
17
+ # @param config [Mammoth::Configuration] loaded configuration
18
+ # @return [Mammoth::OperationalState::SQLiteAdapter]
19
+ def self.from_config(config)
20
+ new(SQLiteStore.connect(config.dig("sqlite", "path")))
21
+ end
22
+
23
+ # @return [Mammoth::CheckpointStore]
24
+ def checkpoint_store
25
+ @checkpoint_store ||= CheckpointStore.new(sqlite_store)
26
+ end
27
+
28
+ # @return [Mammoth::DeadLetterStore]
29
+ def dead_letter_store
30
+ @dead_letter_store ||= DeadLetterStore.new(sqlite_store)
31
+ end
32
+
33
+ # @return [Mammoth::DeliveredEnvelopeStore]
34
+ def delivered_envelope_store
35
+ @delivered_envelope_store ||= DeliveredEnvelopeStore.new(sqlite_store)
36
+ end
37
+
38
+ # @return [Hash] JSON-friendly SQLite state summary
39
+ def summary
40
+ super.merge(adapter: "sqlite", path: sqlite_store.path, tables: sqlite_store.tables)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ # Small explicit registry for built-in and extension-provided adapters.
5
+ class Registry
6
+ attr_reader :namespace, :entries
7
+
8
+ # @param namespace [String] human-readable registry name for errors
9
+ def initialize(namespace)
10
+ @namespace = namespace
11
+ @entries = {}
12
+ end
13
+
14
+ # Register an adapter under a stable name.
15
+ #
16
+ # @param name [String, Symbol] adapter name
17
+ # @param adapter [Object] adapter object or class
18
+ # @return [Object] registered adapter
19
+ def register(name, adapter)
20
+ key = normalize_name(name)
21
+ raise ConfigurationError, "#{namespace} adapter already registered: #{key}" if entries.key?(key)
22
+
23
+ entries[key] = adapter
24
+ end
25
+
26
+ # Fetch a registered adapter.
27
+ #
28
+ # @param name [String, Symbol] adapter name
29
+ # @return [Object] registered adapter
30
+ def fetch(name)
31
+ key = normalize_name(name)
32
+ entries.fetch(key) { raise ConfigurationError, "unknown #{namespace} adapter: #{key}" }
33
+ end
34
+
35
+ # @return [Array<String>] registered adapter names
36
+ def names
37
+ entries.keys.sort
38
+ end
39
+
40
+ private
41
+
42
+ def normalize_name(name)
43
+ name.to_s
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ # Delivery runtime adapter contracts and registry.
5
+ module Runtimes
6
+ # Base contract for delivery runtime adapters.
7
+ class Adapter
8
+ # @return [void]
9
+ def initialize; end
10
+
11
+ # @return [Hash] JSON-friendly adapter capabilities
12
+ def self.capabilities
13
+ { type: adapter_type }
14
+ end
15
+
16
+ # @return [String] adapter type name
17
+ def self.adapter_type
18
+ type = name.split("::").last.to_s.delete_suffix("Adapter").downcase
19
+ type.empty? ? "adapter" : type
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ module Runtimes
5
+ # Runtime adapter backed by Mammoth's existing cdc-concurrent wrapper.
6
+ class ConcurrentAdapter < Adapter
7
+ class << self
8
+ # @return [String] adapter type name
9
+ def adapter_type
10
+ "concurrent"
11
+ end
12
+
13
+ # Build a concurrent runtime adapter.
14
+ #
15
+ # @param processor [#process] delivery processor
16
+ # @param concurrency [Integer] worker count
17
+ # @param timeout [Numeric, nil] optional timeout
18
+ # @param preserve_order [Boolean] preserve ordering when supported
19
+ # @return [Mammoth::ConcurrentDeliveryRuntime]
20
+ def build(processor:, concurrency:, timeout:, preserve_order:)
21
+ ConcurrentDeliveryRuntime.new(
22
+ processor: processor,
23
+ concurrency: concurrency,
24
+ timeout: timeout,
25
+ preserve_order: preserve_order
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ module Runtimes
5
+ # Inline runtime adapter that processes work in the caller thread.
6
+ class InlineAdapter < Adapter
7
+ attr_reader :processor
8
+
9
+ # @param processor [#process] delivery processor
10
+ def initialize(processor:)
11
+ super()
12
+ @processor = processor
13
+ end
14
+
15
+ # Build an inline runtime.
16
+ #
17
+ # @param processor [#process] delivery processor
18
+ # @return [Mammoth::Runtimes::InlineAdapter]
19
+ def self.build(processor:, **_options)
20
+ new(processor: processor)
21
+ end
22
+
23
+ # @return [String] adapter type name
24
+ def self.adapter_type
25
+ "inline"
26
+ end
27
+
28
+ # @param items [Array<Object>] work units
29
+ # @return [Array<Object>] processor results
30
+ def process_many(items)
31
+ items.map { |item| processor.process(item) }
32
+ end
33
+
34
+ # @return [nil]
35
+ def shutdown
36
+ nil
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ module Runtimes
5
+ # Registry for delivery runtime adapters.
6
+ module Registry
7
+ class << self
8
+ # Register a runtime adapter.
9
+ #
10
+ # @param name [String, Symbol] adapter name
11
+ # @param adapter [Object] adapter class
12
+ # @return [Object] registered adapter
13
+ def register(name, adapter)
14
+ registry.register(name, adapter)
15
+ end
16
+
17
+ # Fetch a runtime adapter.
18
+ #
19
+ # @param name [String, Symbol] adapter name
20
+ # @return [Object] adapter class
21
+ def fetch(name)
22
+ registry.fetch(name)
23
+ end
24
+
25
+ # Build a runtime adapter.
26
+ #
27
+ # @param name [String, Symbol] adapter name
28
+ # @param options [Hash] adapter-specific build options
29
+ # @return [Object] runtime adapter
30
+ def build(name, **options)
31
+ fetch(name).build(**options)
32
+ end
33
+
34
+ # @return [Array<String>] registered runtime adapter names
35
+ def names
36
+ registry.names
37
+ end
38
+
39
+ # @return [Mammoth::Registry] underlying registry
40
+ def registry
41
+ @registry ||= Mammoth::Registry.new("runtime")
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -3,39 +3,73 @@
3
3
  module Mammoth
4
4
  # Builds and prints a boring operational status snapshot.
5
5
  class Status
6
- attr_reader :config, :sqlite_store
6
+ attr_reader :config, :sqlite_store, :output
7
7
 
8
8
  # Print status for a configuration and optional SQLite store.
9
9
  #
10
10
  # @param config [Mammoth::Configuration] loaded configuration
11
11
  # @param sqlite_store [Mammoth::SQLiteStore, nil] operational store
12
12
  # @return [void]
13
- def self.call(config, sqlite_store: nil)
14
- new(config, sqlite_store: sqlite_store).call
13
+ def self.call(config, sqlite_store: nil, output: $stdout)
14
+ new(config, sqlite_store: sqlite_store, output: output).call
15
15
  end
16
16
 
17
17
  # @param config [Mammoth::Configuration] loaded configuration
18
18
  # @param sqlite_store [Mammoth::SQLiteStore, nil] operational store
19
- def initialize(config, sqlite_store: nil)
19
+ # @param output [#puts] output stream
20
+ def initialize(config, sqlite_store: nil, output: $stdout)
20
21
  @config = config
21
22
  @sqlite_store = sqlite_store
23
+ @output = output
22
24
  end
23
25
 
24
26
  # Print the status snapshot.
25
27
  #
26
28
  # @return [void]
27
29
  def call
28
- puts "Mammoth: #{config.dig("mammoth", "name")}"
29
- puts "Replication slot: #{config.dig("replication", "slot")}"
30
- puts "Replication publications: #{Array(config.dig("replication", "publications")).join(", ")}"
31
- puts "Runtime: not started"
32
- puts "SQLite: #{sqlite_path}"
33
- puts "Destinations: #{destination_names.join(", ")}"
30
+ status_lines.each { |line| output.puts(line) }
34
31
  print_store_state if sqlite_store
35
32
  end
36
33
 
37
34
  private
38
35
 
36
+ def status_lines
37
+ identity_lines + replication_lines + runtime_lines + destination_lines + ["SQLite: #{sqlite_path}"]
38
+ end
39
+
40
+ def identity_lines
41
+ [
42
+ "Mammoth: #{config.dig("mammoth", "name")}",
43
+ "Node ID: #{node_identity.node_id}",
44
+ "Node name: #{node_identity.node_name}",
45
+ "Fleet: #{node_identity.fleet_id || "unassigned"}",
46
+ "Environment: #{node_identity.environment || "unspecified"}"
47
+ ]
48
+ end
49
+
50
+ def replication_lines
51
+ [
52
+ "Replication slot: #{config.dig("replication", "slot")}",
53
+ "Replication publications: #{Array(config.dig("replication", "publications")).join(", ")}"
54
+ ]
55
+ end
56
+
57
+ def runtime_lines
58
+ [
59
+ "Runtime: not started",
60
+ "Runtime adapter: #{capabilities.fetch(:runtime)}",
61
+ "Operational state: #{capabilities.fetch(:operational_state)}"
62
+ ]
63
+ end
64
+
65
+ def destination_lines
66
+ [
67
+ "Destinations: #{destination_names.join(", ")}",
68
+ "Destination adapters: #{capabilities.fetch(:destinations).join(", ")}",
69
+ "Features: #{capabilities.fetch(:features).join(", ")}"
70
+ ]
71
+ end
72
+
39
73
  def sqlite_path
40
74
  config.dig("sqlite", "path")
41
75
  end
@@ -47,11 +81,19 @@ module Mammoth
47
81
  [config.dig("webhook", "name")]
48
82
  end
49
83
 
84
+ def node_identity
85
+ @node_identity ||= NodeIdentity.from_config(config)
86
+ end
87
+
88
+ def capabilities
89
+ @capabilities ||= Capabilities.call(config)
90
+ end
91
+
50
92
  def print_store_state
51
93
  store = sqlite_store.bootstrap!
52
- puts "Tables: #{store.tables.join(", ")}"
53
- puts "Checkpoints: #{CheckpointStore.new(store).count}"
54
- puts "Dead Letters: #{DeadLetterStore.new(store).count}"
94
+ output.puts "Tables: #{store.tables.join(", ")}"
95
+ output.puts "Checkpoints: #{CheckpointStore.new(store).count}"
96
+ output.puts "Dead Letters: #{DeadLetterStore.new(store).count}"
55
97
  end
56
98
  end
57
99
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Mammoth
4
4
  # Current Mammoth gem version.
5
- VERSION = "0.6.0"
5
+ VERSION = "0.7.0"
6
6
  end
data/lib/mammoth.rb CHANGED
@@ -2,8 +2,12 @@
2
2
 
3
3
  require_relative "mammoth/version"
4
4
  require_relative "mammoth/errors"
5
+ require_relative "mammoth/registry"
5
6
  require_relative "mammoth/configuration"
7
+ require_relative "mammoth/node_identity"
8
+ require_relative "mammoth/capabilities"
6
9
  require_relative "mammoth/status"
10
+ require_relative "mammoth/commands/status_command"
7
11
  require_relative "mammoth/observability_snapshot"
8
12
  require_relative "mammoth/observability_server"
9
13
  require_relative "mammoth/sqlite_store"
@@ -14,10 +18,20 @@ require_relative "mammoth/event_serializer"
14
18
  require_relative "mammoth/transaction_envelope_serializer"
15
19
  require_relative "mammoth/route_filter"
16
20
  require_relative "mammoth/webhook_sink"
21
+ require_relative "mammoth/operational_state/adapter"
22
+ require_relative "mammoth/operational_state/sqlite_adapter"
23
+ require_relative "mammoth/operational_state/registry"
24
+ require_relative "mammoth/destinations/adapter"
25
+ require_relative "mammoth/destinations/webhook_adapter"
26
+ require_relative "mammoth/destinations/registry"
17
27
  require_relative "mammoth/delivery_worker"
18
28
  require_relative "mammoth/fanout_delivery_worker"
19
29
  require_relative "mammoth/delivery_processor"
20
30
  require_relative "mammoth/concurrent_delivery_runtime"
31
+ require_relative "mammoth/runtimes/adapter"
32
+ require_relative "mammoth/runtimes/inline_adapter"
33
+ require_relative "mammoth/runtimes/concurrent_adapter"
34
+ require_relative "mammoth/runtimes/registry"
21
35
  require_relative "mammoth/sources/postgres"
22
36
  require_relative "mammoth/cdc_source"
23
37
  require_relative "mammoth/replication_consumer"
@@ -31,4 +45,8 @@ require_relative "mammoth/cli"
31
45
  # PostgreSQL change events are normalized, persisted through local operational
32
46
  # state, and delivered to webhook destinations.
33
47
  module Mammoth
48
+ OperationalState::Registry.register("sqlite", OperationalState::SQLiteAdapter)
49
+ Destinations::Registry.register("webhook", Destinations::WebhookAdapter)
50
+ Runtimes::Registry.register("inline", Runtimes::InlineAdapter)
51
+ Runtimes::Registry.register("concurrent", Runtimes::ConcurrentAdapter)
34
52
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mammoth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
@@ -145,9 +145,11 @@ files:
145
145
  - exe/mammoth
146
146
  - lib/mammoth.rb
147
147
  - lib/mammoth/application.rb
148
+ - lib/mammoth/capabilities.rb
148
149
  - lib/mammoth/cdc_source.rb
149
150
  - lib/mammoth/checkpoint_store.rb
150
151
  - lib/mammoth/cli.rb
152
+ - lib/mammoth/commands/status_command.rb
151
153
  - lib/mammoth/concurrent_delivery_runtime.rb
152
154
  - lib/mammoth/configuration.rb
153
155
  - lib/mammoth/dead_letter_commands.rb
@@ -155,13 +157,25 @@ files:
155
157
  - lib/mammoth/delivered_envelope_store.rb
156
158
  - lib/mammoth/delivery_processor.rb
157
159
  - lib/mammoth/delivery_worker.rb
160
+ - lib/mammoth/destinations/adapter.rb
161
+ - lib/mammoth/destinations/registry.rb
162
+ - lib/mammoth/destinations/webhook_adapter.rb
158
163
  - lib/mammoth/errors.rb
159
164
  - lib/mammoth/event_serializer.rb
160
165
  - lib/mammoth/fanout_delivery_worker.rb
166
+ - lib/mammoth/node_identity.rb
161
167
  - lib/mammoth/observability_server.rb
162
168
  - lib/mammoth/observability_snapshot.rb
169
+ - lib/mammoth/operational_state/adapter.rb
170
+ - lib/mammoth/operational_state/registry.rb
171
+ - lib/mammoth/operational_state/sqlite_adapter.rb
172
+ - lib/mammoth/registry.rb
163
173
  - lib/mammoth/replication_consumer.rb
164
174
  - lib/mammoth/route_filter.rb
175
+ - lib/mammoth/runtimes/adapter.rb
176
+ - lib/mammoth/runtimes/concurrent_adapter.rb
177
+ - lib/mammoth/runtimes/inline_adapter.rb
178
+ - lib/mammoth/runtimes/registry.rb
165
179
  - lib/mammoth/sources/postgres.rb
166
180
  - lib/mammoth/sql/__bootstrap__.sql
167
181
  - lib/mammoth/sqlite_store.rb