mammoth 0.7.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 +13 -1
- data/README.md +4 -0
- data/config/mammoth.example.yml +1 -1
- data/config/mammoth.schema.json +2 -2
- data/lib/mammoth/application.rb +40 -7
- data/lib/mammoth/cdc_source.rb +1 -1
- data/lib/mammoth/cli.rb +15 -22
- 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/validate_command.rb +24 -0
- data/lib/mammoth/configuration.rb +66 -7
- data/lib/mammoth/dead_letter_commands.rb +16 -4
- data/lib/mammoth/lifecycle_hooks.rb +56 -0
- data/lib/mammoth/version.rb +1 -1
- data/lib/mammoth.rb +10 -3
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d7437cc30ed1a50ca4b414c37bc75ba374569ef3cdfafc7b88f0631d5f462790
|
|
4
|
+
data.tar.gz: d75b6cf2476c892915aa24ceb5169f69889a794cb923e5d1748a7c5a726129e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b38367dbafad260559c1f80558d2aeb6f452688fc1404b5652dbd8310625fbe0058eb2ac45ab0dea9dc226b19e0447efca79ff8a70556f3b10f24ba1ebd2e8c9
|
|
7
|
+
data.tar.gz: f7950310f899a5291889853f95b096937b51332899c8adabe7209e6bef3b5c186863cc37bc0cb331e06a51c3ff5abc15b5049995a0822f07ff51c3076b5b79b5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.7.
|
|
3
|
+
## 0.7.1 Unreleased
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added local lifecycle hooks for start, shutdown, and dead-letter replay extension points.
|
|
8
|
+
- Added file and hash configuration providers for CLI, tests, and future control-plane integration.
|
|
9
|
+
- Added reusable command objects for validate, bootstrap, start, sample delivery, and dead-letter commands.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Bumped Mammoth version to `0.7.1`.
|
|
14
|
+
|
|
15
|
+
## 0.7.0
|
|
4
16
|
|
|
5
17
|
### Added
|
|
6
18
|
|
data/README.md
CHANGED
|
@@ -59,6 +59,7 @@ https://kanutocd.github.io/mammoth/Mammoth.html
|
|
|
59
59
|
- health and metrics endpoints
|
|
60
60
|
- explicit extension registries for state, destination, and runtime adapters
|
|
61
61
|
- node identity and local capability reporting
|
|
62
|
+
- lifecycle hooks, configuration providers, and reusable local command objects
|
|
62
63
|
|
|
63
64
|
## Boundary
|
|
64
65
|
|
|
@@ -75,6 +76,9 @@ Mammoth OSS exposes small adapter registries for future extensions:
|
|
|
75
76
|
- operational state adapters
|
|
76
77
|
- destination adapters
|
|
77
78
|
- runtime adapters
|
|
79
|
+
- lifecycle hooks
|
|
80
|
+
- configuration providers
|
|
81
|
+
- local command objects
|
|
78
82
|
|
|
79
83
|
See [`docs/EXTENSIONS.md`](docs/EXTENSIONS.md).
|
|
80
84
|
|
data/config/mammoth.example.yml
CHANGED
data/config/mammoth.schema.json
CHANGED
|
@@ -264,7 +264,7 @@
|
|
|
264
264
|
"enum": [
|
|
265
265
|
"webhook"
|
|
266
266
|
],
|
|
267
|
-
"description": "Destination adapter type. Mammoth OSS 0.7.
|
|
267
|
+
"description": "Destination adapter type. Mammoth OSS 0.7.x supports webhook destinations."
|
|
268
268
|
},
|
|
269
269
|
"enabled": {
|
|
270
270
|
"type": "boolean",
|
|
@@ -435,7 +435,7 @@
|
|
|
435
435
|
"enum": [
|
|
436
436
|
"sqlite"
|
|
437
437
|
],
|
|
438
|
-
"description": "Operational state adapter. Mammoth OSS 0.7.
|
|
438
|
+
"description": "Operational state adapter. Mammoth OSS 0.7.x ships the sqlite adapter."
|
|
439
439
|
}
|
|
440
440
|
},
|
|
441
441
|
"description": "Operational state adapter selection. SQLite remains the default OSS adapter."
|
data/lib/mammoth/application.rb
CHANGED
|
@@ -10,14 +10,16 @@ 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, :state_adapter, :consumer, :delivery_worker, :checkpoint_store
|
|
13
|
+
attr_reader :config, :state_adapter, :consumer, :delivery_worker, :checkpoint_store, :lifecycle_hooks
|
|
14
14
|
|
|
15
15
|
# @param config [Mammoth::Configuration] loaded configuration
|
|
16
16
|
# @param source [#each, nil] injectable event source for tests and demos
|
|
17
17
|
# @param sink [#deliver, nil] optional destination sink
|
|
18
18
|
# @param sleeper [#call] retry sleep strategy
|
|
19
|
-
|
|
19
|
+
# @param lifecycle_hooks [Mammoth::LifecycleHooks, Hash] local lifecycle callbacks
|
|
20
|
+
def initialize(config, source: nil, sink: nil, sleeper: Kernel.method(:sleep), lifecycle_hooks: LifecycleHooks.new)
|
|
20
21
|
@config = config
|
|
22
|
+
@lifecycle_hooks = build_lifecycle_hooks(lifecycle_hooks)
|
|
21
23
|
@state_adapter = build_state_adapter
|
|
22
24
|
@checkpoint_store = state_adapter.checkpoint_store
|
|
23
25
|
@consumer = ReplicationConsumer.new(source: source || build_source, delivery_unit: delivery_unit)
|
|
@@ -34,6 +36,36 @@ module Mammoth
|
|
|
34
36
|
# @return [Integer] number of processed work units
|
|
35
37
|
def start
|
|
36
38
|
runtime = build_runtime
|
|
39
|
+
processed = nil
|
|
40
|
+
|
|
41
|
+
lifecycle_hooks.call(:before_start, application_context(runtime: runtime))
|
|
42
|
+
processed = process_consumer(runtime)
|
|
43
|
+
lifecycle_hooks.call(:after_start, application_context(runtime: runtime, processed: processed))
|
|
44
|
+
processed
|
|
45
|
+
ensure
|
|
46
|
+
lifecycle_hooks.call(:before_shutdown, application_context(runtime: runtime, processed: processed))
|
|
47
|
+
runtime.shutdown if runtime.respond_to?(:shutdown)
|
|
48
|
+
lifecycle_hooks.call(:after_shutdown, application_context(runtime: runtime, processed: processed))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def build_lifecycle_hooks(hooks)
|
|
54
|
+
return hooks if hooks.is_a?(LifecycleHooks)
|
|
55
|
+
|
|
56
|
+
LifecycleHooks.new(hooks)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def application_context(extra = {})
|
|
60
|
+
{
|
|
61
|
+
config: config,
|
|
62
|
+
state_adapter: state_adapter,
|
|
63
|
+
checkpoint_store: checkpoint_store,
|
|
64
|
+
delivery_worker: delivery_worker
|
|
65
|
+
}.merge(extra)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def process_consumer(runtime)
|
|
37
69
|
processed = 0
|
|
38
70
|
batch = [nil].compact
|
|
39
71
|
|
|
@@ -50,13 +82,14 @@ module Mammoth
|
|
|
50
82
|
end
|
|
51
83
|
end
|
|
52
84
|
|
|
53
|
-
processed
|
|
54
|
-
processed
|
|
55
|
-
ensure
|
|
56
|
-
runtime.shutdown if runtime.respond_to?(:shutdown)
|
|
85
|
+
processed + flush_batch(runtime, batch)
|
|
57
86
|
end
|
|
58
87
|
|
|
59
|
-
|
|
88
|
+
def flush_batch(runtime, batch)
|
|
89
|
+
return 0 unless runtime_batching?(runtime) && batch.any?
|
|
90
|
+
|
|
91
|
+
process_batch(runtime, batch)
|
|
92
|
+
end
|
|
60
93
|
|
|
61
94
|
def process_work(runtime, work)
|
|
62
95
|
runtime.process_many([work])
|
data/lib/mammoth/cdc_source.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Mammoth
|
|
4
4
|
# Backwards-compatible internal alias for the PostgreSQL CDC source.
|
|
5
5
|
#
|
|
6
|
-
# New code should use {Mammoth::Sources::Postgres}. Mammoth
|
|
6
|
+
# New code should use {Mammoth::Sources::Postgres}. Mammoth keeps this
|
|
7
7
|
# constant so older tests or examples that referenced the transitional
|
|
8
8
|
# CdcSource name continue to work while the product-facing source name moves
|
|
9
9
|
# to PostgreSQL.
|
data/lib/mammoth/cli.rb
CHANGED
|
@@ -85,18 +85,12 @@ module Mammoth
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def validate
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
0
|
|
88
|
+
require_config_path!
|
|
89
|
+
Commands::ValidateCommand.new(config_provider).call
|
|
91
90
|
end
|
|
92
91
|
|
|
93
92
|
def bootstrap
|
|
94
|
-
|
|
95
|
-
store = SQLiteStore.connect(config.dig("sqlite", "path")).bootstrap!
|
|
96
|
-
puts "SQLite database initialized"
|
|
97
|
-
puts "Path: #{store.path}"
|
|
98
|
-
puts "Tables: #{store.tables.join(", ")}"
|
|
99
|
-
0
|
|
93
|
+
Commands::BootstrapCommand.new(load_config).call
|
|
100
94
|
end
|
|
101
95
|
|
|
102
96
|
def status
|
|
@@ -106,31 +100,22 @@ module Mammoth
|
|
|
106
100
|
end
|
|
107
101
|
|
|
108
102
|
def start
|
|
109
|
-
|
|
110
|
-
processed = Application.new(config).start
|
|
111
|
-
puts "Processed events: #{processed}"
|
|
112
|
-
0
|
|
103
|
+
Commands::StartCommand.new(load_config).call
|
|
113
104
|
end
|
|
114
105
|
|
|
115
106
|
def deliver_sample
|
|
116
107
|
config = load_config
|
|
117
108
|
event_path = argv.fetch(2, nil)
|
|
118
109
|
raise ConfigurationError, "event JSON path required\n#{USAGE}" unless event_path
|
|
119
|
-
raise ConfigurationError, "event JSON file not found: #{event_path}" unless File.file?(event_path)
|
|
120
110
|
|
|
121
|
-
|
|
122
|
-
processed = Application.new(config, source: [event]).start
|
|
123
|
-
puts "Processed sample events: #{processed}"
|
|
124
|
-
0
|
|
125
|
-
rescue JSON::ParserError => e
|
|
126
|
-
raise ConfigurationError, "invalid event JSON in #{event_path}: #{e.message}"
|
|
111
|
+
Commands::DeliverSampleCommand.new(config, event_path: event_path).call
|
|
127
112
|
end
|
|
128
113
|
|
|
129
114
|
# Dispatch the nested dead-letter command group.
|
|
130
115
|
#
|
|
131
116
|
# @return [Integer] process status code
|
|
132
117
|
def dead_letters
|
|
133
|
-
|
|
118
|
+
Commands::DeadLettersCommand.new(argv).call
|
|
134
119
|
end
|
|
135
120
|
|
|
136
121
|
def observability
|
|
@@ -142,9 +127,17 @@ module Mammoth
|
|
|
142
127
|
end
|
|
143
128
|
|
|
144
129
|
def load_config
|
|
130
|
+
require_config_path!
|
|
131
|
+
|
|
132
|
+
config_provider.load
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def require_config_path!
|
|
145
136
|
raise ConfigurationError, "configuration path required\n#{USAGE}" unless config_path
|
|
137
|
+
end
|
|
146
138
|
|
|
147
|
-
|
|
139
|
+
def config_provider
|
|
140
|
+
Configuration::Providers::FileProvider.new(config_path)
|
|
148
141
|
end
|
|
149
142
|
end
|
|
150
143
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mammoth
|
|
4
|
+
module Commands
|
|
5
|
+
# Initializes the configured operational SQLite database.
|
|
6
|
+
class BootstrapCommand
|
|
7
|
+
attr_reader :config, :output
|
|
8
|
+
|
|
9
|
+
# @param config [Mammoth::Configuration] loaded configuration
|
|
10
|
+
# @param output [#puts] output stream
|
|
11
|
+
def initialize(config, output: $stdout)
|
|
12
|
+
@config = config
|
|
13
|
+
@output = output
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Integer] process-style status code
|
|
17
|
+
def call
|
|
18
|
+
store = SQLiteStore.connect(config.dig("sqlite", "path")).bootstrap!
|
|
19
|
+
output.puts "SQLite database initialized"
|
|
20
|
+
output.puts "Path: #{store.path}"
|
|
21
|
+
output.puts "Tables: #{store.tables.join(", ")}"
|
|
22
|
+
0
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mammoth
|
|
4
|
+
module Commands
|
|
5
|
+
# Reusable local command object for dead-letter inspection and replay.
|
|
6
|
+
class DeadLettersCommand
|
|
7
|
+
attr_reader :argv, :lifecycle_hooks
|
|
8
|
+
|
|
9
|
+
# @param argv [Array<String>] dead-letters CLI-style arguments
|
|
10
|
+
# @param lifecycle_hooks [Mammoth::LifecycleHooks, Hash] local lifecycle callbacks
|
|
11
|
+
def initialize(argv, lifecycle_hooks: LifecycleHooks.new)
|
|
12
|
+
@argv = argv
|
|
13
|
+
@lifecycle_hooks = lifecycle_hooks
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Integer] process-style status code
|
|
17
|
+
def call
|
|
18
|
+
DeadLetterCommands.call(argv, lifecycle_hooks: lifecycle_hooks)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Mammoth
|
|
6
|
+
module Commands
|
|
7
|
+
# Delivers one JSON event file through the configured local application.
|
|
8
|
+
class DeliverSampleCommand
|
|
9
|
+
attr_reader :config, :event_path, :output, :lifecycle_hooks
|
|
10
|
+
|
|
11
|
+
# @param config [Mammoth::Configuration] loaded configuration
|
|
12
|
+
# @param event_path [String] event JSON path
|
|
13
|
+
# @param output [#puts] output stream
|
|
14
|
+
# @param lifecycle_hooks [Mammoth::LifecycleHooks, Hash] local lifecycle callbacks
|
|
15
|
+
def initialize(config, event_path:, output: $stdout, lifecycle_hooks: LifecycleHooks.new)
|
|
16
|
+
@config = config
|
|
17
|
+
@event_path = event_path
|
|
18
|
+
@output = output
|
|
19
|
+
@lifecycle_hooks = lifecycle_hooks
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Integer] process-style status code
|
|
23
|
+
def call
|
|
24
|
+
raise ConfigurationError, "event JSON file not found: #{event_path}" unless File.file?(event_path)
|
|
25
|
+
|
|
26
|
+
event = JSON.parse(File.read(event_path))
|
|
27
|
+
processed = Application.new(config, source: [event], lifecycle_hooks: lifecycle_hooks).start
|
|
28
|
+
output.puts "Processed sample events: #{processed}"
|
|
29
|
+
0
|
|
30
|
+
rescue JSON::ParserError => e
|
|
31
|
+
raise ConfigurationError, "invalid event JSON in #{event_path}: #{e.message}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mammoth
|
|
4
|
+
module Commands
|
|
5
|
+
# Starts a Mammoth application and prints the processed work count.
|
|
6
|
+
class StartCommand
|
|
7
|
+
attr_reader :config, :source, :output, :lifecycle_hooks
|
|
8
|
+
|
|
9
|
+
# @param config [Mammoth::Configuration] loaded configuration
|
|
10
|
+
# @param source [#each, nil] optional injected source
|
|
11
|
+
# @param output [#puts] output stream
|
|
12
|
+
# @param lifecycle_hooks [Mammoth::LifecycleHooks, Hash] local lifecycle callbacks
|
|
13
|
+
def initialize(config, source: nil, output: $stdout, lifecycle_hooks: LifecycleHooks.new)
|
|
14
|
+
@config = config
|
|
15
|
+
@source = source
|
|
16
|
+
@output = output
|
|
17
|
+
@lifecycle_hooks = lifecycle_hooks
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Integer] process-style status code
|
|
21
|
+
def call
|
|
22
|
+
processed = Application.new(config, source: source, lifecycle_hooks: lifecycle_hooks).start
|
|
23
|
+
output.puts "Processed events: #{processed}"
|
|
24
|
+
0
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mammoth
|
|
4
|
+
module Commands
|
|
5
|
+
# Validates a Mammoth configuration through a provider.
|
|
6
|
+
class ValidateCommand
|
|
7
|
+
attr_reader :provider, :output
|
|
8
|
+
|
|
9
|
+
# @param provider [#load] configuration provider
|
|
10
|
+
# @param output [#puts] output stream
|
|
11
|
+
def initialize(provider, output: $stdout)
|
|
12
|
+
@provider = provider
|
|
13
|
+
@output = output
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Integer] process-style status code
|
|
17
|
+
def call
|
|
18
|
+
config = provider.load
|
|
19
|
+
output.puts "Configuration OK: #{config.path}"
|
|
20
|
+
0
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -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
|
|
|
@@ -307,6 +311,14 @@ module Mammoth
|
|
|
307
311
|
def replay_message(row, result)
|
|
308
312
|
"Dead letter #{row.fetch("id")}: #{result.fetch(:status)}"
|
|
309
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
|
|
310
322
|
end
|
|
311
323
|
# rubocop:enable Metrics/ClassLength
|
|
312
324
|
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
|
data/lib/mammoth/version.rb
CHANGED
data/lib/mammoth.rb
CHANGED
|
@@ -4,10 +4,16 @@ require_relative "mammoth/version"
|
|
|
4
4
|
require_relative "mammoth/errors"
|
|
5
5
|
require_relative "mammoth/registry"
|
|
6
6
|
require_relative "mammoth/configuration"
|
|
7
|
+
require_relative "mammoth/lifecycle_hooks"
|
|
7
8
|
require_relative "mammoth/node_identity"
|
|
8
9
|
require_relative "mammoth/capabilities"
|
|
9
10
|
require_relative "mammoth/status"
|
|
11
|
+
require_relative "mammoth/commands/validate_command"
|
|
12
|
+
require_relative "mammoth/commands/bootstrap_command"
|
|
10
13
|
require_relative "mammoth/commands/status_command"
|
|
14
|
+
require_relative "mammoth/commands/start_command"
|
|
15
|
+
require_relative "mammoth/commands/deliver_sample_command"
|
|
16
|
+
require_relative "mammoth/commands/dead_letters_command"
|
|
11
17
|
require_relative "mammoth/observability_snapshot"
|
|
12
18
|
require_relative "mammoth/observability_server"
|
|
13
19
|
require_relative "mammoth/sqlite_store"
|
|
@@ -41,9 +47,10 @@ require_relative "mammoth/cli"
|
|
|
41
47
|
|
|
42
48
|
# Mammoth is a self-hosted PostgreSQL event relay.
|
|
43
49
|
#
|
|
44
|
-
# Mammoth
|
|
45
|
-
#
|
|
46
|
-
# state,
|
|
50
|
+
# Mammoth 0.7.x is a single-node PostgreSQL CDC relay with webhook fanout,
|
|
51
|
+
# replayable operational SQLite state, health/metrics endpoints, and explicit
|
|
52
|
+
# extension contracts for state, destination, runtime, lifecycle, configuration,
|
|
53
|
+
# and local command integrations.
|
|
47
54
|
module Mammoth
|
|
48
55
|
OperationalState::Registry.register("sqlite", OperationalState::SQLiteAdapter)
|
|
49
56
|
Destinations::Registry.register("webhook", Destinations::WebhookAdapter)
|
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.7.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
@@ -149,7 +149,12 @@ files:
|
|
|
149
149
|
- lib/mammoth/cdc_source.rb
|
|
150
150
|
- lib/mammoth/checkpoint_store.rb
|
|
151
151
|
- lib/mammoth/cli.rb
|
|
152
|
+
- lib/mammoth/commands/bootstrap_command.rb
|
|
153
|
+
- lib/mammoth/commands/dead_letters_command.rb
|
|
154
|
+
- lib/mammoth/commands/deliver_sample_command.rb
|
|
155
|
+
- lib/mammoth/commands/start_command.rb
|
|
152
156
|
- lib/mammoth/commands/status_command.rb
|
|
157
|
+
- lib/mammoth/commands/validate_command.rb
|
|
153
158
|
- lib/mammoth/concurrent_delivery_runtime.rb
|
|
154
159
|
- lib/mammoth/configuration.rb
|
|
155
160
|
- lib/mammoth/dead_letter_commands.rb
|
|
@@ -163,6 +168,7 @@ files:
|
|
|
163
168
|
- lib/mammoth/errors.rb
|
|
164
169
|
- lib/mammoth/event_serializer.rb
|
|
165
170
|
- lib/mammoth/fanout_delivery_worker.rb
|
|
171
|
+
- lib/mammoth/lifecycle_hooks.rb
|
|
166
172
|
- lib/mammoth/node_identity.rb
|
|
167
173
|
- lib/mammoth/observability_server.rb
|
|
168
174
|
- lib/mammoth/observability_snapshot.rb
|