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.
@@ -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,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ # Matches CDC events and transaction envelopes against destination route rules.
5
+ class RouteFilter
6
+ attr_reader :schemas, :tables, :operations
7
+
8
+ def initialize(config = nil)
9
+ route = config || {}
10
+ @schemas = normalized_set(route["schemas"])
11
+ @tables = normalized_set(route["tables"])
12
+ @operations = normalized_set(route["operations"])
13
+ end
14
+
15
+ def match?(work, serializer:)
16
+ match_payload?(serializer.call(work))
17
+ end
18
+
19
+ def match_payload?(payload)
20
+ if transaction_payload?(payload)
21
+ payload.fetch("events").any? { |event| event_match?(event) }
22
+ else
23
+ event_match?(payload)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def normalized_set(values)
30
+ return [] unless values
31
+
32
+ values.map { |value| value.to_s.downcase }.freeze
33
+ end
34
+
35
+ def transaction_payload?(payload)
36
+ payload.fetch("type", nil) == TransactionEnvelopeSerializer::PAYLOAD_TYPE
37
+ end
38
+
39
+ def event_match?(payload)
40
+ matches?(schemas, payload["namespace"]) &&
41
+ matches?(tables, payload["entity"]) &&
42
+ matches?(operations, payload["operation"])
43
+ end
44
+
45
+ def matches?(allowed_values, actual_value)
46
+ allowed_values.empty? || allowed_values.include?(actual_value.to_s.downcase)
47
+ end
48
+ end
49
+ 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.5.1"
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"
@@ -12,11 +16,22 @@ require_relative "mammoth/dead_letter_store"
12
16
  require_relative "mammoth/delivered_envelope_store"
13
17
  require_relative "mammoth/event_serializer"
14
18
  require_relative "mammoth/transaction_envelope_serializer"
19
+ require_relative "mammoth/route_filter"
15
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"
16
27
  require_relative "mammoth/delivery_worker"
17
28
  require_relative "mammoth/fanout_delivery_worker"
18
29
  require_relative "mammoth/delivery_processor"
19
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"
20
35
  require_relative "mammoth/sources/postgres"
21
36
  require_relative "mammoth/cdc_source"
22
37
  require_relative "mammoth/replication_consumer"
@@ -30,4 +45,8 @@ require_relative "mammoth/cli"
30
45
  # PostgreSQL change events are normalized, persisted through local operational
31
46
  # state, and delivered to webhook destinations.
32
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)
33
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.5.1
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,12 +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
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
164
179
  - lib/mammoth/sources/postgres.rb
165
180
  - lib/mammoth/sql/__bootstrap__.sql
166
181
  - lib/mammoth/sqlite_store.rb