mammoth 0.1.1 → 0.3.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 +4 -4
- data/CHANGELOG.md +62 -0
- data/README.md +46 -5
- data/config/mammoth.example.yml +249 -0
- data/config/mammoth.schema.json +186 -40
- data/lib/mammoth/application.rb +87 -8
- data/lib/mammoth/cli.rb +23 -2
- data/lib/mammoth/concurrent_delivery_runtime.rb +52 -0
- data/lib/mammoth/dead_letter_commands.rb +217 -0
- data/lib/mammoth/dead_letter_store.rb +28 -7
- data/lib/mammoth/delivered_envelope_store.rb +102 -0
- data/lib/mammoth/delivery_processor.rb +72 -0
- data/lib/mammoth/delivery_worker.rb +74 -12
- data/lib/mammoth/replication_consumer.rb +39 -4
- data/lib/mammoth/sources/postgres.rb +189 -9
- data/lib/mammoth/sql/__bootstrap__.sql +24 -0
- data/lib/mammoth/transaction_envelope_serializer.rb +91 -0
- data/lib/mammoth/version.rb +1 -1
- data/lib/mammoth/webhook_sink.rb +94 -16
- data/lib/mammoth.rb +5 -0
- metadata +20 -1
data/config/mammoth.schema.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
3
3
|
"id": "https://kanutocd.github.io/mammoth/schema/mammoth.schema.json",
|
|
4
|
-
|
|
4
|
+
"title": "Mammoth Configuration",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"additionalProperties": false,
|
|
7
|
-
|
|
8
7
|
"required": [
|
|
9
8
|
"mammoth",
|
|
10
9
|
"postgres",
|
|
@@ -14,20 +13,22 @@
|
|
|
14
13
|
"sqlite",
|
|
15
14
|
"logging"
|
|
16
15
|
],
|
|
17
|
-
|
|
18
16
|
"properties": {
|
|
19
17
|
"mammoth": {
|
|
20
18
|
"type": "object",
|
|
21
19
|
"additionalProperties": false,
|
|
22
|
-
"required": [
|
|
20
|
+
"required": [
|
|
21
|
+
"name"
|
|
22
|
+
],
|
|
23
23
|
"properties": {
|
|
24
24
|
"name": {
|
|
25
25
|
"type": "string",
|
|
26
|
-
"minLength": 1
|
|
26
|
+
"minLength": 1,
|
|
27
|
+
"description": "Human-readable Mammoth instance name used in logs, status output, metrics, and future dashboards. Keep stable per environment."
|
|
27
28
|
}
|
|
28
|
-
}
|
|
29
|
+
},
|
|
30
|
+
"description": "Mammoth instance identity and operator-facing metadata."
|
|
29
31
|
},
|
|
30
|
-
|
|
31
32
|
"postgres": {
|
|
32
33
|
"type": "object",
|
|
33
34
|
"additionalProperties": false,
|
|
@@ -41,28 +42,33 @@
|
|
|
41
42
|
"properties": {
|
|
42
43
|
"host": {
|
|
43
44
|
"type": "string",
|
|
44
|
-
"minLength": 1
|
|
45
|
+
"minLength": 1,
|
|
46
|
+
"description": "PostgreSQL host reachable from the Mammoth process or pod."
|
|
45
47
|
},
|
|
46
48
|
"port": {
|
|
47
49
|
"type": "integer",
|
|
48
50
|
"minimum": 1,
|
|
49
|
-
"maximum": 65535
|
|
51
|
+
"maximum": 65535,
|
|
52
|
+
"description": "PostgreSQL TCP port."
|
|
50
53
|
},
|
|
51
54
|
"database": {
|
|
52
55
|
"type": "string",
|
|
53
|
-
"minLength": 1
|
|
56
|
+
"minLength": 1,
|
|
57
|
+
"description": "Source PostgreSQL database name."
|
|
54
58
|
},
|
|
55
59
|
"username": {
|
|
56
60
|
"type": "string",
|
|
57
|
-
"minLength": 1
|
|
61
|
+
"minLength": 1,
|
|
62
|
+
"description": "PostgreSQL user used by Mammoth. This user must be able to connect in replication mode and read publication metadata."
|
|
58
63
|
},
|
|
59
64
|
"password_env": {
|
|
60
65
|
"type": "string",
|
|
61
|
-
"minLength": 1
|
|
66
|
+
"minLength": 1,
|
|
67
|
+
"description": "Name of the environment variable containing the PostgreSQL password. The password itself should not be stored in YAML."
|
|
62
68
|
}
|
|
63
|
-
}
|
|
69
|
+
},
|
|
70
|
+
"description": "PostgreSQL connection settings for the source database."
|
|
64
71
|
},
|
|
65
|
-
|
|
66
72
|
"replication": {
|
|
67
73
|
"type": "object",
|
|
68
74
|
"additionalProperties": false,
|
|
@@ -73,7 +79,8 @@
|
|
|
73
79
|
"properties": {
|
|
74
80
|
"slot": {
|
|
75
81
|
"type": "string",
|
|
76
|
-
"minLength": 1
|
|
82
|
+
"minLength": 1,
|
|
83
|
+
"description": "PostgreSQL logical replication slot name. Permanent slots preserve WAL until Mammoth acknowledges progress. Changing this value creates a separate replication position."
|
|
77
84
|
},
|
|
78
85
|
"publications": {
|
|
79
86
|
"type": "array",
|
|
@@ -81,25 +88,33 @@
|
|
|
81
88
|
"items": {
|
|
82
89
|
"type": "string",
|
|
83
90
|
"minLength": 1
|
|
84
|
-
}
|
|
91
|
+
},
|
|
92
|
+
"description": "PostgreSQL publications to subscribe to. Publications define which table changes PostgreSQL sends to Mammoth."
|
|
85
93
|
},
|
|
86
94
|
"start_lsn": {
|
|
87
|
-
"type": [
|
|
95
|
+
"type": [
|
|
96
|
+
"string",
|
|
97
|
+
"null"
|
|
98
|
+
],
|
|
99
|
+
"description": "Optional Log Sequence Number for advanced recovery or controlled replay. Leave null or unset for normal checkpoint-based operation."
|
|
88
100
|
},
|
|
89
101
|
"auto_create_slot": {
|
|
90
|
-
"type": "boolean"
|
|
102
|
+
"type": "boolean",
|
|
103
|
+
"description": "When true, Mammoth creates the replication slot if it does not exist. Convenient for development; DBA-managed slots are recommended for production."
|
|
91
104
|
},
|
|
92
105
|
"temporary_slot": {
|
|
93
|
-
"type": "boolean"
|
|
106
|
+
"type": "boolean",
|
|
107
|
+
"description": "When true, use a temporary slot that disappears when the replication session ends. Durable deployments should use false."
|
|
94
108
|
},
|
|
95
109
|
"feedback_interval": {
|
|
96
110
|
"type": "number",
|
|
97
111
|
"minimum": 0,
|
|
98
|
-
"exclusiveMinimum": true
|
|
112
|
+
"exclusiveMinimum": true,
|
|
113
|
+
"description": "Standby feedback interval in seconds. Lower values improve liveness/progress visibility; higher values reduce chatter."
|
|
99
114
|
}
|
|
100
|
-
}
|
|
115
|
+
},
|
|
116
|
+
"description": "Logical replication settings. Mammoth uses one replication slot and one WAL stream; downstream runtime concurrency does not create additional replication connections."
|
|
101
117
|
},
|
|
102
|
-
|
|
103
118
|
"webhook": {
|
|
104
119
|
"type": "object",
|
|
105
120
|
"additionalProperties": false,
|
|
@@ -111,19 +126,69 @@
|
|
|
111
126
|
"properties": {
|
|
112
127
|
"name": {
|
|
113
128
|
"type": "string",
|
|
114
|
-
"minLength": 1
|
|
129
|
+
"minLength": 1,
|
|
130
|
+
"description": "Destination name used in logs, dead-letter records, and future metrics."
|
|
115
131
|
},
|
|
116
132
|
"url": {
|
|
117
133
|
"type": "string",
|
|
118
|
-
"format": "uri"
|
|
134
|
+
"format": "uri",
|
|
135
|
+
"description": "Destination URL for delivered CDC payloads. Transaction delivery sends a TransactionEnvelope payload; event delivery sends a single event payload."
|
|
119
136
|
},
|
|
120
137
|
"timeout_seconds": {
|
|
121
138
|
"type": "integer",
|
|
122
|
-
"minimum": 1
|
|
139
|
+
"minimum": 1,
|
|
140
|
+
"description": "HTTP timeout in seconds for webhook delivery attempts."
|
|
141
|
+
},
|
|
142
|
+
"headers": {
|
|
143
|
+
"type": "object",
|
|
144
|
+
"additionalProperties": {
|
|
145
|
+
"type": "string"
|
|
146
|
+
},
|
|
147
|
+
"description": "Static HTTP headers added to every webhook request. Do not put secrets here; use header_env for secret-backed values."
|
|
148
|
+
},
|
|
149
|
+
"header_env": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"additionalProperties": {
|
|
152
|
+
"type": "string",
|
|
153
|
+
"minLength": 1
|
|
154
|
+
},
|
|
155
|
+
"description": "HTTP headers whose values are read from environment variables. Keys are header names; values are environment variable names."
|
|
156
|
+
},
|
|
157
|
+
"signing": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"additionalProperties": false,
|
|
160
|
+
"required": [
|
|
161
|
+
"secret_env"
|
|
162
|
+
],
|
|
163
|
+
"properties": {
|
|
164
|
+
"algorithm": {
|
|
165
|
+
"type": "string",
|
|
166
|
+
"enum": [
|
|
167
|
+
"hmac_sha256"
|
|
168
|
+
],
|
|
169
|
+
"description": "Webhook signing algorithm. Mammoth signs '<timestamp>.<body>' using HMAC-SHA256."
|
|
170
|
+
},
|
|
171
|
+
"secret_env": {
|
|
172
|
+
"type": "string",
|
|
173
|
+
"minLength": 1,
|
|
174
|
+
"description": "Environment variable containing the webhook signing secret."
|
|
175
|
+
},
|
|
176
|
+
"signature_header": {
|
|
177
|
+
"type": "string",
|
|
178
|
+
"minLength": 1,
|
|
179
|
+
"description": "Header name used for the generated signature. Defaults to X-Mammoth-Signature."
|
|
180
|
+
},
|
|
181
|
+
"timestamp_header": {
|
|
182
|
+
"type": "string",
|
|
183
|
+
"minLength": 1,
|
|
184
|
+
"description": "Header name used for the signing timestamp. Defaults to X-Mammoth-Timestamp."
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
"description": "Optional HMAC-SHA256 request signing for webhook receivers."
|
|
123
188
|
}
|
|
124
|
-
}
|
|
189
|
+
},
|
|
190
|
+
"description": "HTTP webhook destination settings."
|
|
125
191
|
},
|
|
126
|
-
|
|
127
192
|
"retry": {
|
|
128
193
|
"type": "object",
|
|
129
194
|
"additionalProperties": false,
|
|
@@ -134,7 +199,8 @@
|
|
|
134
199
|
"properties": {
|
|
135
200
|
"max_attempts": {
|
|
136
201
|
"type": "integer",
|
|
137
|
-
"minimum": 1
|
|
202
|
+
"minimum": 1,
|
|
203
|
+
"description": "Maximum delivery attempts before moving the event or envelope to the dead-letter store."
|
|
138
204
|
},
|
|
139
205
|
"schedule_seconds": {
|
|
140
206
|
"type": "array",
|
|
@@ -142,27 +208,33 @@
|
|
|
142
208
|
"items": {
|
|
143
209
|
"type": "integer",
|
|
144
210
|
"minimum": 1
|
|
145
|
-
}
|
|
211
|
+
},
|
|
212
|
+
"description": "Progressive retry delay schedule in seconds. If attempts exceed the schedule length, the final value may be reused."
|
|
146
213
|
}
|
|
147
|
-
}
|
|
214
|
+
},
|
|
215
|
+
"description": "Retry policy for failed delivery attempts before dead-lettering."
|
|
148
216
|
},
|
|
149
|
-
|
|
150
217
|
"sqlite": {
|
|
151
218
|
"type": "object",
|
|
152
219
|
"additionalProperties": false,
|
|
153
|
-
"required": [
|
|
220
|
+
"required": [
|
|
221
|
+
"path"
|
|
222
|
+
],
|
|
154
223
|
"properties": {
|
|
155
224
|
"path": {
|
|
156
225
|
"type": "string",
|
|
157
|
-
"minLength": 1
|
|
226
|
+
"minLength": 1,
|
|
227
|
+
"description": "Filesystem path to Mammoth SQLite operational database. In Kubernetes, back this path with persistent storage."
|
|
158
228
|
}
|
|
159
|
-
}
|
|
229
|
+
},
|
|
230
|
+
"description": "SQLite operational store settings for checkpoints, attempts, and dead-letter records."
|
|
160
231
|
},
|
|
161
|
-
|
|
162
232
|
"logging": {
|
|
163
233
|
"type": "object",
|
|
164
234
|
"additionalProperties": false,
|
|
165
|
-
"required": [
|
|
235
|
+
"required": [
|
|
236
|
+
"level"
|
|
237
|
+
],
|
|
166
238
|
"properties": {
|
|
167
239
|
"level": {
|
|
168
240
|
"type": "string",
|
|
@@ -171,9 +243,83 @@
|
|
|
171
243
|
"info",
|
|
172
244
|
"warn",
|
|
173
245
|
"error"
|
|
174
|
-
]
|
|
246
|
+
],
|
|
247
|
+
"description": "Log verbosity. Use debug for diagnostics, info for normal operation, and warn/error for quieter production output."
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
"description": "Logging configuration."
|
|
251
|
+
},
|
|
252
|
+
"delivery": {
|
|
253
|
+
"type": "object",
|
|
254
|
+
"additionalProperties": false,
|
|
255
|
+
"properties": {
|
|
256
|
+
"unit": {
|
|
257
|
+
"type": "string",
|
|
258
|
+
"enum": [
|
|
259
|
+
"event",
|
|
260
|
+
"transaction"
|
|
261
|
+
],
|
|
262
|
+
"description": "Delivery granularity. event delivers each change independently; transaction delivers a complete TransactionEnvelope and is the safer default for checkpoint correctness."
|
|
263
|
+
},
|
|
264
|
+
"ordering": {
|
|
265
|
+
"type": "object",
|
|
266
|
+
"additionalProperties": false,
|
|
267
|
+
"properties": {
|
|
268
|
+
"scope": {
|
|
269
|
+
"type": "string",
|
|
270
|
+
"enum": [
|
|
271
|
+
"global",
|
|
272
|
+
"transaction",
|
|
273
|
+
"relation",
|
|
274
|
+
"primary_key",
|
|
275
|
+
"none"
|
|
276
|
+
],
|
|
277
|
+
"description": "Ordering scope. global preserves complete WAL order; transaction preserves commit order; relation preserves per-table order; primary_key preserves per-row order; none is best-effort only."
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"description": "Ordering policy consumed by the delivery runtime."
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
"description": "Delivery semantics for CDC payloads."
|
|
284
|
+
},
|
|
285
|
+
"runtime": {
|
|
286
|
+
"type": "object",
|
|
287
|
+
"additionalProperties": false,
|
|
288
|
+
"properties": {
|
|
289
|
+
"adapter": {
|
|
290
|
+
"type": "string",
|
|
291
|
+
"enum": [
|
|
292
|
+
"inline",
|
|
293
|
+
"concurrent"
|
|
294
|
+
],
|
|
295
|
+
"description": "Delivery runtime adapter. inline executes synchronously; concurrent uses CDC::Concurrent::ProcessorPool for I/O-heavy delivery work."
|
|
296
|
+
},
|
|
297
|
+
"concurrency": {
|
|
298
|
+
"type": "integer",
|
|
299
|
+
"minimum": 1,
|
|
300
|
+
"description": "Number of downstream delivery workers. Does not create extra PostgreSQL replication slots or connections."
|
|
301
|
+
},
|
|
302
|
+
"batch_size": {
|
|
303
|
+
"type": "integer",
|
|
304
|
+
"minimum": 1,
|
|
305
|
+
"description": "Number of work units submitted to the concurrent runtime together. Values greater than 1 allow ordering-insensitive deliveries to overlap more visibly."
|
|
306
|
+
},
|
|
307
|
+
"preserve_order": {
|
|
308
|
+
"type": "boolean",
|
|
309
|
+
"description": "When true, preserve configured delivery ordering while using the runtime. Disable only for idempotent ordering-insensitive destinations."
|
|
310
|
+
},
|
|
311
|
+
"timeout_seconds": {
|
|
312
|
+
"type": [
|
|
313
|
+
"number",
|
|
314
|
+
"null"
|
|
315
|
+
],
|
|
316
|
+
"minimum": 0,
|
|
317
|
+
"exclusiveMinimum": true,
|
|
318
|
+
"description": "Optional runtime-level timeout in seconds. Leave null to rely on destination-specific timeouts."
|
|
175
319
|
}
|
|
176
|
-
}
|
|
320
|
+
},
|
|
321
|
+
"description": "Downstream delivery runtime settings. Runtime concurrency affects webhook/destination delivery only, not PostgreSQL replication stream count."
|
|
177
322
|
}
|
|
178
|
-
}
|
|
179
|
-
|
|
323
|
+
},
|
|
324
|
+
"description": "Mammoth relay configuration. Mammoth consumes PostgreSQL logical replication changes and delivers them to downstream destinations while preserving checkpoint, retry, and dead-letter semantics."
|
|
325
|
+
}
|
data/lib/mammoth/application.rb
CHANGED
|
@@ -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
|
|
13
|
+
attr_reader :config, :sqlite_store, :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
|
|
@@ -19,32 +19,111 @@ module Mammoth
|
|
|
19
19
|
def initialize(config, source: nil, sink: nil, sleeper: Kernel.method(:sleep))
|
|
20
20
|
@config = config
|
|
21
21
|
@sqlite_store = SQLiteStore.connect(config.dig("sqlite", "path")).bootstrap!
|
|
22
|
-
@
|
|
22
|
+
@checkpoint_store = CheckpointStore.new(sqlite_store)
|
|
23
|
+
@consumer = ReplicationConsumer.new(source: source || build_source, delivery_unit: delivery_unit)
|
|
23
24
|
@delivery_worker = build_delivery_worker(sink: sink || WebhookSink.from_config(config), sleeper: sleeper)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
# Start the application runtime and deliver consumed
|
|
27
|
+
# Start the application runtime and deliver consumed CDC work.
|
|
27
28
|
#
|
|
28
|
-
# @return [Integer] number of processed
|
|
29
|
+
# @return [Integer] number of processed work units
|
|
29
30
|
def start
|
|
31
|
+
runtime = build_runtime
|
|
30
32
|
processed = 0
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
batch = [nil].compact
|
|
34
|
+
|
|
35
|
+
consumer.start do |work|
|
|
36
|
+
if runtime_batching?(runtime)
|
|
37
|
+
batch << work
|
|
38
|
+
next unless batch.size >= runtime_batch_size
|
|
39
|
+
|
|
40
|
+
processed += process_batch(runtime, batch)
|
|
41
|
+
batch = []
|
|
42
|
+
else
|
|
43
|
+
process_work(runtime, work)
|
|
44
|
+
processed += 1
|
|
45
|
+
end
|
|
34
46
|
end
|
|
47
|
+
|
|
48
|
+
processed += process_batch(runtime, batch) if runtime_batching?(runtime) && batch.any?
|
|
35
49
|
processed
|
|
50
|
+
ensure
|
|
51
|
+
runtime.shutdown if runtime.respond_to?(:shutdown)
|
|
36
52
|
end
|
|
37
53
|
|
|
38
54
|
private
|
|
39
55
|
|
|
56
|
+
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
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def process_batch(runtime, batch)
|
|
67
|
+
runtime.process_many(batch)
|
|
68
|
+
batch.size
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def runtime_batching?(runtime)
|
|
72
|
+
runtime && runtime_batch_size > 1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_runtime
|
|
76
|
+
return unless runtime_adapter == "concurrent"
|
|
77
|
+
|
|
78
|
+
ConcurrentDeliveryRuntime.new(
|
|
79
|
+
processor: DeliveryProcessor.new(delivery_worker:, delivery_unit: delivery_unit),
|
|
80
|
+
concurrency: runtime_concurrency,
|
|
81
|
+
timeout: runtime_timeout,
|
|
82
|
+
preserve_order: runtime_preserve_order?
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_source
|
|
87
|
+
Sources::Postgres.new(config, checkpoint_store: checkpoint_store)
|
|
88
|
+
end
|
|
89
|
+
|
|
40
90
|
def build_delivery_worker(sink:, sleeper:)
|
|
41
91
|
DeliveryWorker.from_config(
|
|
42
92
|
config,
|
|
43
93
|
sink: sink,
|
|
44
|
-
checkpoint_store:
|
|
94
|
+
checkpoint_store: checkpoint_store,
|
|
45
95
|
dead_letter_store: DeadLetterStore.new(sqlite_store),
|
|
46
96
|
sleeper: sleeper
|
|
47
97
|
)
|
|
48
98
|
end
|
|
99
|
+
|
|
100
|
+
def transaction_delivery?
|
|
101
|
+
delivery_unit == :transaction
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def delivery_unit
|
|
105
|
+
(config.dig("delivery", "unit") || "event").to_sym
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def runtime_adapter
|
|
109
|
+
config.dig("runtime", "adapter") || "inline"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def runtime_concurrency
|
|
113
|
+
config.dig("runtime", "concurrency") || 1
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def runtime_timeout
|
|
117
|
+
config.dig("runtime", "timeout_seconds")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def runtime_batch_size
|
|
121
|
+
config.dig("runtime", "batch_size") || 1
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def runtime_preserve_order?
|
|
125
|
+
value = config.dig("runtime", "preserve_order")
|
|
126
|
+
value.nil? || value
|
|
127
|
+
end
|
|
49
128
|
end
|
|
50
129
|
end
|
data/lib/mammoth/cli.rb
CHANGED
|
@@ -5,6 +5,16 @@ require "json"
|
|
|
5
5
|
module Mammoth
|
|
6
6
|
# Small command dispatcher for Mammoth's operator-facing CLI.
|
|
7
7
|
class CLI
|
|
8
|
+
# Internal replay envelope used for transaction dead-letter recovery.
|
|
9
|
+
DEAD_LETTER_TRANSACTION_ENVELOPE = Data.define(
|
|
10
|
+
:events,
|
|
11
|
+
:transaction_id,
|
|
12
|
+
:source_position,
|
|
13
|
+
:commit_lsn,
|
|
14
|
+
:committed_at,
|
|
15
|
+
:metadata
|
|
16
|
+
)
|
|
17
|
+
|
|
8
18
|
# Human-readable command usage printed for invalid or incomplete invocations.
|
|
9
19
|
USAGE = [
|
|
10
20
|
"Usage:",
|
|
@@ -13,7 +23,10 @@ module Mammoth
|
|
|
13
23
|
" mammoth bootstrap CONFIG",
|
|
14
24
|
" mammoth status CONFIG",
|
|
15
25
|
" mammoth start CONFIG",
|
|
16
|
-
" mammoth deliver-sample CONFIG EVENT_JSON"
|
|
26
|
+
" mammoth deliver-sample CONFIG EVENT_JSON",
|
|
27
|
+
" mammoth dead-letters list CONFIG [--status STATUS] [--limit N]",
|
|
28
|
+
" mammoth dead-letters show CONFIG ID",
|
|
29
|
+
" mammoth dead-letters replay CONFIG [ID ...]"
|
|
17
30
|
].join("\n")
|
|
18
31
|
|
|
19
32
|
# Run the CLI.
|
|
@@ -42,6 +55,7 @@ module Mammoth
|
|
|
42
55
|
when "status" then status
|
|
43
56
|
when "start" then start
|
|
44
57
|
when "deliver-sample" then deliver_sample
|
|
58
|
+
when "dead-letters" then dead_letters
|
|
45
59
|
else
|
|
46
60
|
warn USAGE
|
|
47
61
|
1
|
|
@@ -90,7 +104,7 @@ module Mammoth
|
|
|
90
104
|
|
|
91
105
|
def start
|
|
92
106
|
config = load_config
|
|
93
|
-
processed = Application.new(config
|
|
107
|
+
processed = Application.new(config).start
|
|
94
108
|
puts "Processed events: #{processed}"
|
|
95
109
|
0
|
|
96
110
|
end
|
|
@@ -109,6 +123,13 @@ module Mammoth
|
|
|
109
123
|
raise ConfigurationError, "invalid event JSON in #{event_path}: #{e.message}"
|
|
110
124
|
end
|
|
111
125
|
|
|
126
|
+
# Dispatch the nested dead-letter command group.
|
|
127
|
+
#
|
|
128
|
+
# @return [Integer] process status code
|
|
129
|
+
def dead_letters
|
|
130
|
+
DeadLetterCommands.call(argv)
|
|
131
|
+
end
|
|
132
|
+
|
|
112
133
|
def load_config
|
|
113
134
|
raise ConfigurationError, "configuration path required\n#{USAGE}" unless config_path
|
|
114
135
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mammoth
|
|
4
|
+
# Delivery runtime backed by CDC::Concurrent::ProcessorPool.
|
|
5
|
+
#
|
|
6
|
+
# Mammoth keeps a single upstream replication stream and delegates downstream
|
|
7
|
+
# I/O fan-out to cdc-concurrent. This class is intentionally small so the
|
|
8
|
+
# runtime boundary remains easy to test and replace.
|
|
9
|
+
class ConcurrentDeliveryRuntime
|
|
10
|
+
attr_reader :processor, :concurrency, :timeout, :preserve_order, :pool
|
|
11
|
+
|
|
12
|
+
# @param processor [#process] delivery processor
|
|
13
|
+
# @param concurrency [Integer] number of concurrent delivery workers
|
|
14
|
+
# @param timeout [Numeric, nil] optional per-item timeout
|
|
15
|
+
# @param preserve_order [Boolean] preserve output order when supported
|
|
16
|
+
def initialize(processor:, concurrency:, timeout:, preserve_order:)
|
|
17
|
+
@processor = processor
|
|
18
|
+
@concurrency = concurrency
|
|
19
|
+
@timeout = timeout
|
|
20
|
+
@preserve_order = preserve_order
|
|
21
|
+
@pool = build_pool
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Process many work items through cdc-concurrent.
|
|
25
|
+
#
|
|
26
|
+
# @param items [Array<Object>] CDC work units
|
|
27
|
+
# @return [Array<Object>] processor results
|
|
28
|
+
def process_many(items)
|
|
29
|
+
return [] if items.empty?
|
|
30
|
+
|
|
31
|
+
pool.process_many(items)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Shutdown the underlying runtime when supported.
|
|
35
|
+
#
|
|
36
|
+
# @return [nil]
|
|
37
|
+
def shutdown
|
|
38
|
+
pool.shutdown if pool.respond_to?(:shutdown)
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def build_pool
|
|
45
|
+
require "cdc/concurrent"
|
|
46
|
+
CDC::Concurrent::ProcessorPool.new(processor:, concurrency:, timeout:, preserve_order:)
|
|
47
|
+
rescue LoadError => e
|
|
48
|
+
raise ConfigurationError,
|
|
49
|
+
"runtime.adapter=concurrent requires cdc-concurrent. Add `gem \"cdc-concurrent\"`. Original error: #{e.message}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|