mammoth 0.4.0 → 0.5.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 +8 -0
- data/README.md +5 -3
- data/config/mammoth.example.yml +46 -10
- data/config/mammoth.schema.json +99 -2
- data/lib/mammoth/application.rb +17 -1
- data/lib/mammoth/configuration.rb +12 -0
- data/lib/mammoth/dead_letter_commands.rb +17 -1
- data/lib/mammoth/fanout_delivery_worker.rb +73 -0
- data/lib/mammoth/status.rb +8 -1
- data/lib/mammoth/version.rb +1 -1
- data/lib/mammoth/webhook_sink.rb +24 -15
- data/lib/mammoth.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8f299dc783af6453331a635200b8fe3ddf03159b156a3a0cec6cc6377ede1c34
|
|
4
|
+
data.tar.gz: f56eabc963912066610c22a98af55af9a6db192f40869dfb1d5743c8f1eff98e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c3167effc0333430719118b2ad3f8d8b53841c8c584b1e45949e256411e1a275eb56b71bd023d4d3778e31ec92f179c338d5814220f11c251f447aad7eee0233
|
|
7
|
+
data.tar.gz: d6db35472dfb2847e00f2158fd8677f26da5cb3c19d91e002566d598d9ecd8d67681e35dc2310ffd7fd41dcc0aec5535e822fd52f55f3865f9f65cc267633383
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.5.0
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added multi-destination webhook fanout through `destinations`, with independent per-destination retry, delivered-ledger, and dead-letter state.
|
|
10
|
+
- Added targeted fanout dead-letter replay so replay sends failed work back only to the original destination.
|
|
11
|
+
- Added Helm rendering for fanout destinations, including secret-backed environment variables for Authorization headers and HMAC signing secrets.
|
|
12
|
+
|
|
5
13
|
## 0.4.0
|
|
6
14
|
|
|
7
15
|
### Added
|
data/README.md
CHANGED
|
@@ -17,7 +17,7 @@ CDC::Core::TransactionEnvelope
|
|
|
17
17
|
↓
|
|
18
18
|
Mammoth
|
|
19
19
|
↓
|
|
20
|
-
Webhook
|
|
20
|
+
Webhook fanout
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
🦣 Mammoth is intentionally boring infrastructure. It uses YAML configuration,
|
|
@@ -46,6 +46,7 @@ https://kanutocd.github.io/mammoth/Mammoth.html
|
|
|
46
46
|
- checkpoint persistence
|
|
47
47
|
- dead letter persistence
|
|
48
48
|
- webhook delivery sink
|
|
49
|
+
- webhook fanout to multiple destinations
|
|
49
50
|
- delivery worker with retry, checkpoint, and DLQ handling
|
|
50
51
|
- dead-letter inspection and replay commands
|
|
51
52
|
- CDC-core event serialization boundary
|
|
@@ -53,11 +54,11 @@ https://kanutocd.github.io/mammoth/Mammoth.html
|
|
|
53
54
|
- Docker image support
|
|
54
55
|
- public Helm chart support
|
|
55
56
|
- unit and e2e test tasks
|
|
56
|
-
-
|
|
57
|
+
- health and metrics endpoints
|
|
57
58
|
|
|
58
59
|
## Boundary
|
|
59
60
|
|
|
60
|
-
Mammoth begins at CDC-core work items and ends at webhook delivery.
|
|
61
|
+
Mammoth begins at CDC-core work items and ends at webhook fanout delivery.
|
|
61
62
|
|
|
62
63
|
Mammoth does not own pgoutput protocol parsing, value decoding, source
|
|
63
64
|
normalization, ordering policy, or runtime execution. Those belong to the
|
|
@@ -103,6 +104,7 @@ Mammoth stores operational memory in SQLite:
|
|
|
103
104
|
- `schema_migrations`
|
|
104
105
|
- `checkpoints`
|
|
105
106
|
- `dead_letters`
|
|
107
|
+
- `delivered_envelopes`
|
|
106
108
|
|
|
107
109
|
## Performance
|
|
108
110
|
|
data/config/mammoth.example.yml
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
# Mammoth example configuration.
|
|
4
4
|
#
|
|
5
|
-
# This file is intentionally verbose. It is
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# This file is intentionally verbose. It is the primary operator-facing
|
|
6
|
+
# reference for local development, Kubernetes values, and production
|
|
7
|
+
# deployments. Update URLs, environment variables, and PostgreSQL settings
|
|
8
|
+
# before using it as a runtime config.
|
|
8
9
|
#
|
|
9
10
|
# Architecture reminder:
|
|
10
11
|
#
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
mammoth:
|
|
25
26
|
# Human-readable Mammoth instance name.
|
|
26
27
|
#
|
|
27
|
-
# Used in logs, status output, metrics, and
|
|
28
|
+
# Used in logs, status output, metrics, and operational dashboards.
|
|
28
29
|
# Keep this stable per environment.
|
|
29
30
|
#
|
|
30
31
|
# Examples:
|
|
@@ -177,8 +178,7 @@ runtime:
|
|
|
177
178
|
#
|
|
178
179
|
# concurrent:
|
|
179
180
|
# Use CDC::Concurrent::ProcessorPool for I/O-heavy delivery workloads such as
|
|
180
|
-
# webhook HTTP calls, retry sleeps/backoff, and
|
|
181
|
-
# fanout.
|
|
181
|
+
# webhook HTTP calls, retry sleeps/backoff, and multi-destination fanout.
|
|
182
182
|
adapter: concurrent
|
|
183
183
|
|
|
184
184
|
# Number of downstream delivery workers.
|
|
@@ -193,6 +193,12 @@ runtime:
|
|
|
193
193
|
# webhook-heavy production: 10-50, after soak testing
|
|
194
194
|
concurrency: 1
|
|
195
195
|
|
|
196
|
+
# Number of work units submitted to the runtime together.
|
|
197
|
+
#
|
|
198
|
+
# Keep this at 1 unless you have validated downstream ordering, retry, and
|
|
199
|
+
# receiver idempotency behavior under load.
|
|
200
|
+
batch_size: 1
|
|
201
|
+
|
|
196
202
|
# Preserve configured ordering while using the runtime.
|
|
197
203
|
#
|
|
198
204
|
# true:
|
|
@@ -206,11 +212,17 @@ runtime:
|
|
|
206
212
|
# Optional runtime timeout in seconds.
|
|
207
213
|
#
|
|
208
214
|
# Leave blank to let destination-specific timeouts control delivery behavior.
|
|
209
|
-
#
|
|
215
|
+
# Use this to bound end-to-end processor execution.
|
|
210
216
|
timeout_seconds:
|
|
211
217
|
|
|
218
|
+
# Single webhook destination shorthand.
|
|
219
|
+
#
|
|
220
|
+
# Use this for one destination. Use destinations below instead when Mammoth
|
|
221
|
+
# should fan out to multiple webhook receivers. Do not configure both unless you
|
|
222
|
+
# intentionally want destinations to replace this shorthand.
|
|
212
223
|
webhook:
|
|
213
|
-
# Destination name used in logs, dead-letter records,
|
|
224
|
+
# Destination name used in logs, dead-letter records, delivered ledgers, and
|
|
225
|
+
# metrics.
|
|
214
226
|
name: primary_webhook
|
|
215
227
|
|
|
216
228
|
# Destination URL for delivered CDC payloads.
|
|
@@ -230,6 +242,7 @@ webhook:
|
|
|
230
242
|
X-Mammoth-Source: local_mammoth
|
|
231
243
|
|
|
232
244
|
# Optional HTTP headers whose values are read from environment variables.
|
|
245
|
+
# In Kubernetes, set these variables from Secrets.
|
|
233
246
|
#
|
|
234
247
|
# Example:
|
|
235
248
|
# header_env:
|
|
@@ -244,6 +257,7 @@ webhook:
|
|
|
244
257
|
#
|
|
245
258
|
# using the secret from secret_env, then sends the timestamp and signature in
|
|
246
259
|
# configurable headers. The signature value is prefixed with "sha256=".
|
|
260
|
+
# In Kubernetes, set the secret_env variable from a Secret.
|
|
247
261
|
#
|
|
248
262
|
# signing:
|
|
249
263
|
# algorithm: hmac_sha256
|
|
@@ -251,6 +265,28 @@ webhook:
|
|
|
251
265
|
# signature_header: X-Mammoth-Signature
|
|
252
266
|
# timestamp_header: X-Mammoth-Timestamp
|
|
253
267
|
|
|
268
|
+
# Optional fanout configuration.
|
|
269
|
+
#
|
|
270
|
+
# Use destinations instead of webhook when one PostgreSQL replication stream
|
|
271
|
+
# should deliver the same CDC work item to multiple webhook receivers. Each
|
|
272
|
+
# destination gets independent retry, delivered-ledger, and dead-letter state.
|
|
273
|
+
# The destinations list replaces the webhook shorthand above.
|
|
274
|
+
#
|
|
275
|
+
# destinations:
|
|
276
|
+
# - name: primary_webhook
|
|
277
|
+
# type: webhook
|
|
278
|
+
# url: https://example.com/webhooks/postgres
|
|
279
|
+
# timeout_seconds: 5
|
|
280
|
+
# header_env:
|
|
281
|
+
# Authorization: MAMMOTH_PRIMARY_WEBHOOK_AUTHORIZATION
|
|
282
|
+
# signing:
|
|
283
|
+
# algorithm: hmac_sha256
|
|
284
|
+
# secret_env: MAMMOTH_PRIMARY_WEBHOOK_SIGNING_SECRET
|
|
285
|
+
# - name: audit_webhook
|
|
286
|
+
# type: webhook
|
|
287
|
+
# url: https://audit.example.com/cdc
|
|
288
|
+
# timeout_seconds: 5
|
|
289
|
+
|
|
254
290
|
retry:
|
|
255
291
|
# Maximum delivery attempts before the event/envelope is moved to the dead
|
|
256
292
|
# letter store.
|
|
@@ -270,8 +306,8 @@ retry:
|
|
|
270
306
|
sqlite:
|
|
271
307
|
# Local operational database path.
|
|
272
308
|
#
|
|
273
|
-
# Mammoth stores checkpoints,
|
|
274
|
-
# In Kubernetes, back this path with a PersistentVolumeClaim.
|
|
309
|
+
# Mammoth stores checkpoints, delivered-envelope ledgers, and dead-letter
|
|
310
|
+
# records here. In Kubernetes, back this path with a PersistentVolumeClaim.
|
|
275
311
|
path: data/mammoth.db
|
|
276
312
|
|
|
277
313
|
observability:
|
data/config/mammoth.schema.json
CHANGED
|
@@ -8,11 +8,22 @@
|
|
|
8
8
|
"mammoth",
|
|
9
9
|
"postgres",
|
|
10
10
|
"replication",
|
|
11
|
-
"webhook",
|
|
12
11
|
"retry",
|
|
13
12
|
"sqlite",
|
|
14
13
|
"logging"
|
|
15
14
|
],
|
|
15
|
+
"anyOf": [
|
|
16
|
+
{
|
|
17
|
+
"required": [
|
|
18
|
+
"webhook"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"required": [
|
|
23
|
+
"destinations"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
16
27
|
"properties": {
|
|
17
28
|
"mammoth": {
|
|
18
29
|
"type": "object",
|
|
@@ -187,7 +198,93 @@
|
|
|
187
198
|
"description": "Optional HMAC-SHA256 request signing for webhook receivers."
|
|
188
199
|
}
|
|
189
200
|
},
|
|
190
|
-
"description": "HTTP webhook destination settings."
|
|
201
|
+
"description": "HTTP webhook destination settings. This is shorthand for a single webhook destination; use destinations for fanout."
|
|
202
|
+
},
|
|
203
|
+
"destinations": {
|
|
204
|
+
"type": "array",
|
|
205
|
+
"minItems": 1,
|
|
206
|
+
"items": {
|
|
207
|
+
"type": "object",
|
|
208
|
+
"additionalProperties": false,
|
|
209
|
+
"required": [
|
|
210
|
+
"name",
|
|
211
|
+
"type",
|
|
212
|
+
"url",
|
|
213
|
+
"timeout_seconds"
|
|
214
|
+
],
|
|
215
|
+
"properties": {
|
|
216
|
+
"name": {
|
|
217
|
+
"type": "string",
|
|
218
|
+
"minLength": 1,
|
|
219
|
+
"description": "Destination name used in delivered ledgers, dead-letter records, logs, and metrics."
|
|
220
|
+
},
|
|
221
|
+
"type": {
|
|
222
|
+
"type": "string",
|
|
223
|
+
"enum": [
|
|
224
|
+
"webhook"
|
|
225
|
+
],
|
|
226
|
+
"description": "Destination adapter type. Mammoth OSS 0.5.0 supports webhook destinations."
|
|
227
|
+
},
|
|
228
|
+
"url": {
|
|
229
|
+
"type": "string",
|
|
230
|
+
"format": "uri",
|
|
231
|
+
"description": "Webhook URL for delivered CDC payloads. Transaction delivery sends a TransactionEnvelope payload; event delivery sends a single event payload."
|
|
232
|
+
},
|
|
233
|
+
"timeout_seconds": {
|
|
234
|
+
"type": "integer",
|
|
235
|
+
"minimum": 1,
|
|
236
|
+
"description": "HTTP timeout in seconds for this destination's delivery attempts."
|
|
237
|
+
},
|
|
238
|
+
"headers": {
|
|
239
|
+
"type": "object",
|
|
240
|
+
"additionalProperties": {
|
|
241
|
+
"type": "string"
|
|
242
|
+
},
|
|
243
|
+
"description": "Static HTTP headers added to every request for this destination. Do not put secrets here; use header_env for secret-backed values."
|
|
244
|
+
},
|
|
245
|
+
"header_env": {
|
|
246
|
+
"type": "object",
|
|
247
|
+
"additionalProperties": {
|
|
248
|
+
"type": "string",
|
|
249
|
+
"minLength": 1
|
|
250
|
+
},
|
|
251
|
+
"description": "HTTP headers whose values are read from environment variables. Keys are header names; values are environment variable names."
|
|
252
|
+
},
|
|
253
|
+
"signing": {
|
|
254
|
+
"type": "object",
|
|
255
|
+
"additionalProperties": false,
|
|
256
|
+
"required": [
|
|
257
|
+
"secret_env"
|
|
258
|
+
],
|
|
259
|
+
"properties": {
|
|
260
|
+
"algorithm": {
|
|
261
|
+
"type": "string",
|
|
262
|
+
"enum": [
|
|
263
|
+
"hmac_sha256"
|
|
264
|
+
],
|
|
265
|
+
"description": "Webhook signing algorithm. Mammoth signs '<timestamp>.<body>' using HMAC-SHA256."
|
|
266
|
+
},
|
|
267
|
+
"secret_env": {
|
|
268
|
+
"type": "string",
|
|
269
|
+
"minLength": 1,
|
|
270
|
+
"description": "Environment variable containing the webhook signing secret."
|
|
271
|
+
},
|
|
272
|
+
"signature_header": {
|
|
273
|
+
"type": "string",
|
|
274
|
+
"minLength": 1,
|
|
275
|
+
"description": "Header name used for the generated signature. Defaults to X-Mammoth-Signature."
|
|
276
|
+
},
|
|
277
|
+
"timestamp_header": {
|
|
278
|
+
"type": "string",
|
|
279
|
+
"minLength": 1,
|
|
280
|
+
"description": "Header name used for the signing timestamp. Defaults to X-Mammoth-Timestamp."
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
"description": "Optional HMAC-SHA256 request signing for this webhook destination."
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
"description": "Fanout destination list. Each CDC work item is delivered independently to each configured destination."
|
|
191
288
|
},
|
|
192
289
|
"retry": {
|
|
193
290
|
"type": "object",
|
data/lib/mammoth/application.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Mammoth
|
|
|
21
21
|
@sqlite_store = SQLiteStore.connect(config.dig("sqlite", "path")).bootstrap!
|
|
22
22
|
@checkpoint_store = CheckpointStore.new(sqlite_store)
|
|
23
23
|
@consumer = ReplicationConsumer.new(source: source || build_source, delivery_unit: delivery_unit)
|
|
24
|
-
@delivery_worker = build_delivery_worker(sink: sink
|
|
24
|
+
@delivery_worker = sink ? build_delivery_worker(sink: sink, sleeper: sleeper) : build_configured_delivery_worker(sleeper:)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# Start the application runtime and deliver consumed CDC work.
|
|
@@ -97,6 +97,22 @@ module Mammoth
|
|
|
97
97
|
)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
def build_configured_delivery_worker(sleeper:)
|
|
101
|
+
workers = destination_sinks.map { |sink| build_delivery_worker(sink:, sleeper:) }
|
|
102
|
+
return workers.fetch(0) if workers.one?
|
|
103
|
+
|
|
104
|
+
FanoutDeliveryWorker.new(workers)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def destination_sinks
|
|
108
|
+
destinations = config.data["destinations"]
|
|
109
|
+
return [WebhookSink.from_config(config)] unless destinations
|
|
110
|
+
|
|
111
|
+
destinations.map.with_index(1) do |destination, index|
|
|
112
|
+
WebhookSink.from_destination_config(destination, label: "destinations[#{index - 1}]")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
100
116
|
def transaction_delivery?
|
|
101
117
|
delivery_unit == :transaction
|
|
102
118
|
end
|
|
@@ -44,6 +44,7 @@ module Mammoth
|
|
|
44
44
|
raise ConfigurationError, "configuration must be a YAML mapping" unless data.is_a?(Hash)
|
|
45
45
|
|
|
46
46
|
validate_schema!
|
|
47
|
+
validate_destination_names!
|
|
47
48
|
self
|
|
48
49
|
rescue Psych::SyntaxError => e
|
|
49
50
|
raise ConfigurationError, "invalid YAML in #{path}: #{e.message}"
|
|
@@ -73,5 +74,16 @@ module Mammoth
|
|
|
73
74
|
def schema_error_message(errors)
|
|
74
75
|
(["configuration failed schema validation:"] + errors.map { |error| "- #{error}" }).join("\n")
|
|
75
76
|
end
|
|
77
|
+
|
|
78
|
+
def validate_destination_names!
|
|
79
|
+
destinations = data["destinations"]
|
|
80
|
+
return unless destinations
|
|
81
|
+
|
|
82
|
+
names = destinations.map { |destination| destination["name"] }
|
|
83
|
+
duplicates = names.tally.select { |_name, count| count > 1 }.keys
|
|
84
|
+
return if duplicates.empty?
|
|
85
|
+
|
|
86
|
+
raise ConfigurationError, "destination names must be unique: #{duplicates.join(", ")}"
|
|
87
|
+
end
|
|
76
88
|
end
|
|
77
89
|
end
|
|
@@ -163,7 +163,23 @@ module Mammoth
|
|
|
163
163
|
|
|
164
164
|
def replay_row(row)
|
|
165
165
|
payload = JSON.parse(row.fetch("payload_json"))
|
|
166
|
-
transaction_payload?(payload)
|
|
166
|
+
if transaction_payload?(payload)
|
|
167
|
+
replay_transaction(row.fetch("destination_name"), transaction_envelope(payload))
|
|
168
|
+
else
|
|
169
|
+
replay_event(row.fetch("destination_name"), payload)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def replay_event(destination_name, payload)
|
|
174
|
+
return worker.deliver_to(destination_name, payload) if worker.respond_to?(:deliver_to)
|
|
175
|
+
|
|
176
|
+
worker.deliver(payload)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def replay_transaction(destination_name, envelope)
|
|
180
|
+
return worker.deliver_transaction_to(destination_name, envelope) if worker.respond_to?(:deliver_transaction_to)
|
|
181
|
+
|
|
182
|
+
worker.deliver_transaction(envelope)
|
|
167
183
|
end
|
|
168
184
|
|
|
169
185
|
def transaction_payload?(payload)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mammoth
|
|
4
|
+
# Delivers one CDC work item to multiple destination-specific workers.
|
|
5
|
+
class FanoutDeliveryWorker
|
|
6
|
+
attr_reader :delivery_workers
|
|
7
|
+
|
|
8
|
+
# @param delivery_workers [Array<Mammoth::DeliveryWorker>] destination workers
|
|
9
|
+
def initialize(delivery_workers)
|
|
10
|
+
raise ConfigurationError, "at least one destination is required" if delivery_workers.empty?
|
|
11
|
+
|
|
12
|
+
@delivery_workers = delivery_workers
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Deliver an event to every configured destination.
|
|
16
|
+
#
|
|
17
|
+
# @param event [Hash, #to_h] normalized event
|
|
18
|
+
# @return [Hash] aggregate fanout summary
|
|
19
|
+
def deliver(event)
|
|
20
|
+
fanout(:deliver, event)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Deliver a transaction envelope to every configured destination.
|
|
24
|
+
#
|
|
25
|
+
# @param envelope [#events, #transaction_id] CDC transaction envelope
|
|
26
|
+
# @return [Hash] aggregate fanout summary
|
|
27
|
+
def deliver_transaction(envelope)
|
|
28
|
+
fanout(:deliver_transaction, envelope)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Deliver an event to one configured destination.
|
|
32
|
+
#
|
|
33
|
+
# @param destination_name [String] destination name
|
|
34
|
+
# @param event [Hash, #to_h] normalized event
|
|
35
|
+
# @return [Hash] destination delivery summary
|
|
36
|
+
def deliver_to(destination_name, event)
|
|
37
|
+
worker_for(destination_name).deliver(event)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Deliver a transaction envelope to one configured destination.
|
|
41
|
+
#
|
|
42
|
+
# @param destination_name [String] destination name
|
|
43
|
+
# @param envelope [#events, #transaction_id] CDC transaction envelope
|
|
44
|
+
# @return [Hash] destination delivery summary
|
|
45
|
+
def deliver_transaction_to(destination_name, envelope)
|
|
46
|
+
worker_for(destination_name).deliver_transaction(envelope)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def fanout(delivery_method, work)
|
|
52
|
+
results = delivery_workers.map { |worker| worker.public_send(delivery_method, work) }
|
|
53
|
+
{
|
|
54
|
+
status: aggregate_status(results),
|
|
55
|
+
destinations: results,
|
|
56
|
+
delivered: results.count { |result| result.fetch(:status) == "delivered" },
|
|
57
|
+
skipped: results.count { |result| result.fetch(:status) == "skipped" },
|
|
58
|
+
dead_lettered: results.count { |result| result.fetch(:status) == "dead_lettered" }
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def aggregate_status(results)
|
|
63
|
+
return "fanout_delivered" if results.all? { |result| %w[delivered skipped].include?(result.fetch(:status)) }
|
|
64
|
+
|
|
65
|
+
"fanout_partial"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def worker_for(destination_name)
|
|
69
|
+
delivery_workers.find { |worker| worker.send(:destination_name) == destination_name } ||
|
|
70
|
+
raise(ConfigurationError, "destination not configured: #{destination_name}")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/mammoth/status.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Mammoth
|
|
|
30
30
|
puts "Replication publications: #{Array(config.dig("replication", "publications")).join(", ")}"
|
|
31
31
|
puts "Runtime: not started"
|
|
32
32
|
puts "SQLite: #{sqlite_path}"
|
|
33
|
-
puts "
|
|
33
|
+
puts "Destinations: #{destination_names.join(", ")}"
|
|
34
34
|
print_store_state if sqlite_store
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -40,6 +40,13 @@ module Mammoth
|
|
|
40
40
|
config.dig("sqlite", "path")
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def destination_names
|
|
44
|
+
destinations = config.data["destinations"]
|
|
45
|
+
return destinations.map { |destination| destination.fetch("name") } if destinations
|
|
46
|
+
|
|
47
|
+
[config.dig("webhook", "name")]
|
|
48
|
+
end
|
|
49
|
+
|
|
43
50
|
def print_store_state
|
|
44
51
|
store = sqlite_store.bootstrap!
|
|
45
52
|
puts "Tables: #{store.tables.join(", ")}"
|
data/lib/mammoth/version.rb
CHANGED
data/lib/mammoth/webhook_sink.rb
CHANGED
|
@@ -39,43 +39,52 @@ module Mammoth
|
|
|
39
39
|
# @param config [Mammoth::Configuration] loaded configuration
|
|
40
40
|
# @return [Mammoth::WebhookSink]
|
|
41
41
|
def from_config(config)
|
|
42
|
+
from_destination_config(config.data["webhook"], label: "webhook")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Build a sink from one destination configuration entry.
|
|
46
|
+
#
|
|
47
|
+
# @param destination [Hash] destination configuration
|
|
48
|
+
# @param label [String] configuration path used in error messages
|
|
49
|
+
# @return [Mammoth::WebhookSink]
|
|
50
|
+
def from_destination_config(destination, label: "destination")
|
|
42
51
|
new(
|
|
43
|
-
name:
|
|
44
|
-
url:
|
|
45
|
-
timeout_seconds:
|
|
46
|
-
headers: configured_headers(
|
|
47
|
-
signing: configured_signing(
|
|
52
|
+
name: destination.fetch("name"),
|
|
53
|
+
url: destination.fetch("url"),
|
|
54
|
+
timeout_seconds: destination.fetch("timeout_seconds"),
|
|
55
|
+
headers: configured_headers(destination, label: label),
|
|
56
|
+
signing: configured_signing(destination, label: label)
|
|
48
57
|
)
|
|
49
58
|
end
|
|
50
59
|
|
|
51
60
|
private
|
|
52
61
|
|
|
53
|
-
def configured_headers(
|
|
54
|
-
static_headers =
|
|
55
|
-
env_headers =
|
|
62
|
+
def configured_headers(destination, label:)
|
|
63
|
+
static_headers = destination["headers"] || {}
|
|
64
|
+
env_headers = destination["header_env"] || {}
|
|
56
65
|
|
|
57
|
-
static_headers.merge(resolve_env_headers(env_headers))
|
|
66
|
+
static_headers.merge(resolve_env_headers(env_headers, label: label))
|
|
58
67
|
end
|
|
59
68
|
|
|
60
|
-
def resolve_env_headers(env_headers)
|
|
69
|
+
def resolve_env_headers(env_headers, label:)
|
|
61
70
|
env_headers.each_with_object(Hash.new) do |(header, env_name), resolved| # rubocop:disable Style/EmptyLiteral
|
|
62
71
|
resolved[header] = ENV.fetch(env_name) do
|
|
63
|
-
raise ConfigurationError, "
|
|
72
|
+
raise ConfigurationError, "#{label}.header_env.#{header} references missing environment variable #{env_name}"
|
|
64
73
|
end
|
|
65
74
|
end
|
|
66
75
|
end
|
|
67
76
|
|
|
68
|
-
def configured_signing(
|
|
69
|
-
signing =
|
|
77
|
+
def configured_signing(destination, label:)
|
|
78
|
+
signing = destination["signing"]
|
|
70
79
|
return unless signing
|
|
71
80
|
|
|
72
81
|
algorithm = signing.fetch("algorithm", SIGNING_ALGORITHM)
|
|
73
|
-
raise ConfigurationError, "
|
|
82
|
+
raise ConfigurationError, "#{label}.signing.algorithm must be #{SIGNING_ALGORITHM}" unless algorithm == SIGNING_ALGORITHM
|
|
74
83
|
|
|
75
84
|
secret_env = signing.fetch("secret_env")
|
|
76
85
|
{
|
|
77
86
|
secret: ENV.fetch(secret_env) do
|
|
78
|
-
raise ConfigurationError, "
|
|
87
|
+
raise ConfigurationError, "#{label}.signing.secret_env references missing environment variable #{secret_env}"
|
|
79
88
|
end,
|
|
80
89
|
signature_header: signing.fetch("signature_header", "X-Mammoth-Signature"),
|
|
81
90
|
timestamp_header: signing.fetch("timestamp_header", "X-Mammoth-Timestamp")
|
data/lib/mammoth.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "mammoth/event_serializer"
|
|
|
14
14
|
require_relative "mammoth/transaction_envelope_serializer"
|
|
15
15
|
require_relative "mammoth/webhook_sink"
|
|
16
16
|
require_relative "mammoth/delivery_worker"
|
|
17
|
+
require_relative "mammoth/fanout_delivery_worker"
|
|
17
18
|
require_relative "mammoth/delivery_processor"
|
|
18
19
|
require_relative "mammoth/concurrent_delivery_runtime"
|
|
19
20
|
require_relative "mammoth/sources/postgres"
|
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.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
@@ -157,6 +157,7 @@ files:
|
|
|
157
157
|
- lib/mammoth/delivery_worker.rb
|
|
158
158
|
- lib/mammoth/errors.rb
|
|
159
159
|
- lib/mammoth/event_serializer.rb
|
|
160
|
+
- lib/mammoth/fanout_delivery_worker.rb
|
|
160
161
|
- lib/mammoth/observability_server.rb
|
|
161
162
|
- lib/mammoth/observability_snapshot.rb
|
|
162
163
|
- lib/mammoth/replication_consumer.rb
|