mammoth 0.8.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -0
  3. data/README.md +93 -10
  4. data/config/mammoth.example.yml +19 -9
  5. data/config/mammoth.schema.json +3 -3
  6. data/lib/mammoth/application.rb +54 -43
  7. data/lib/mammoth/cli.rb +1 -13
  8. data/lib/mammoth/commands/bootstrap_command.rb +11 -7
  9. data/lib/mammoth/commands/dead_letters_command.rb +5 -3
  10. data/lib/mammoth/commands/deliver_sample_command.rb +1 -1
  11. data/lib/mammoth/commands/status_command.rb +5 -5
  12. data/lib/mammoth/concurrent_delivery_runtime.rb +15 -3
  13. data/lib/mammoth/dead_letter_commands.rb +16 -25
  14. data/lib/mammoth/dead_letter_store.rb +1 -1
  15. data/lib/mammoth/delivery_processor.rb +51 -9
  16. data/lib/mammoth/delivery_progress_coordinator.rb +153 -0
  17. data/lib/mammoth/delivery_worker.rb +19 -33
  18. data/lib/mammoth/dispatch_metrics.rb +55 -0
  19. data/lib/mammoth/event_serializer.rb +23 -12
  20. data/lib/mammoth/fanout_delivery_worker.rb +4 -4
  21. data/lib/mammoth/metrics_observer.rb +52 -0
  22. data/lib/mammoth/observability_metrics.rb +71 -0
  23. data/lib/mammoth/observability_server.rb +8 -6
  24. data/lib/mammoth/observability_snapshot.rb +118 -66
  25. data/lib/mammoth/operational_state/adapter.rb +16 -0
  26. data/lib/mammoth/operational_state/registry.rb +8 -0
  27. data/lib/mammoth/operational_state/sqlite_adapter.rb +18 -2
  28. data/lib/mammoth/persisted_payload_deserializer.rb +85 -0
  29. data/lib/mammoth/postgres_observability_metrics.rb +80 -0
  30. data/lib/mammoth/replication_consumer.rb +41 -52
  31. data/lib/mammoth/runtimes/batching_runtime.rb +59 -0
  32. data/lib/mammoth/runtimes/concurrent_adapter.rb +4 -2
  33. data/lib/mammoth/runtimes/inline_adapter.rb +19 -5
  34. data/lib/mammoth/runtimes/registry.rb +3 -2
  35. data/lib/mammoth/sources/postgres.rb +261 -4
  36. data/lib/mammoth/sources/postgres_publication_inspector.rb +214 -0
  37. data/lib/mammoth/sources/postgres_slot_health.rb +90 -0
  38. data/lib/mammoth/status.rb +22 -18
  39. data/lib/mammoth/transaction_envelope_serializer.rb +13 -15
  40. data/lib/mammoth/version.rb +1 -1
  41. data/lib/mammoth/webhook_sink.rb +2 -2
  42. data/lib/mammoth.rb +12 -1
  43. metadata +38 -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
@@ -3,23 +3,23 @@
3
3
  module Mammoth
4
4
  # Builds and prints a boring operational status snapshot.
5
5
  class Status
6
- attr_reader :config, :sqlite_store, :output
6
+ attr_reader :config, :state_adapter, :output
7
7
 
8
- # Print status for a configuration and optional SQLite store.
8
+ # Print status for a configuration and optional operational-state adapter.
9
9
  #
10
10
  # @param config [Mammoth::Configuration] loaded configuration
11
- # @param sqlite_store [Mammoth::SQLiteStore, nil] operational store
11
+ # @param state_adapter [Mammoth::OperationalState::Adapter, nil] operational state dependency
12
12
  # @return [void]
13
- def self.call(config, sqlite_store: nil, output: $stdout)
14
- new(config, sqlite_store: sqlite_store, output: output).call
13
+ def self.call(config, state_adapter: nil, output: $stdout)
14
+ new(config, state_adapter: state_adapter, output: output).call
15
15
  end
16
16
 
