mammoth 0.1.0 → 0.2.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 +102 -0
- data/README.md +59 -4
- 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 +1 -1
- data/lib/mammoth/concurrent_delivery_runtime.rb +52 -0
- data/lib/mammoth/dead_letter_store.rb +3 -3
- data/lib/mammoth/delivered_envelope_store.rb +98 -0
- data/lib/mammoth/delivery_processor.rb +68 -0
- data/lib/mammoth/delivery_worker.rb +74 -12
- data/lib/mammoth/replication_consumer.rb +39 -4
- data/lib/mammoth/sources/postgres.rb +188 -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 +92 -16
- data/lib/mammoth.rb +4 -0
- metadata +22 -3
data/lib/mammoth/webhook_sink.rb
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "net/http"
|
|
5
|
+
require "openssl"
|
|
5
6
|
require "socket"
|
|
7
|
+
require "time"
|
|
6
8
|
require "uri"
|
|
7
9
|
|
|
8
10
|
module Mammoth
|
|
@@ -11,27 +13,72 @@ module Mammoth
|
|
|
11
13
|
# HTTP status range treated as successful webhook delivery.
|
|
12
14
|
SUCCESS_RANGE = 200..299
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
SIGNING_ALGORITHM = "hmac_sha256"
|
|
17
|
+
SIGNATURE_PREFIX = "sha256="
|
|
18
|
+
|
|
19
|
+
attr_reader :name, :url, :timeout_seconds, :headers, :signing
|
|
15
20
|
|
|
16
21
|
# @param name [String] destination name
|
|
17
22
|
# @param url [String] webhook endpoint URL
|
|
18
23
|
# @param timeout_seconds [Integer] HTTP open/read timeout in seconds
|
|
19
|
-
|
|
24
|
+
# @param headers [Hash] static HTTP headers applied to every request
|
|
25
|
+
# @param signing [Hash, nil] HMAC signing configuration
|
|
26
|
+
def initialize(name:, url:, timeout_seconds: 5, headers: {}, signing: nil)
|
|
20
27
|
@name = name
|
|
21
28
|
@url = URI(url)
|
|
22
29
|
@timeout_seconds = timeout_seconds
|
|
30
|
+
@headers = headers.transform_keys(&:to_s)
|
|
31
|
+
@signing = signing
|
|
23
32
|
end
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
class << self
|
|
35
|
+
# Build a sink from Mammoth configuration.
|
|
36
|
+
#
|
|
37
|
+
# @param config [Mammoth::Configuration] loaded configuration
|
|
38
|
+
# @return [Mammoth::WebhookSink]
|
|
39
|
+
def from_config(config)
|
|
40
|
+
new(
|
|
41
|
+
name: config.dig("webhook", "name"),
|
|
42
|
+
url: config.dig("webhook", "url"),
|
|
43
|
+
timeout_seconds: config.dig("webhook", "timeout_seconds"),
|
|
44
|
+
headers: configured_headers(config),
|
|
45
|
+
signing: configured_signing(config)
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def configured_headers(config)
|
|
52
|
+
static_headers = config.dig("webhook", "headers") || {}
|
|
53
|
+
env_headers = config.dig("webhook", "header_env") || {}
|
|
54
|
+
|
|
55
|
+
static_headers.merge(resolve_env_headers(env_headers))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resolve_env_headers(env_headers)
|
|
59
|
+
env_headers.each_with_object(Hash.new) do |(header, env_name), resolved| # rubocop:disable Style/EmptyLiteral
|
|
60
|
+
resolved[header] = ENV.fetch(env_name) do
|
|
61
|
+
raise ConfigurationError, "webhook.header_env.#{header} references missing environment variable #{env_name}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def configured_signing(config)
|
|
67
|
+
signing = config.dig("webhook", "signing")
|
|
68
|
+
return unless signing
|
|
69
|
+
|
|
70
|
+
algorithm = signing.fetch("algorithm", SIGNING_ALGORITHM)
|
|
71
|
+
raise ConfigurationError, "webhook.signing.algorithm must be #{SIGNING_ALGORITHM}" unless algorithm == SIGNING_ALGORITHM
|
|
72
|
+
|
|
73
|
+
secret_env = signing.fetch("secret_env")
|
|
74
|
+
{
|
|
75
|
+
secret: ENV.fetch(secret_env) do
|
|
76
|
+
raise ConfigurationError, "webhook.signing.secret_env references missing environment variable #{secret_env}"
|
|
77
|
+
end,
|
|
78
|
+
signature_header: signing.fetch("signature_header", "X-Mammoth-Signature"),
|
|
79
|
+
timestamp_header: signing.fetch("timestamp_header", "X-Mammoth-Timestamp")
|
|
80
|
+
}
|
|
81
|
+
end
|
|
35
82
|
end
|
|
36
83
|
|
|
37
84
|
# Deliver an event to the webhook endpoint.
|
|
@@ -40,7 +87,21 @@ module Mammoth
|
|
|
40
87
|
# @return [Hash] delivery result
|
|
41
88
|
# @raise [Mammoth::DeliveryError] when delivery fails
|
|
42
89
|
def deliver(event)
|
|
43
|
-
|
|
90
|
+
deliver_payload(EventSerializer.call(event))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Deliver a transaction envelope to the webhook endpoint.
|
|
94
|
+
#
|
|
95
|
+
# @param envelope [#events, #transaction_id] CDC transaction envelope
|
|
96
|
+
# @return [Hash] delivery result
|
|
97
|
+
# @raise [Mammoth::DeliveryError] when delivery fails
|
|
98
|
+
def deliver_transaction(envelope)
|
|
99
|
+
deliver_payload(TransactionEnvelopeSerializer.call(envelope))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def deliver_payload(payload)
|
|
44
105
|
response = perform_request(payload)
|
|
45
106
|
return delivery_result(payload, response) if SUCCESS_RANGE.cover?(response.code.to_i)
|
|
46
107
|
|
|
@@ -49,8 +110,6 @@ module Mammoth
|
|
|
49
110
|
raise DeliveryError, "webhook #{name} delivery failed: #{e.message}"
|
|
50
111
|
end
|
|
51
112
|
|
|
52
|
-
private
|
|
53
|
-
|
|
54
113
|
def perform_request(payload)
|
|
55
114
|
Net::HTTP.start(url.host, url.port, use_ssl: url.scheme == "https", open_timeout: timeout_seconds,
|
|
56
115
|
read_timeout: timeout_seconds) do |http|
|
|
@@ -59,19 +118,36 @@ module Mammoth
|
|
|
59
118
|
end
|
|
60
119
|
|
|
61
120
|
def build_request(payload)
|
|
121
|
+
body = JSON.generate(payload)
|
|
62
122
|
Net::HTTP::Post.new(url.request_uri).tap do |request|
|
|
63
123
|
request["Content-Type"] = "application/json"
|
|
64
|
-
|
|
124
|
+
headers.each { |header, value| request[header] = value }
|
|
125
|
+
apply_signature_headers(request, body)
|
|
126
|
+
request.body = body
|
|
65
127
|
end
|
|
66
128
|
end
|
|
67
129
|
|
|
68
130
|
def delivery_result(payload, response)
|
|
69
131
|
{
|
|
70
132
|
event_id: payload.fetch("event_id"),
|
|
133
|
+
payload_type: payload["type"] || "event",
|
|
71
134
|
destination: name,
|
|
72
135
|
status: "delivered",
|
|
73
136
|
http_status: response.code.to_i
|
|
74
137
|
}
|
|
75
138
|
end
|
|
139
|
+
|
|
140
|
+
def apply_signature_headers(request, body)
|
|
141
|
+
return unless signing
|
|
142
|
+
|
|
143
|
+
timestamp = Time.now.utc.iso8601
|
|
144
|
+
request[signing.fetch(:timestamp_header)] = timestamp
|
|
145
|
+
request[signing.fetch(:signature_header)] = signed_body(timestamp, body)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def signed_body(timestamp, body)
|
|
149
|
+
digest = OpenSSL::HMAC.hexdigest("SHA256", signing.fetch(:secret), "#{timestamp}.#{body}")
|
|
150
|
+
"#{SIGNATURE_PREFIX}#{digest}"
|
|
151
|
+
end
|
|
76
152
|
end
|
|
77
153
|
end
|
data/lib/mammoth.rb
CHANGED
|
@@ -7,9 +7,13 @@ require_relative "mammoth/status"
|
|
|
7
7
|
require_relative "mammoth/sqlite_store"
|
|
8
8
|
require_relative "mammoth/checkpoint_store"
|
|
9
9
|
require_relative "mammoth/dead_letter_store"
|
|
10
|
+
require_relative "mammoth/delivered_envelope_store"
|
|
10
11
|
require_relative "mammoth/event_serializer"
|
|
12
|
+
require_relative "mammoth/transaction_envelope_serializer"
|
|
11
13
|
require_relative "mammoth/webhook_sink"
|
|
12
14
|
require_relative "mammoth/delivery_worker"
|
|
15
|
+
require_relative "mammoth/delivery_processor"
|
|
16
|
+
require_relative "mammoth/concurrent_delivery_runtime"
|
|
13
17
|
require_relative "mammoth/sources/postgres"
|
|
14
18
|
require_relative "mammoth/cdc_source"
|
|
15
19
|
require_relative "mammoth/replication_consumer"
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: cdc-concurrent
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: cdc-core
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -134,8 +148,11 @@ files:
|
|
|
134
148
|
- lib/mammoth/cdc_source.rb
|
|
135
149
|
- lib/mammoth/checkpoint_store.rb
|
|
136
150
|
- lib/mammoth/cli.rb
|
|
151
|
+
- lib/mammoth/concurrent_delivery_runtime.rb
|
|
137
152
|
- lib/mammoth/configuration.rb
|
|
138
153
|
- lib/mammoth/dead_letter_store.rb
|
|
154
|
+
- lib/mammoth/delivered_envelope_store.rb
|
|
155
|
+
- lib/mammoth/delivery_processor.rb
|
|
139
156
|
- lib/mammoth/delivery_worker.rb
|
|
140
157
|
- lib/mammoth/errors.rb
|
|
141
158
|
- lib/mammoth/event_serializer.rb
|
|
@@ -144,6 +161,7 @@ files:
|
|
|
144
161
|
- lib/mammoth/sql/__bootstrap__.sql
|
|
145
162
|
- lib/mammoth/sqlite_store.rb
|
|
146
163
|
- lib/mammoth/status.rb
|
|
164
|
+
- lib/mammoth/transaction_envelope_serializer.rb
|
|
147
165
|
- lib/mammoth/version.rb
|
|
148
166
|
- lib/mammoth/webhook_sink.rb
|
|
149
167
|
homepage: https://kanutocd.github.io/mammoth/
|
|
@@ -153,7 +171,7 @@ metadata:
|
|
|
153
171
|
homepage_uri: https://kanutocd.github.io/mammoth/
|
|
154
172
|
source_code_uri: https://github.com/kanutocd/mammoth
|
|
155
173
|
changelog_uri: https://github.com/kanutocd/mammoth/blob/main/CHANGELOG.md
|
|
156
|
-
documentation_uri: https://kanutocd.github.io/mammoth/
|
|
174
|
+
documentation_uri: https://kanutocd.github.io/mammoth/Mammoth.html
|
|
157
175
|
rubygems_mfa_required: 'true'
|
|
158
176
|
rdoc_options: []
|
|
159
177
|
require_paths:
|
|
@@ -171,5 +189,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
171
189
|
requirements: []
|
|
172
190
|
rubygems_version: 4.0.10
|
|
173
191
|
specification_version: 4
|
|
174
|
-
summary: Reliable delivery of PostgreSQL
|
|
192
|
+
summary: Reliable delivery of PostgreSQL logical replication events with retries,
|
|
193
|
+
dead letters, and operational state.
|
|
175
194
|
test_files: []
|