mammoth 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -1
- data/README.md +16 -0
- data/config/mammoth.example.yml +17 -0
- data/config/mammoth.schema.json +56 -1
- data/lib/mammoth/application.rb +58 -23
- data/lib/mammoth/capabilities.rb +59 -0
- data/lib/mammoth/cdc_source.rb +1 -1
- data/lib/mammoth/cli.rb +16 -24
- data/lib/mammoth/commands/bootstrap_command.rb +26 -0
- data/lib/mammoth/commands/dead_letters_command.rb +22 -0
- data/lib/mammoth/commands/deliver_sample_command.rb +35 -0
- data/lib/mammoth/commands/start_command.rb +28 -0
- data/lib/mammoth/commands/status_command.rb +28 -0
- data/lib/mammoth/commands/validate_command.rb +24 -0
- data/lib/mammoth/configuration.rb +66 -7
- data/lib/mammoth/dead_letter_commands.rb +17 -9
- data/lib/mammoth/dead_letter_store.rb +4 -0
- data/lib/mammoth/delivered_envelope_store.rb +3 -0
- data/lib/mammoth/destinations/adapter.rb +20 -0
- data/lib/mammoth/destinations/registry.rb +46 -0
- data/lib/mammoth/destinations/webhook_adapter.rb +29 -0
- data/lib/mammoth/lifecycle_hooks.rb +56 -0
- data/lib/mammoth/node_identity.rb +54 -0
- data/lib/mammoth/observability_server.rb +2 -0
- data/lib/mammoth/operational_state/adapter.rb +37 -0
- data/lib/mammoth/operational_state/registry.rb +46 -0
- data/lib/mammoth/operational_state/sqlite_adapter.rb +44 -0
- data/lib/mammoth/registry.rb +46 -0
- data/lib/mammoth/runtimes/adapter.rb +23 -0
- data/lib/mammoth/runtimes/concurrent_adapter.rb +31 -0
- data/lib/mammoth/runtimes/inline_adapter.rb +40 -0
- data/lib/mammoth/runtimes/registry.rb +46 -0
- data/lib/mammoth/status.rb +55 -13
- data/lib/mammoth/version.rb +1 -1
- data/lib/mammoth.rb +28 -3
- metadata +21 -1
|
@@ -5,7 +5,7 @@ require "json-schema"
|
|
|
5
5
|
require "yaml"
|
|
6
6
|
|
|
7
7
|
module Mammoth
|
|
8
|
-
# Loads and validates Mammoth
|
|
8
|
+
# Loads and validates Mammoth configuration.
|
|
9
9
|
#
|
|
10
10
|
# Configuration is intentionally schema-backed so the same contract can power
|
|
11
11
|
# editor IntelliSense, preflight validation, and runtime startup checks.
|
|
@@ -22,15 +22,27 @@ module Mammoth
|
|
|
22
22
|
# @return [Mammoth::Configuration] loaded configuration
|
|
23
23
|
# @raise [Mammoth::ConfigurationError] when the file is missing or invalid
|
|
24
24
|
def self.load(path, schema_path: DEFAULT_SCHEMA_PATH)
|
|
25
|
-
new(path
|
|
25
|
+
Providers::FileProvider.new(path).load(schema_path: schema_path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Build and validate a configuration from an in-memory Hash.
|
|
29
|
+
#
|
|
30
|
+
# @param data [Hash] configuration data
|
|
31
|
+
# @param path [String, nil] optional display path/source
|
|
32
|
+
# @param schema_path [String] JSON Schema path
|
|
33
|
+
# @return [Mammoth::Configuration] loaded configuration
|
|
34
|
+
# @raise [Mammoth::ConfigurationError] when the data is invalid
|
|
35
|
+
def self.from_hash(data, path: nil, schema_path: DEFAULT_SCHEMA_PATH)
|
|
36
|
+
Providers::HashProvider.new(data, path: path).load(schema_path: schema_path)
|
|
26
37
|
end
|
|
27
38
|
|
|
28
39
|
# @param path [String] YAML configuration path
|
|
29
40
|
# @param schema_path [String] JSON Schema path
|
|
30
|
-
|
|
41
|
+
# @param data [Hash, nil] already parsed configuration data
|
|
42
|
+
def initialize(path, schema_path: DEFAULT_SCHEMA_PATH, data: nil)
|
|
31
43
|
@path = path
|
|
32
44
|
@schema_path = schema_path
|
|
33
|
-
@data =
|
|
45
|
+
@data = data
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
# Load and validate the configuration file.
|
|
@@ -38,9 +50,7 @@ module Mammoth
|
|
|
38
50
|
# @return [Mammoth::Configuration] self
|
|
39
51
|
# @raise [Mammoth::ConfigurationError] when validation fails
|
|
40
52
|
def load
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@data = YAML.safe_load_file(path, permitted_classes: [], aliases: false)
|
|
53
|
+
@data ||= load_yaml_file
|
|
44
54
|
raise ConfigurationError, "configuration must be a YAML mapping" unless data.is_a?(Hash)
|
|
45
55
|
|
|
46
56
|
validate_schema!
|
|
@@ -60,6 +70,12 @@ module Mammoth
|
|
|
60
70
|
|
|
61
71
|
private
|
|
62
72
|
|
|
73
|
+
def load_yaml_file
|
|
74
|
+
raise ConfigurationError, "configuration file not found: #{path}" unless path && File.file?(path)
|
|
75
|
+
|
|
76
|
+
YAML.safe_load_file(path, permitted_classes: [], aliases: false)
|
|
77
|
+
end
|
|
78
|
+
|
|
63
79
|
def validate_schema!
|
|
64
80
|
raise ConfigurationError, "schema file not found: #{schema_path}" unless File.file?(schema_path)
|
|
65
81
|
|
|
@@ -85,5 +101,48 @@ module Mammoth
|
|
|
85
101
|
|
|
86
102
|
raise ConfigurationError, "destination names must be unique: #{duplicates.join(", ")}"
|
|
87
103
|
end
|
|
104
|
+
|
|
105
|
+
# Configuration provider contracts shared by CLI and future control agents.
|
|
106
|
+
module Providers
|
|
107
|
+
# Loads configuration from a YAML file.
|
|
108
|
+
class FileProvider
|
|
109
|
+
attr_reader :path
|
|
110
|
+
|
|
111
|
+
# @param path [String] YAML configuration path
|
|
112
|
+
def initialize(path)
|
|
113
|
+
@path = path
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @param schema_path [String] JSON Schema path
|
|
117
|
+
# @return [Mammoth::Configuration]
|
|
118
|
+
def load(schema_path: Configuration::DEFAULT_SCHEMA_PATH)
|
|
119
|
+
Configuration.new(path, schema_path: schema_path).load
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Loads configuration from an already parsed Hash.
|
|
124
|
+
class HashProvider
|
|
125
|
+
attr_reader :data, :path
|
|
126
|
+
|
|
127
|
+
# @param data [Hash] configuration data
|
|
128
|
+
# @param path [String, nil] optional source name for diagnostics/status
|
|
129
|
+
def initialize(data, path: nil)
|
|
130
|
+
@data = data
|
|
131
|
+
@path = path || "<hash>"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @param schema_path [String] JSON Schema path
|
|
135
|
+
# @return [Mammoth::Configuration]
|
|
136
|
+
def load(schema_path: Configuration::DEFAULT_SCHEMA_PATH)
|
|
137
|
+
Configuration.new(path, schema_path: schema_path, data: deep_copy(data)).load
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def deep_copy(value)
|
|
143
|
+
Marshal.load(Marshal.dump(value))
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
88
147
|
end
|
|
89
148
|
end
|
|
@@ -17,16 +17,18 @@ module Mammoth
|
|
|
17
17
|
:metadata
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
attr_reader :argv
|
|
20
|
+
attr_reader :argv, :lifecycle_hooks
|
|
21
21
|
|
|
22
22
|
# @param argv [Array<String>] command line arguments
|
|
23
|
-
def self.call(argv)
|
|
24
|
-
new(argv).call
|
|
23
|
+
def self.call(argv, lifecycle_hooks: LifecycleHooks.new)
|
|
24
|
+
new(argv, lifecycle_hooks: lifecycle_hooks).call
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# @param argv [Array<String>] command line arguments
|
|
28
|
-
|
|
28
|
+
# @param lifecycle_hooks [Mammoth::LifecycleHooks, Hash] local lifecycle callbacks
|
|
29
|
+
def initialize(argv, lifecycle_hooks: LifecycleHooks.new)
|
|
29
30
|
@argv = argv
|
|
31
|
+
@lifecycle_hooks = lifecycle_hooks.is_a?(LifecycleHooks) ? lifecycle_hooks : LifecycleHooks.new(lifecycle_hooks)
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
# Dispatch the nested dead-letter subcommand.
|
|
@@ -79,11 +81,13 @@ module Mammoth
|
|
|
79
81
|
rows = replay_rows
|
|
80
82
|
raise ConfigurationError, "no dead letters found to replay" if rows.empty?
|
|
81
83
|
|
|
84
|
+
lifecycle_hooks.call(:before_replay, replay_context(rows: rows))
|
|
82
85
|
rows.each do |row|
|
|
83
86
|
result = replay_row(row)
|
|
84
87
|
dead_letter_store.resolve(row.fetch("id")) if replay_resolved?(result)
|
|
85
88
|
puts replay_message(row, result)
|
|
86
89
|
end
|
|
90
|
+
lifecycle_hooks.call(:after_replay, replay_context(rows: rows))
|
|
87
91
|
0
|
|
88
92
|
end
|
|
89
93
|
|
|
@@ -144,8 +148,6 @@ module Mammoth
|
|
|
144
148
|
|
|
145
149
|
raise ConfigurationError, "unexpected argument #{args[index]}\n#{CLI::USAGE}"
|
|
146
150
|
end
|
|
147
|
-
rescue ArgumentError
|
|
148
|
-
raise ConfigurationError, "invalid dead letter list option"
|
|
149
151
|
rescue IndexError
|
|
150
152
|
raise ConfigurationError, "missing value for dead letter option"
|
|
151
153
|
end
|
|
@@ -188,13 +190,11 @@ module Mammoth
|
|
|
188
190
|
end
|
|
189
191
|
|
|
190
192
|
ids.map do |raw_id|
|
|
191
|
-
id =
|
|
193
|
+
id = raw_id
|
|
192
194
|
row = dead_letter_store.fetch(id)
|
|
193
195
|
raise ConfigurationError, "dead letter not found: #{id}" unless row
|
|
194
196
|
|
|
195
197
|
row
|
|
196
|
-
rescue ArgumentError
|
|
197
|
-
raise ConfigurationError, "dead letter id must be an integer"
|
|
198
198
|
end
|
|
199
199
|
end
|
|
200
200
|
|
|
@@ -311,6 +311,14 @@ module Mammoth
|
|
|
311
311
|
def replay_message(row, result)
|
|
312
312
|
"Dead letter #{row.fetch("id")}: #{result.fetch(:status)}"
|
|
313
313
|
end
|
|
314
|
+
|
|
315
|
+
def replay_context(extra = {})
|
|
316
|
+
{
|
|
317
|
+
config: load_config,
|
|
318
|
+
dead_letter_store: dead_letter_store,
|
|
319
|
+
delivery_worker: worker
|
|
320
|
+
}.merge(extra)
|
|
321
|
+
end
|
|
314
322
|
end
|
|
315
323
|
# rubocop:enable Metrics/ClassLength
|
|
316
324
|
end
|
|
@@ -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,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mammoth
|
|
4
|
+
# Executes optional lifecycle callbacks around local Mammoth operations.
|
|
5
|
+
#
|
|
6
|
+
# Hooks are intentionally in-process and explicit. They give extensions and
|
|
7
|
+
# future control agents stable observation points without changing the data
|
|
8
|
+
# plane or introducing remote behavior into OSS.
|
|
9
|
+
class LifecycleHooks
|
|
10
|
+
EVENTS = %i[
|
|
11
|
+
before_start
|
|
12
|
+
after_start
|
|
13
|
+
before_shutdown
|
|
14
|
+
after_shutdown
|
|
15
|
+
before_replay
|
|
16
|
+
after_replay
|
|
17
|
+
].freeze
|
|
18
|
+
EMPTY_CALLBACKS = [nil].compact.freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :callbacks
|
|
21
|
+
|
|
22
|
+
# @param callbacks [Hash] lifecycle callbacks keyed by event name
|
|
23
|
+
def initialize(callbacks = {})
|
|
24
|
+
@callbacks = normalize(callbacks || {})
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param event [Symbol, String] lifecycle event
|
|
28
|
+
# @param context [Hash] event context
|
|
29
|
+
# @return [void]
|
|
30
|
+
def call(event, context = {})
|
|
31
|
+
event = event.to_sym
|
|
32
|
+
raise ConfigurationError, "unknown lifecycle hook: #{event}" unless EVENTS.include?(event)
|
|
33
|
+
|
|
34
|
+
callback_list = callbacks.fetch(event, EMPTY_CALLBACKS)
|
|
35
|
+
# @type var callback_list: untyped
|
|
36
|
+
Array(callback_list).each do |callback|
|
|
37
|
+
# @type var callback: untyped
|
|
38
|
+
callback.call(context.merge(event: event))
|
|
39
|
+
end
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def normalize(callbacks)
|
|
46
|
+
normalized = {}
|
|
47
|
+
# @type var normalized: Hash[Symbol, untyped]
|
|
48
|
+
callbacks.each_with_object(normalized) do |(event, callback), memo|
|
|
49
|
+
key = event.to_sym
|
|
50
|
+
raise ConfigurationError, "unknown lifecycle hook: #{key}" unless EVENTS.include?(key)
|
|
51
|
+
|
|
52
|
+
memo[key] = Array(callback)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
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
|