17
17
  # @param config [Mammoth::Configuration] loaded configuration
18
- # @param sqlite_store [Mammoth::SQLiteStore, nil] operational store
18
+ # @param state_adapter [Mammoth::OperationalState::Adapter, nil] operational state dependency
19
19
  # @param output [#puts] output stream
20
- def initialize(config, sqlite_store: nil, output: $stdout)
20
+ def initialize(config, state_adapter: nil, output: $stdout)
21
21
  @config = config
22
- @sqlite_store = sqlite_store
22
+ @state_adapter = state_adapter
23
23
  @output = output
24
24
  end
25
25
 
@@ -28,13 +28,13 @@ module Mammoth
28
28
  # @return [void]
29
29
  def call
30
30
  status_lines.each { |line| output.puts(line) }
31
- print_store_state if sqlite_store
31
+ print_store_state if state_adapter
32
32
  end
33
33
 
34
34
  private
35
35
 
36
36
  def status_lines
37
- identity_lines + replication_lines + runtime_lines + destination_lines + ["SQLite: #{sqlite_path}"]
37
+ identity_lines + replication_lines + runtime_lines + destination_lines
38
38
  end
39
39
 
40
40
  def identity_lines
@@ -70,10 +70,6 @@ module Mammoth
70
70
  ]
71
71
  end
72
72
 
73
- def sqlite_path
74
- config.dig("sqlite", "path")
75
- end
76
-
77
73
  def destination_names
78
74
  destinations = config.data["destinations"]
79
75
  return destinations.map { |destination| destination.fetch("name") } if destinations
@@ -90,10 +86,18 @@ module Mammoth
90
86
  end
91
87
 
92
88
  def print_store_state
93
- store = sqlite_store.bootstrap!
94
- output.puts "Tables: #{store.tables.join(", ")}"
95
- output.puts "Checkpoints: #{CheckpointStore.new(store).count}"
96
- output.puts "Dead Letters: #{DeadLetterStore.new(store).count}"
89
+ output.puts "Operational state ready: #{state_adapter.ready?}"
90
+ state_adapter.summary.each do |key, value|
91
+ output.puts "#{status_label(key)}: #{format_status_value(value)}"
92
+ end
93
+ end
94
+
95
+ def status_label(key)
96
+ key.to_s.split("_").map(&:capitalize).join(" ")
97
+ end
98
+
99
+ def format_status_value(value)
100
+ value.is_a?(Array) ? value.join(", ") : value
97
101
  end
98
102
  end
99
103
  end
@@ -15,16 +15,20 @@ module Mammoth
15
15
  # Default payload type for transaction webhook delivery.
16
16
  PAYLOAD_TYPE = "transaction.committed"
17
17
 
18
- # Serialize a CDC::Core::TransactionEnvelope-like object into a Hash.
18
+ # Serialize a CDC::Core::TransactionEnvelope into a Hash.
19
19
  #
20
- # @param envelope [#events, #transaction_id] transaction envelope
20
+ # @param envelope [CDC::Core::TransactionEnvelope] transaction envelope
21
21
  # @return [Hash] webhook-ready transaction payload
22
22
  def self.call(envelope)
23
23
  new(envelope).call
24
24
  end
25
25
 
26
- # @param envelope [#events, #transaction_id] transaction envelope
26
+ # @param envelope [CDC::Core::TransactionEnvelope] transaction envelope
27
27
  def initialize(envelope)
28
+ unless envelope.is_a?(CDC::Core::TransactionEnvelope)
29
+ raise ArgumentError, "envelope must be a CDC::Core::TransactionEnvelope"
30
+ end
31
+
28
32
  @envelope = envelope
29
33
  end
30
34
 
@@ -34,7 +38,7 @@ module Mammoth
34
38
  def call
35
39
  event_payloads = envelope.events.map { |event| EventSerializer.call(event) }
