mammoth 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8fdbb7afb90f584e11024c75d7300f54978f5e5b1a3628597bb460f96a306c22
4
- data.tar.gz: 9ced2250d7e2f95558a8bcce51809aa4d670d49953d8d970e168a0c48cf90215
3
+ metadata.gz: c04c055c5cf5faedef6100513bc63a9ada3c69b99b904f701d0be93501aa4ebd
4
+ data.tar.gz: 15e75ed7ed39723f5f0864253196a830ae0413d6609ae504640be2eca7d6b2da
5
5
  SHA512:
6
- metadata.gz: cd6b5b3246cf70adb9beb0cc9243e6a35788d20f3442ab406bca66a4cb08bd4a7d80639386c4e42291b518190a0198f960ed9e16cbd165f335a7f3fee10317b7
7
- data.tar.gz: 87aa4ee77c38aa69d1a505bf7eb2b838de86504316840dfc26b4e875870333f64da906c85aa180a205e5e3bff4f439e36d2f82ffa79d650117d9f5ccc4c29b69
6
+ metadata.gz: 1d0b5446e4b5edd10f5d82007dd8ae85e7e9e40f056725ef1faa5e090c2fa844a9007c94c7ac47b41cb7eeec6fe8d9be3eb0dec7685a757da749ff7dbf85ccbd
7
+ data.tar.gz: 3f3185bc9743f100bab26a62f2fa45f94f04aa3243e787ab05d0b13aafc697a22ab432a84da1e40dfd7214e880bafe727e17c8ca4e15236ca97547f55166687c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.5.1
6
+
7
+ ### Added
8
+
9
+ - Added benchmark scripts for webhook delivery, webhook fanout, SQLite operational state, observability snapshots, and dead-letter replay.
10
+ - Added shared benchmark helpers for synthetic CDC work items, local HTTP receivers, table output, JSON output, and environment-driven benchmark knobs.
11
+ - Added a benchmark snapshot runner that writes Markdown, JSON, and per-trial logs for publishable benchmark results.
12
+
13
+ ### Changed
14
+
15
+ - Aligned benchmark documentation across `README.md`, `docs/BENCHMARKS.md`, and `benchmark/README.md` around operator config tuning.
16
+
17
+ ## 0.5.0
18
+
19
+ ### Added
20
+
21
+ - Added multi-destination webhook fanout through `destinations`, with independent per-destination retry, delivered-ledger, and dead-letter state.
22
+ - Added targeted fanout dead-letter replay so replay sends failed work back only to the original destination.
23
+ - Added Helm rendering for fanout destinations, including secret-backed environment variables for Authorization headers and HMAC signing secrets.
24
+
5
25
  ## 0.4.0
6
26
 
7
27
  ### 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
- - Health and metrics endpoints
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,50 +104,31 @@ 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
 
109
- Mammoth scales downstream delivery throughput using the `cdc-concurrent` runtime while maintaining a single PostgreSQL logical replication stream.
111
+ Mammoth includes local benchmarks for the product surfaces operators tune in
112
+ production:
110
113
 
111
- ### Concurrent Delivery Benchmark
114
+ - concurrent delivery runtime
115
+ - real webhook delivery
116
+ - multi-destination webhook fanout
117
+ - SQLite operational state
118
+ - observability snapshots
119
+ - DLQ replay
112
120
 
113
- Environment:
121
+ The historical numbers in [Benchmarks](https://github.com/kanutocd/mammoth/tree/main/docs/BENCHMARKS.md) are retained as a
122
+ snapshot, not a universal performance claim. Re-run the scripts in
123
+ [`benchmark/`](https://github.com/kanutocd/mammoth/tree/main/benchmark) on your own hardware when choosing
124
+ `runtime.concurrency`, `destinations`, SQLite storage, scrape frequency, and
125
+ DLQ replay expectations.
114
126
 
115
- * 10,000 transactions
116
- * 4 events per transaction
117
- * 40,000 total events
127
+ Create a publishable benchmark snapshot with:
118
128
 
119
- #### Fast Downstream (10ms)
120
-
121
- | Concurrency | Transactions/sec |
122
- | ----------- | ---------------: |
123
- | 1 | 96.50 |
124
- | 25 | 2419.65 |
125
-
126
- #### Realistic Webhook (50ms)
127
-
128
- | Concurrency | Transactions/sec |
129
- | ----------- | ---------------: |
130
- | 1 | 19.85 |
131
- | 25 | 495.11 |
132
-
133
- Observed throughput improved from:
134
-
135
- - 96.50 → 2419.65 transactions/sec (10ms sink latency)
136
- - 19.85 → 495.11 transactions/sec (50ms sink latency)
137
-
138
- when increasing delivery concurrency from 1 to 25.
139
-
140
- The benchmark demonstrates near-linear scaling of delivery throughput without increasing PostgreSQL replication connections.
141
-
142
- See:
143
-
144
- - docs/BENCHMARKS.md
145
- - examples/transaction_webhook
146
-
147
- For full benchmark methodology and results see:
148
-
149
- See [Benchmarks](docs/BENCHMARKS.md).
129
+ ```bash
130
+ bundle exec ruby benchmark/snapshot.rb
131
+ ```
150
132
 
151
133
  ## E2E
152
134
 
@@ -2,9 +2,10 @@
2
2
 
3
3
  # Mammoth example configuration.
4
4
  #
5
- # This file is intentionally verbose. It is both a runnable starting point and
6
- # the primary operator-facing documentation for local development, Kubernetes
7
- # values, and production deployments.
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 future operational dashboards.
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 future multi-destination
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
- # Useful later for bounding end-to-end processor execution.
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, and future metrics.
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, delivery attempts, and dead-letter records here.
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:
@@ -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.1 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",
@@ -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 || WebhookSink.from_config(config), sleeper: sleeper)
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) ? worker.deliver_transaction(transaction_envelope(payload)) : worker.deliver(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
@@ -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 "Webhook: #{config.dig("webhook", "name")}"
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(", ")}"
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Mammoth
4
4
  # Current Mammoth gem version.
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.1"
6
6
  end
@@ -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: config.dig("webhook", "name"),
44
- url: config.dig("webhook", "url"),
45
- timeout_seconds: config.dig("webhook", "timeout_seconds"),
46
- headers: configured_headers(config),
47
- signing: configured_signing(config)
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(config)
54
- static_headers = config.dig("webhook", "headers") || {}
55
- env_headers = config.dig("webhook", "header_env") || {}
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, "webhook.header_env.#{header} references missing environment variable #{env_name}"
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(config)
69
- signing = config.dig("webhook", "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, "webhook.signing.algorithm must be #{SIGNING_ALGORITHM}" unless algorithm == SIGNING_ALGORITHM
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, "webhook.signing.secret_env references missing environment variable #{secret_env}"
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.0
4
+ version: 0.5.1
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