mammoth 0.9.0 → 1.0.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 +66 -0
- data/README.md +69 -7
- data/config/mammoth.example.yml +19 -9
- data/config/mammoth.schema.json +2 -2
- data/lib/mammoth/application.rb +34 -3
- data/lib/mammoth/delivery_processor.rb +5 -2
- data/lib/mammoth/delivery_progress_coordinator.rb +153 -0
- data/lib/mammoth/delivery_worker.rb +9 -25
- data/lib/mammoth/observability_metrics.rb +3 -1
- data/lib/mammoth/observability_server.rb +5 -3
- data/lib/mammoth/observability_snapshot.rb +63 -10
- data/lib/mammoth/postgres_observability_metrics.rb +80 -0
- data/lib/mammoth/replication_consumer.rb +28 -7
- data/lib/mammoth/sources/postgres.rb +252 -4
- data/lib/mammoth/sources/postgres_publication_inspector.rb +214 -0
- data/lib/mammoth/sources/postgres_slot_health.rb +90 -0
- data/lib/mammoth/version.rb +1 -1
- data/lib/mammoth.rb +5 -1
- metadata +33 -9
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Mammoth
|
|
6
|
+
module Sources
|
|
7
|
+
# Immutable replica-identity facts for one table in configured publications.
|
|
8
|
+
class PostgresPublicationTable
|
|
9
|
+
attr_reader :relation_id, :schema_name, :table_name, :publishes_updates, :publishes_deletes, :replica_identity,
|
|
10
|
+
:replica_identity_columns, :primary_key_usable, :replica_identity_index_usable
|
|
11
|
+
|
|
12
|
+
# @return [void]
|
|
13
|
+
def initialize(relation_id:, schema_name:, table_name:, publishes_updates:, publishes_deletes:,
|
|
14
|
+
replica_identity:, replica_identity_columns:, primary_key_usable:,
|
|
15
|
+
replica_identity_index_usable:)
|
|
16
|
+
@relation_id = relation_id
|
|
17
|
+
@schema_name = schema_name
|
|
18
|
+
@table_name = table_name
|
|
19
|
+
@publishes_updates = publishes_updates
|
|
20
|
+
@publishes_deletes = publishes_deletes
|
|
21
|
+
@replica_identity = replica_identity
|
|
22
|
+
@replica_identity_columns = replica_identity_columns.freeze
|
|
23
|
+
@primary_key_usable = primary_key_usable
|
|
24
|
+
@replica_identity_index_usable = replica_identity_index_usable
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Schema-qualified table name for diagnostics.
|
|
29
|
+
#
|
|
30
|
+
# @return [String]
|
|
31
|
+
def qualified_name
|
|
32
|
+
"#{schema_name}.#{table_name}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Publication actions that require old-row identity.
|
|
36
|
+
#
|
|
37
|
+
# @return [Array<String>]
|
|
38
|
+
def identity_actions
|
|
39
|
+
actions = [] # : Array[String]
|
|
40
|
+
actions.tap do
|
|
41
|
+
actions << "UPDATE" if publishes_updates
|
|
42
|
+
actions << "DELETE" if publishes_deletes
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Whether any configured publication requires replica identity.
|
|
47
|
+
#
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
def identity_required?
|
|
50
|
+
publishes_updates || publishes_deletes
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Whether PostgreSQL can identify old rows for the published actions.
|
|
54
|
+
#
|
|
55
|
+
# `d` uses a primary key, `i` uses a selected replica-identity index,
|
|
56
|
+
# and `f` logs the full old row. `n` provides no old-row identity.
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
def identity_usable?
|
|
60
|
+
return true unless identity_required?
|
|
61
|
+
|
|
62
|
+
case replica_identity
|
|
63
|
+
when "d" then primary_key_usable
|
|
64
|
+
when "i" then replica_identity_index_usable
|
|
65
|
+
when "f" then true
|
|
66
|
+
else false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Read-only PostgreSQL publication and replica-identity catalog inspector.
|
|
72
|
+
class PostgresPublicationInspector
|
|
73
|
+
QUERY = <<~SQL
|
|
74
|
+
SELECT
|
|
75
|
+
relation.oid AS relation_id,
|
|
76
|
+
namespace.nspname AS schema_name,
|
|
77
|
+
relation.relname AS table_name,
|
|
78
|
+
bool_or(publication.pubupdate) AS publishes_updates,
|
|
79
|
+
bool_or(publication.pubdelete) AS publishes_deletes,
|
|
80
|
+
relation.relreplident AS replica_identity,
|
|
81
|
+
CASE relation.relreplident
|
|
82
|
+
WHEN 'd' THEN (
|
|
83
|
+
SELECT COALESCE(jsonb_agg(attribute.attname ORDER BY key.ordinality), '[]'::jsonb)
|
|
84
|
+
FROM pg_index AS identity_columns_index
|
|
85
|
+
JOIN LATERAL unnest(identity_columns_index.indkey)
|
|
86
|
+
WITH ORDINALITY AS key(attribute_number, ordinality) ON true
|
|
87
|
+
JOIN pg_attribute AS attribute
|
|
88
|
+
ON attribute.attrelid = relation.oid
|
|
89
|
+
AND attribute.attnum = key.attribute_number
|
|
90
|
+
WHERE identity_columns_index.indrelid = relation.oid
|
|
91
|
+
AND identity_columns_index.indisprimary
|
|
92
|
+
AND identity_columns_index.indisvalid
|
|
93
|
+
AND identity_columns_index.indisready
|
|
94
|
+
AND identity_columns_index.indislive
|
|
95
|
+
)
|
|
96
|
+
WHEN 'i' THEN (
|
|
97
|
+
SELECT COALESCE(jsonb_agg(attribute.attname ORDER BY key.ordinality), '[]'::jsonb)
|
|
98
|
+
FROM pg_index AS identity_columns_index
|
|
99
|
+
JOIN LATERAL unnest(identity_columns_index.indkey)
|
|
100
|
+
WITH ORDINALITY AS key(attribute_number, ordinality) ON true
|
|
101
|
+
JOIN pg_attribute AS attribute
|
|
102
|
+
ON attribute.attrelid = relation.oid
|
|
103
|
+
AND attribute.attnum = key.attribute_number
|
|
104
|
+
WHERE identity_columns_index.indrelid = relation.oid
|
|
105
|
+
AND identity_columns_index.indisreplident
|
|
106
|
+
AND identity_columns_index.indisvalid
|
|
107
|
+
AND identity_columns_index.indisready
|
|
108
|
+
AND identity_columns_index.indislive
|
|
109
|
+
)
|
|
110
|
+
WHEN 'f' THEN (
|
|
111
|
+
SELECT COALESCE(jsonb_agg(attribute.attname ORDER BY attribute.attnum), '[]'::jsonb)
|
|
112
|
+
FROM pg_attribute AS attribute
|
|
113
|
+
WHERE attribute.attrelid = relation.oid
|
|
114
|
+
AND attribute.attnum > 0
|
|
115
|
+
AND NOT attribute.attisdropped
|
|
116
|
+
)
|
|
117
|
+
ELSE '[]'::jsonb
|
|
118
|
+
END AS replica_identity_columns,
|
|
119
|
+
EXISTS (
|
|
120
|
+
SELECT 1
|
|
121
|
+
FROM pg_index AS primary_index
|
|
122
|
+
WHERE primary_index.indrelid = relation.oid
|
|
123
|
+
AND primary_index.indisprimary
|
|
124
|
+
AND primary_index.indisvalid
|
|
125
|
+
AND primary_index.indisready
|
|
126
|
+
AND primary_index.indislive
|
|
127
|
+
) AS primary_key_usable,
|
|
128
|
+
EXISTS (
|
|
129
|
+
SELECT 1
|
|
130
|
+
FROM pg_index AS identity_index
|
|
131
|
+
WHERE identity_index.indrelid = relation.oid
|
|
132
|
+
AND identity_index.indisreplident
|
|
133
|
+
AND identity_index.indisvalid
|
|
134
|
+
AND identity_index.indisready
|
|
135
|
+
AND identity_index.indislive
|
|
136
|
+
) AS replica_identity_index_usable
|
|
137
|
+
FROM pg_publication_tables AS publication_table
|
|
138
|
+
JOIN pg_publication AS publication
|
|
139
|
+
ON publication.pubname = publication_table.pubname
|
|
140
|
+
JOIN pg_namespace AS namespace
|
|
141
|
+
ON namespace.nspname = publication_table.schemaname
|
|
142
|
+
JOIN pg_class AS relation
|
|
143
|
+
ON relation.relnamespace = namespace.oid
|
|
144
|
+
AND relation.relname = publication_table.tablename
|
|
145
|
+
WHERE publication.pubname IN (
|
|
146
|
+
SELECT jsonb_array_elements_text($1::jsonb)
|
|
147
|
+
)
|
|
148
|
+
GROUP BY namespace.nspname, relation.relname, relation.oid, relation.relreplident
|
|
149
|
+
ORDER BY namespace.nspname, relation.relname
|
|
150
|
+
SQL
|
|
151
|
+
|
|
152
|
+
attr_reader :database_url
|
|
153
|
+
|
|
154
|
+
# @param database_url [String] PostgreSQL connection URL
|
|
155
|
+
# @param connection_factory [#call, nil] optional connection factory for tests
|
|
156
|
+
def initialize(database_url:, connection_factory: nil)
|
|
157
|
+
@database_url = database_url
|
|
158
|
+
@connection_factory = connection_factory
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Inspect tables included by the configured publications.
|
|
162
|
+
#
|
|
163
|
+
# @param publication_names [Array<String>] configured publication names
|
|
164
|
+
# @return [Array<PostgresPublicationTable>]
|
|
165
|
+
def inspect(publication_names)
|
|
166
|
+
connection = open_connection
|
|
167
|
+
rows = connection.exec_params(QUERY, [JSON.generate(publication_names)])
|
|
168
|
+
rows.map { |row| build_table(row) }
|
|
169
|
+
rescue pg_error_class => e
|
|
170
|
+
raise ReplicationError, "PostgreSQL publication inspection failed: #{e.message}"
|
|
171
|
+
ensure
|
|
172
|
+
connection&.close unless connection&.finished?
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def open_connection
|
|
178
|
+
return @connection_factory.call(database_url) if @connection_factory
|
|
179
|
+
|
|
180
|
+
require "pg"
|
|
181
|
+
PG.connect(database_url)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def build_table(row)
|
|
185
|
+
PostgresPublicationTable.new(
|
|
186
|
+
relation_id: Integer(row.fetch("relation_id")),
|
|
187
|
+
schema_name: row.fetch("schema_name"),
|
|
188
|
+
table_name: row.fetch("table_name"),
|
|
189
|
+
publishes_updates: postgres_boolean(row.fetch("publishes_updates")),
|
|
190
|
+
publishes_deletes: postgres_boolean(row.fetch("publishes_deletes")),
|
|
191
|
+
replica_identity: row.fetch("replica_identity"),
|
|
192
|
+
replica_identity_columns: parse_columns(row.fetch("replica_identity_columns")),
|
|
193
|
+
primary_key_usable: postgres_boolean(row.fetch("primary_key_usable")),
|
|
194
|
+
replica_identity_index_usable: postgres_boolean(row.fetch("replica_identity_index_usable"))
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def parse_columns(value)
|
|
199
|
+
columns = value.is_a?(String) ? JSON.parse(value) : value
|
|
200
|
+
column_list = Array(columns) # : Array[untyped]
|
|
201
|
+
column_list.map(&:to_s)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def postgres_boolean(value)
|
|
205
|
+
[true, "t"].include?(value)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def pg_error_class
|
|
209
|
+
require "pg"
|
|
210
|
+
PG::Error
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mammoth
|
|
4
|
+
module Sources
|
|
5
|
+
# Immutable operator-facing health snapshot for one PostgreSQL replication slot.
|
|
6
|
+
#
|
|
7
|
+
# This is a Mammoth-owned policy view. The transport layer supplies catalog
|
|
8
|
+
# facts; Mammoth decides whether they make the configured source ready for
|
|
9
|
+
# continuous delivery.
|
|
10
|
+
# Typed PostgreSQL slot health and readiness policy.
|
|
11
|
+
class PostgresSlotHealth
|
|
12
|
+
attr_reader :slot_name, :present, :active, :retained_wal_bytes, :wal_status, :safe_wal_size, :inactive_since,
|
|
13
|
+
:invalidation_reason, :restart_lsn, :restart_lsn_bytes, :confirmed_flush_lsn,
|
|
14
|
+
:confirmed_flush_lsn_bytes, :conflicting
|
|
15
|
+
|
|
16
|
+
# Build a PostgreSQL slot health snapshot from normalized catalog facts.
|
|
17
|
+
#
|
|
18
|
+
# @return [void]
|
|
19
|
+
def initialize(slot_name:, present:, active:, retained_wal_bytes:, wal_status:, safe_wal_size:, inactive_since:,
|
|
20
|
+
invalidation_reason:, restart_lsn:, restart_lsn_bytes:, confirmed_flush_lsn:,
|
|
21
|
+
confirmed_flush_lsn_bytes:, conflicting:)
|
|
22
|
+
@slot_name = slot_name
|
|
23
|
+
@present = present
|
|
24
|
+
@active = active
|
|
25
|
+
@retained_wal_bytes = retained_wal_bytes
|
|
26
|
+
@wal_status = wal_status
|
|
27
|
+
@safe_wal_size = safe_wal_size
|
|
28
|
+
@inactive_since = inactive_since
|
|
29
|
+
@invalidation_reason = invalidation_reason
|
|
30
|
+
@restart_lsn = restart_lsn
|
|
31
|
+
@restart_lsn_bytes = restart_lsn_bytes
|
|
32
|
+
@confirmed_flush_lsn = confirmed_flush_lsn
|
|
33
|
+
@confirmed_flush_lsn_bytes = confirmed_flush_lsn_bytes
|
|
34
|
+
@conflicting = conflicting
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Build a snapshot for a missing configured slot.
|
|
39
|
+
#
|
|
40
|
+
# @param slot_name [String] configured replication slot
|
|
41
|
+
# @return [PostgresSlotHealth]
|
|
42
|
+
def self.missing(slot_name)
|
|
43
|
+
new(
|
|
44
|
+
slot_name:, present: false, active: false, retained_wal_bytes: nil,
|
|
45
|
+
wal_status: nil, safe_wal_size: nil, inactive_since: nil,
|
|
46
|
+
invalidation_reason: nil, restart_lsn: nil, restart_lsn_bytes: nil,
|
|
47
|
+
confirmed_flush_lsn: nil, confirmed_flush_lsn_bytes: nil, conflicting: false
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Whether the slot is present, active, and retaining usable WAL.
|
|
52
|
+
#
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
def ready?
|
|
55
|
+
reason.nil?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Operator-facing reason the slot is not ready.
|
|
59
|
+
#
|
|
60
|
+
# @return [String, nil]
|
|
61
|
+
def reason
|
|
62
|
+
return "slot is missing" unless present
|
|
63
|
+
return "slot is inactive" unless active
|
|
64
|
+
return "wal_status=#{wal_status}" if %w[lost unreserved].include?(wal_status)
|
|
65
|
+
return "slot is invalidated: #{invalidation_reason}" unless blank?(invalidation_reason)
|
|
66
|
+
return "slot is conflicting" if conflicting
|
|
67
|
+
return "slot has no restart LSN" if blank?(restart_lsn)
|
|
68
|
+
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Stable readiness payload without transport-library types.
|
|
73
|
+
#
|
|
74
|
+
# @return [Hash]
|
|
75
|
+
def summary
|
|
76
|
+
{
|
|
77
|
+
slot_name:, present:, active:, retained_wal_bytes:, wal_status:, safe_wal_size:, inactive_since:,
|
|
78
|
+
invalidation_reason:, restart_lsn:, restart_lsn_bytes:, confirmed_flush_lsn:,
|
|
79
|
+
confirmed_flush_lsn_bytes:, conflicting:, ready: ready?, reason:
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def blank?(value)
|
|
86
|
+
value.nil? || value == ""
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/mammoth/version.rb
CHANGED
data/lib/mammoth.rb
CHANGED
|
@@ -18,6 +18,9 @@ require_relative "mammoth/commands/deliver_sample_command"
|
|
|
18
18
|
require_relative "mammoth/commands/dead_letters_command"
|
|
19
19
|
require_relative "mammoth/dispatch_metrics"
|
|
20
20
|
require_relative "mammoth/metrics_observer"
|
|
21
|
+
require_relative "mammoth/sources/postgres_slot_health"
|
|
22
|
+
require_relative "mammoth/sources/postgres_publication_inspector"
|
|
23
|
+
require_relative "mammoth/postgres_observability_metrics"
|
|
21
24
|
require_relative "mammoth/observability_metrics"
|
|
22
25
|
require_relative "mammoth/observability_snapshot"
|
|
23
26
|
require_relative "mammoth/observability_server"
|
|
@@ -25,6 +28,7 @@ require_relative "mammoth/sqlite_store"
|
|
|
25
28
|
require_relative "mammoth/checkpoint_store"
|
|
26
29
|
require_relative "mammoth/dead_letter_store"
|
|
27
30
|
require_relative "mammoth/delivered_envelope_store"
|
|
31
|
+
require_relative "mammoth/delivery_progress_coordinator"
|
|
28
32
|
require_relative "mammoth/event_serializer"
|
|
29
33
|
require_relative "mammoth/transaction_envelope_serializer"
|
|
30
34
|
require_relative "mammoth/persisted_payload_deserializer"
|
|
@@ -54,7 +58,7 @@ require_relative "mammoth/cli"
|
|
|
54
58
|
|
|
55
59
|
# Mammoth is a self-hosted PostgreSQL event relay.
|
|
56
60
|
#
|
|
57
|
-
# Mammoth
|
|
61
|
+
# Mammoth 1.x is a single-node PostgreSQL CDC relay with webhook fanout,
|
|
58
62
|
# replayable operational SQLite state, health/metrics endpoints, and explicit
|
|
59
63
|
# extension contracts for state, destination, runtime, lifecycle, configuration,
|
|
60
64
|
# and local command integrations.
|
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: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
@@ -51,34 +51,54 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '6.2'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: pg
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.6'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.6'
|
|
54
68
|
- !ruby/object:Gem::Dependency
|
|
55
69
|
name: pgoutput-client
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
57
71
|
requirements:
|
|
58
72
|
- - "~>"
|
|
59
73
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0.
|
|
74
|
+
version: '0.4'
|
|
61
75
|
type: :runtime
|
|
62
76
|
prerelease: false
|
|
63
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
78
|
requirements:
|
|
65
79
|
- - "~>"
|
|
66
80
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '0.
|
|
81
|
+
version: '0.4'
|
|
68
82
|
- !ruby/object:Gem::Dependency
|
|
69
83
|
name: pgoutput-decoder
|
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
|
71
85
|
requirements:
|
|
72
86
|
- - "~>"
|
|
73
87
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '0.
|
|
88
|
+
version: '0.2'
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: 0.2.0
|
|
75
92
|
type: :runtime
|
|
76
93
|
prerelease: false
|
|
77
94
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
95
|
requirements:
|
|
79
96
|
- - "~>"
|
|
80
97
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: '0.
|
|
98
|
+
version: '0.2'
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: 0.2.0
|
|
82
102
|
- !ruby/object:Gem::Dependency
|
|
83
103
|
name: pgoutput-parser
|
|
84
104
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -99,20 +119,20 @@ dependencies:
|
|
|
99
119
|
requirements:
|
|
100
120
|
- - "~>"
|
|
101
121
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: '0.
|
|
122
|
+
version: '0.3'
|
|
103
123
|
- - ">="
|
|
104
124
|
- !ruby/object:Gem::Version
|
|
105
|
-
version: 0.
|
|
125
|
+
version: 0.3.0
|
|
106
126
|
type: :runtime
|
|
107
127
|
prerelease: false
|
|
108
128
|
version_requirements: !ruby/object:Gem::Requirement
|
|
109
129
|
requirements:
|
|
110
130
|
- - "~>"
|
|
111
131
|
- !ruby/object:Gem::Version
|
|
112
|
-
version: '0.
|
|
132
|
+
version: '0.3'
|
|
113
133
|
- - ">="
|
|
114
134
|
- !ruby/object:Gem::Version
|
|
115
|
-
version: 0.
|
|
135
|
+
version: 0.3.0
|
|
116
136
|
- !ruby/object:Gem::Dependency
|
|
117
137
|
name: sqlite3
|
|
118
138
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -181,6 +201,7 @@ files:
|
|
|
181
201
|
- lib/mammoth/dead_letter_store.rb
|
|
182
202
|
- lib/mammoth/delivered_envelope_store.rb
|
|
183
203
|
- lib/mammoth/delivery_processor.rb
|
|
204
|
+
- lib/mammoth/delivery_progress_coordinator.rb
|
|
184
205
|
- lib/mammoth/delivery_worker.rb
|
|
185
206
|
- lib/mammoth/destinations/adapter.rb
|
|
186
207
|
- lib/mammoth/destinations/registry.rb
|
|
@@ -199,6 +220,7 @@ files:
|
|
|
199
220
|
- lib/mammoth/operational_state/registry.rb
|
|
200
221
|
- lib/mammoth/operational_state/sqlite_adapter.rb
|
|
201
222
|
- lib/mammoth/persisted_payload_deserializer.rb
|
|
223
|
+
- lib/mammoth/postgres_observability_metrics.rb
|
|
202
224
|
- lib/mammoth/registry.rb
|
|
203
225
|
- lib/mammoth/replication_consumer.rb
|
|
204
226
|
- lib/mammoth/route_filter.rb
|
|
@@ -208,6 +230,8 @@ files:
|
|
|
208
230
|
- lib/mammoth/runtimes/inline_adapter.rb
|
|
209
231
|
- lib/mammoth/runtimes/registry.rb
|
|
210
232
|
- lib/mammoth/sources/postgres.rb
|
|
233
|
+
- lib/mammoth/sources/postgres_publication_inspector.rb
|
|
234
|
+
- lib/mammoth/sources/postgres_slot_health.rb
|
|
211
235
|
- lib/mammoth/sql/__bootstrap__.sql
|
|
212
236
|
- lib/mammoth/sqlite_store.rb
|
|
213
237
|
- lib/mammoth/status.rb
|