36
40
  {
37
- "event_id" => envelope_value("event_id") || SecureRandom.uuid,
41
+ "event_id" => envelope_metadata["event_id"] || SecureRandom.uuid,
38
42
  "type" => PAYLOAD_TYPE,
39
43
  "source" => first_event_value(event_payloads, "source") || EventSerializer::DEFAULT_SOURCE,
40
44
  "transaction_id" => envelope.transaction_id,
@@ -43,7 +47,7 @@ module Mammoth
43
47
  "committed_at" => committed_at,
44
48
  "event_count" => event_payloads.length,
45
49
  "events" => event_payloads,
46
- "metadata" => envelope_value("metadata") || {}
50
+ "metadata" => envelope_metadata
47
51
  }
48
52
  end
49
53
 
@@ -58,27 +62,21 @@ module Mammoth
58
62
 
59
63
  attr_reader :envelope
60
64
 
61
- def envelope_hash
62
- @envelope_hash ||= envelope.respond_to?(:to_h) ? stringify_keys(envelope.to_h) : {}
63
- end
64
-
65
65
  def stringify_keys(hash)
66
66
  hash.to_h.transform_keys(&:to_s)
67
67
  end
68
68
 
69
- def envelope_value(key)
70
- return envelope.public_send(key) if envelope.respond_to?(key)
71
-
72
- envelope_hash[key]
69
+ def envelope_metadata
70
+ @envelope_metadata ||= stringify_keys(envelope.metadata)
73
71
  end
74
72
 
75
73
  def source_position(event_payloads)
76
- envelope_value("source_position") || envelope_value("commit_lsn") ||
74
+ envelope.commit_lsn ||
77
75
  first_event_value(event_payloads.reverse, "source_position")
78
76
  end
79
77
 
80
78
  def committed_at
81
- value = envelope_value("committed_at") || envelope_value("commit_time")
79
+ value = envelope.committed_at
82
80
  return value.iso8601 if value.respond_to?(:iso8601)
83
81
 
84
82
  value || Time.now.utc.iso8601
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Mammoth
4
4
  # Current Mammoth gem version.
5
- VERSION = "0.8.0"
5
+ VERSION = "1.0.0"
6
6
  end
@@ -94,7 +94,7 @@ module Mammoth
94
94
 
95
95
  # Deliver an event to the webhook endpoint.
96
96
  #
97
- # @param event [Hash, #to_h] normalized event
97
+ # @param event [CDC::Core::ChangeEvent] normalized event
98
98
  # @return [Hash] delivery result
99
99
  # @raise [Mammoth::DeliveryError] when delivery fails
100
100
  def deliver(event)
@@ -103,7 +103,7 @@ module Mammoth
103
103
 
104
104
  # Deliver a transaction envelope to the webhook endpoint.
105
105
  #
106
- # @param envelope [#events, #transaction_id] CDC transaction envelope
106
+ # @param envelope [CDC::Core::TransactionEnvelope] CDC transaction envelope
107
107
  # @return [Hash] delivery result
108
108
  # @raise [Mammoth::DeliveryError] when delivery fails
109
109
  def deliver_transaction(envelope)
data/lib/mammoth.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cdc/core"
4
+
3
5
  require_relative "mammoth/version"
4
6
  require_relative "mammoth/errors"
5
7
  require_relative "mammoth/registry"
@@ -14,14 +16,22 @@ require_relative "mammoth/commands/status_command"
14
16
  require_relative "mammoth/commands/start_command"
15
17
  require_relative "mammoth/commands/deliver_sample_command"
16
18
  require_relative "mammoth/commands/dead_letters_command"
19
+ require_relative "mammoth/dispatch_metrics"
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"
24
+ require_relative "mammoth/observability_metrics"
17
25
  require_relative "mammoth/observability_snapshot"
18
26
  require_relative "mammoth/observability_server"
19
27
  require_relative "mammoth/sqlite_store"
20
28
  require_relative "mammoth/checkpoint_store"
21
29
  require_relative "mammoth/dead_letter_store"
22
30
  require_relative "mammoth/delivered_envelope_store"
31
+ require_relative "mammoth/delivery_progress_coordinator"
23
32
  require_relative "mammoth/event_serializer"
24
33
  require_relative "mammoth/transaction_envelope_serializer"
34
+ require_relative "mammoth/persisted_payload_deserializer"
25
35
  require_relative "mammoth/route_filter"
26
36
  require_relative "mammoth/webhook_sink"
27
37
  require_relative "mammoth/operational_state/adapter"
@@ -37,6 +47,7 @@ require_relative "mammoth/concurrent_delivery_runtime"
37
47
  require_relative "mammoth/runtimes/adapter"
38
48
  require_relative "mammoth/runtimes/inline_adapter"
39
49
  require_relative "mammoth/runtimes/concurrent_adapter"
50
+ require_relative "mammoth/runtimes/batching_runtime"
40
51
  require_relative "mammoth/runtimes/registry"
41
52
  require_relative "mammoth/sources/postgres"
42
53
  require_relative "mammoth/cdc_source"
@@ -47,7 +58,7 @@ require_relative "mammoth/cli"
47
58
 
48
59
  # Mammoth is a self-hosted PostgreSQL event relay.
49
60
  #
50
- # Mammoth 0.7.x is a single-node PostgreSQL CDC relay with webhook fanout,
61
+ # Mammoth 1.x is a single-node PostgreSQL CDC relay with webhook fanout,
51
62
  # replayable operational SQLite state, health/metrics endpoints, and explicit
52
63
  # extension contracts for state, destination, runtime, lifecycle, configuration,
53
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.8.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.2'
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.2'
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.1'
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.1'
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.2'
122
+ version: '0.3'
103
123
  - - ">="
104
124
  - !ruby/object:Gem::Version
105
- version: 0.2.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.2'
132
+ version: '0.3'
113
133
  - - ">="
114
134
  - !ruby/object:Gem::Version
115
- version: 0.2.0
135
+ version: 0.3.0
116
136
  - !ruby/object:Gem::Dependency
117
137
  name: sqlite3
118
138
  requirement: !ruby/object:Gem::Requirement
@@ -181,28 +201,37 @@ 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
187
208
  - lib/mammoth/destinations/webhook_adapter.rb
209
+ - lib/mammoth/dispatch_metrics.rb
188
210
  - lib/mammoth/errors.rb
189
211
  - lib/mammoth/event_serializer.rb
190
212
  - lib/mammoth/fanout_delivery_worker.rb
191
213
  - lib/mammoth/lifecycle_hooks.rb
214
+ - lib/mammoth/metrics_observer.rb
192
215
  - lib/mammoth/node_identity.rb
216
+ - lib/mammoth/observability_metrics.rb
193
217
  - lib/mammoth/observability_server.rb
194
218
  - lib/mammoth/observability_snapshot.rb
195
219
  - lib/mammoth/operational_state/adapter.rb
196
220
  - lib/mammoth/operational_state/registry.rb
197
221
  - lib/mammoth/operational_state/sqlite_adapter.rb
222
+ - lib/mammoth/persisted_payload_deserializer.rb
223
+ - lib/mammoth/postgres_observability_metrics.rb
198
224
  - lib/mammoth/registry.rb
199
225
  - lib/mammoth/replication_consumer.rb
200
226
  - lib/mammoth/route_filter.rb
201
227
  - lib/mammoth/runtimes/adapter.rb
228
+ - lib/mammoth/runtimes/batching_runtime.rb
202
229
  - lib/mammoth/runtimes/concurrent_adapter.rb
203
230
  - lib/mammoth/runtimes/inline_adapter.rb
204
231
  - lib/mammoth/runtimes/registry.rb
205
232
  - lib/mammoth/sources/postgres.rb
233
+ - lib/mammoth/sources/postgres_publication_inspector.rb
234
+ - lib/mammoth/sources/postgres_slot_health.rb
206
235
  - lib/mammoth/sql/__bootstrap__.sql
207
236
  - lib/mammoth/sqlite_store.rb
208
237
  - lib/mammoth/status.rb