mammoth 0.7.2 → 0.9.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +38 -7
  4. data/config/mammoth.schema.json +1 -1
  5. data/lib/mammoth/application.rb +20 -40
  6. data/lib/mammoth/cli.rb +1 -13
  7. data/lib/mammoth/commands/bootstrap_command.rb +11 -7
  8. data/lib/mammoth/commands/dead_letters_command.rb +5 -3
  9. data/lib/mammoth/commands/deliver_sample_command.rb +1 -1
  10. data/lib/mammoth/commands/status_command.rb +5 -5
  11. data/lib/mammoth/concurrent_delivery_runtime.rb +15 -3
  12. data/lib/mammoth/dead_letter_commands.rb +16 -25
  13. data/lib/mammoth/dead_letter_store.rb +1 -1
  14. data/lib/mammoth/delivery_processor.rb +46 -7
  15. data/lib/mammoth/delivery_worker.rb +10 -8
  16. data/lib/mammoth/dispatch_metrics.rb +55 -0
  17. data/lib/mammoth/event_serializer.rb +23 -12
  18. data/lib/mammoth/fanout_delivery_worker.rb +4 -4
  19. data/lib/mammoth/metrics_observer.rb +52 -0
  20. data/lib/mammoth/observability_metrics.rb +69 -0
  21. data/lib/mammoth/observability_server.rb +6 -6
  22. data/lib/mammoth/observability_snapshot.rb +59 -60
  23. data/lib/mammoth/operational_state/adapter.rb +16 -0
  24. data/lib/mammoth/operational_state/registry.rb +8 -0
  25. data/lib/mammoth/operational_state/sqlite_adapter.rb +18 -2
  26. data/lib/mammoth/persisted_payload_deserializer.rb +85 -0
  27. data/lib/mammoth/replication_consumer.rb +15 -47
  28. data/lib/mammoth/runtimes/batching_runtime.rb +59 -0
  29. data/lib/mammoth/runtimes/concurrent_adapter.rb +4 -2
  30. data/lib/mammoth/runtimes/inline_adapter.rb +19 -5
  31. data/lib/mammoth/runtimes/registry.rb +3 -2
  32. data/lib/mammoth/sources/postgres.rb +39 -129
  33. data/lib/mammoth/status.rb +22 -18
  34. data/lib/mammoth/transaction_envelope_serializer.rb +13 -15
  35. data/lib/mammoth/version.rb +1 -1
  36. data/lib/mammoth/webhook_sink.rb +2 -2
  37. data/lib/mammoth.rb +8 -1
  38. metadata +14 -3
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Mammoth
6
+ # Reconstructs exact CDC core work items from persisted webhook payloads.
7
+ #
8
+ # Dead letters and sample files contain Mammoth's JSON delivery projection,
9
+ # not live CDC objects. This class is the explicit boundary that converts
10
+ # those persisted representations back into core vocabulary.
11
+ class PersistedPayloadDeserializer
12
+ class << self
13
+ # Deserialize one persisted event payload.
14
+ #
15
+ # @param payload [Hash] Mammoth event payload
16
+ # @return [CDC::Core::ChangeEvent] reconstructed core event
17
+ def event(payload)
18
+ attributes = stringify_keys(payload)
19
+ data = attributes["data"]
20
+ metadata = enriched_metadata(attributes)
21
+
22
+ CDC::Core::ChangeEvent.new(
23
+ operation: attributes.fetch("operation"),
24
+ schema: attributes["namespace"] || attributes.fetch("schema"),
25
+ table: attributes["entity"] || attributes.fetch("table"),
26
+ old_values: attributes["old_values"] || delete_values(attributes, data),
27
+ new_values: attributes["new_values"] || changed_values(attributes, data),
28
+ primary_key: attributes["identity"] || attributes["primary_key"],
29
+ transaction_id: attributes["transaction_id"],
30
+ commit_lsn: attributes["commit_lsn"] || attributes["source_position"],
31
+ sequence_number: attributes["sequence_number"],
32
+ occurred_at: parse_time(attributes["occurred_at"]),
33
+ metadata: metadata
34
+ )
35
+ rescue KeyError, ArgumentError, TypeError => e
36
+ raise ConfigurationError, "invalid persisted CDC event: #{e.message}"
37
+ end
38
+
39
+ # Deserialize one persisted transaction payload.
40
+ #
41
+ # @param payload [Hash] Mammoth transaction webhook payload
42
+ # @return [CDC::Core::TransactionEnvelope] reconstructed core transaction
43
+ def transaction(payload)
44
+ attributes = stringify_keys(payload)
45
+ CDC::Core::TransactionEnvelope.new(
46
+ transaction_id: attributes.fetch("transaction_id"),
47
+ events: attributes.fetch("events").map { |event_payload| event(event_payload) },
48
+ commit_lsn: attributes["commit_lsn"] || attributes["source_position"],
49
+ committed_at: parse_time(attributes["committed_at"]),
50
+ metadata: enriched_metadata(attributes)
51
+ )
52
+ rescue KeyError, ArgumentError, TypeError => e
53
+ raise ConfigurationError, "invalid persisted CDC transaction: #{e.message}"
54
+ end
55
+
56
+ private
57
+
58
+ def stringify_keys(payload)
59
+ payload.to_h.transform_keys(&:to_s)
60
+ end
61
+
62
+ def enriched_metadata(attributes)
63
+ metadata = stringify_keys(attributes["metadata"] || {})
64
+ %w[event_id source source_position changes type].each do |key|
65
+ metadata[key] = attributes[key] if attributes.key?(key)
66
+ end
67
+ metadata
68
+ end
69
+
70
+ def delete_values(attributes, data)
71
+ data if attributes["operation"].to_s == "delete"
72
+ end
73
+
74
+ def changed_values(attributes, data)
75
+ data unless attributes["operation"].to_s == "delete"
76
+ end
77
+
78
+ def parse_time(value)
79
+ return value if value.nil? || value.is_a?(Time)
80
+
81
+ Time.iso8601(value.to_s)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -20,7 +20,7 @@ module Mammoth
20
20
 
21
21
  # Consume normalized CDC work from the configured source.
22
22
  #
23
- # @yieldparam event [Object] CDC::Core::ChangeEvent-compatible event
23
+ # @yieldparam event [CDC::Core::ChangeEvent, CDC::Core::TransactionEnvelope] core work item
24
24
  # @return [Integer] number of consumed events
25
25
  def start
26
26
  return enum_for(:start) unless block_given?
@@ -49,65 +49,33 @@ module Mammoth
49
49
 
50
50
  def flatten_cdc_work(work)
51
51
  return [] if work.nil?
52
- return validate_transaction_envelope(work) if transaction_envelope?(work) && transaction_delivery?
53
- return validate_events(work.events) if transaction_envelope?(work)
54
52
  return work.flat_map { |item| flatten_cdc_work(item) } if work.is_a?(Array)
55
- return [synthetic_transaction_envelope(work)] if transaction_delivery?
53
+ return transaction_work(work) if work.is_a?(CDC::Core::TransactionEnvelope)
54
+ return event_work(work) if work.is_a?(CDC::Core::ChangeEvent)
56
55
 
57
- validate_events([work])
56
+ raise ReplicationError, "CDC source yielded non-core work: #{work.class}"
58
57
  end
59
58
 
60
59
  def transaction_delivery?
61
60
  delivery_unit == :transaction
62
61
  end
63
62
 
64
- def validate_transaction_envelope(envelope)
65
- validate_events(envelope.events)
66
- [envelope]
63
+ def transaction_work(envelope)
64
+ transaction_delivery? ? [envelope] : envelope.events
67
65
  end
68
66
 
69
- def validate_events(events)
70
- events.each { |event| validate_cdc_event!(event) }
67
+ def event_work(event)
68
+ transaction_delivery? ? [transaction_envelope(event)] : [event]
71
69
  end
72
70
 
73
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
74
- def synthetic_transaction_envelope(event)
75
- validate_cdc_event!(event)
76
-
77
- event_hash = event.to_h
78
- SyntheticTransactionEnvelope.new(
79
- [event],
80
- event_hash["transaction_id"] || event_hash[:transaction_id] ||
81
- event_hash["xid"] || event_hash[:xid] || event_hash["event_id"] || event_hash[:event_id],
82
- event_hash["commit_lsn"] || event_hash[:commit_lsn] ||
83
- event_hash["source_position"] || event_hash[:source_position],
84
- event_hash["committed_at"] || event_hash[:committed_at] ||
85
- event_hash["occurred_at"] || event_hash[:occurred_at],
86
- event_hash["metadata"] || event_hash[:metadata] || {}
71
+ def transaction_envelope(event)
72
+ CDC::Core::TransactionEnvelope.new(
73
+ transaction_id: event.transaction_id || event.metadata[:event_id] || event.commit_lsn,
74
+ events: [event],
75
+ commit_lsn: event.commit_lsn,
76
+ committed_at: event.occurred_at,
77
+ metadata: event.metadata
87
78
  )
88
79
  end
89
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
90
-
91
- def validate_cdc_event!(event)
92
- return event if event.respond_to?(:to_h) && cdc_event_hash?(event.to_h)
93
-
94
- raise ReplicationError, "CDC source yielded non-CDC work: #{event.class}"
95
- end
96
-
97
- def cdc_event_hash?(event_hash)
98
- return false unless event_hash.respond_to?(:key?)
99
-
100
- has_operation = event_hash.key?("operation") || event_hash.key?(:operation)
101
- has_position = event_hash.key?("source_position") || event_hash.key?(:source_position) ||
102
- event_hash.key?("commit_lsn") || event_hash.key?(:commit_lsn)
103
- has_operation && has_position
104
- end
105
-
106
- def transaction_envelope?(work)
107
- work.respond_to?(:events) && work.respond_to?(:transaction_id)
108
- end
109
-
110
- SyntheticTransactionEnvelope = Data.define(:events, :transaction_id, :commit_lsn, :commit_time, :metadata)
111
- private_constant :SyntheticTransactionEnvelope
112
80
  end
113
81
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mammoth
4
+ module Runtimes
5
+ # Adds configured batch submission to a selected delivery runtime.
6
+ #
7
+ # Application streams one work item at a time into this runtime boundary.
8
+ # BatchingRuntime owns accumulation and submits complete or final partial
9
+ # batches through the selected adapter runtime's #process_many contract.
10
+ class BatchingRuntime
11
+ attr_reader :runtime, :batch_size, :buffer
12
+
13
+ # @param runtime [#process_many] selected delivery runtime
14
+ # @param batch_size [Integer] maximum work items submitted together
15
+ def initialize(runtime:, batch_size:)
16
+ @runtime = runtime
17
+ @batch_size = batch_size
18
+ @buffer = []
19
+ end
20
+
21
+ # Buffer one work item and submit a full batch when ready.
22
+ #
23
+ # @param work [CDC::Core::ChangeEvent, CDC::Core::TransactionEnvelope] core work item
24
+ # @return [Array] processor results when a batch is submitted, otherwise an empty array
25
+ def process(work)
26
+ buffer << work
27
+ return [] if buffer.size < batch_size
28
+
29
+ flush
30
+ end
31
+
32
+ # Submit work immediately without changing the buffered stream.
33
+ #
34
+ # @param items [Array] work items
35
+ # @return [Array] processor results
36
+ def process_many(items)
37
+ runtime.process_many(items)
38
+ end
39
+
40
+ # Submit the final partial batch.
41
+ #
42
+ # @return [Array] processor results, or an empty array when no work is buffered
43
+ def flush
44
+ return [] if buffer.empty?
45
+
46
+ runtime.process_many(buffer.shift(buffer.size))
47
+ end
48
+
49
+ # Flush pending work and shut down the selected runtime when supported.
50
+ #
51
+ # @return [nil]
52
+ def shutdown
53
+ flush
54
+ runtime.shutdown if runtime.respond_to?(:shutdown)
55
+ nil
56
+ end
57
+ end
58
+ end
59
+ end
@@ -16,13 +16,15 @@ module Mammoth
16
16
  # @param concurrency [Integer] worker count
17
17
  # @param timeout [Numeric, nil] optional timeout
18
18
  # @param preserve_order [Boolean] preserve ordering when supported
19
+ # @param observer [CDC::Core::Observer] dispatch lifecycle observer
19
20
  # @return [Mammoth::ConcurrentDeliveryRuntime]
20
- def build(processor:, concurrency:, timeout:, preserve_order:)
21
+ def build(processor:, concurrency:, timeout:, preserve_order:, observer: CDC::Core::Observer.new)
21
22
  ConcurrentDeliveryRuntime.new(
22
23
  processor: processor,
23
24
  concurrency: concurrency,
24
25
  timeout: timeout,
25
- preserve_order: preserve_order
26
+ preserve_order: preserve_order,
27
+ observer: observer
26
28
  )
27
29
  end
28
30
  end
@@ -4,20 +4,22 @@ module Mammoth
4
4
  module Runtimes
5
5
  # Inline runtime adapter that processes work in the caller thread.
6
6
  class InlineAdapter < Adapter
7
- attr_reader :processor
7
+ attr_reader :processor, :observer
8
8
 
9
9
  # @param processor [#process] delivery processor
10
- def initialize(processor:)
10
+ # @param observer [CDC::Core::Observer] dispatch lifecycle observer
11
+ def initialize(processor:, observer: CDC::Core::Observer.new)
11
12
  super()
12
13
  @processor = processor
14
+ @observer = observer
13
15
  end
14
16
 
15
17
  # Build an inline runtime.
16
18
  #
17
19
  # @param processor [#process] delivery processor
18
20
  # @return [Mammoth::Runtimes::InlineAdapter]
19
- def self.build(processor:, **_options)
20
- new(processor: processor)
21
+ def self.build(processor:, observer: CDC::Core::Observer.new, **_options)
22
+ new(processor: processor, observer: observer)
21
23
  end
22
24
 
23
25
  # @return [String] adapter type name
@@ -28,13 +30,25 @@ module Mammoth
28
30
  # @param items [Array<Object>] work units
29
31
  # @return [Array<Object>] processor results
30
32
  def process_many(items)
31
- items.map { |item| processor.process(item) }
33
+ items.map do |item|
34
+ observer.dispatch_started(item)
35
+ processor.process(item).tap { |result| observe_result(result) }
36
+ end
32
37
  end
33
38
 
34
39
  # @return [nil]
35
40
  def shutdown
36
41
  nil
37
42
  end
43
+
44
+ private
45
+
46
+ def observe_result(result)
47
+ return observer.dispatch_succeeded(result) if result.success?
48
+ return observer.dispatch_failed(result) if result.failure?
49
+
50
+ observer.dispatch_skipped(result)
51
+ end
38
52
  end
39
53
  end
40
54
  end
@@ -27,8 +27,9 @@ module Mammoth
27
27
  # @param name [String, Symbol] adapter name
28
28
  # @param options [Hash] adapter-specific build options
29
29
  # @return [Object] runtime adapter
30
- def build(name, **options)
31
- fetch(name).build(**options)
30
+ def build(name, batch_size: 1, **options)
31
+ runtime = fetch(name).build(**options)
32
+ BatchingRuntime.new(runtime:, batch_size:)
32
33
  end
33
34
 
34
35
  # @return [Array<String>] registered runtime adapter names
@@ -8,7 +8,7 @@ module Mammoth
8
8
  # Postgres realizes the CDC Ecosystem libraries for Mammoth's product
9
9
  # boundary. It composes pgoutput-client, pgoutput-parser,
10
10
  # pgoutput-decoder, and pgoutput-source-adapter into a single source that
11
- # yields CDC::Core-shaped work to the delivery runtime.
11
+ # yields CDC::Core work to the delivery runtime.
12
12
  #
13
13
  # This class may mention pgoutput implementation details because it is the
14
14
  # concrete PostgreSQL source adapter used by Mammoth. The rest of Mammoth
@@ -45,21 +45,27 @@ module Mammoth
45
45
  @checkpoint_store = checkpoint_store
46
46
  end
47
47
 
48
- # Stream CDC::Core-shaped work from PostgreSQL logical replication.
48
+ # Stream CDC::Core work from PostgreSQL logical replication.
49
49
  #
50
50
  # Calling this method starts the injected or configured pgoutput-client
51
51
  # runner. The runner owns the PostgreSQL replication connection and slot
52
- # lifecycle; this class only composes the parser, decoder, and adapter
53
- # libraries around the stream.
52
+ # lifecycle, while pgoutput-source-adapter owns transaction buffering and
53
+ # normalization into CDC::Core work items. This class only composes those
54
+ # layers and forwards transport source positions to the adapter.
54
55
  #
55
- # @yieldparam work [Object] CDC::Core::ChangeEvent or TransactionEnvelope
56
+ # @yieldparam work [CDC::Core::ChangeEvent, CDC::Core::TransactionEnvelope]
56
57
  # @return [Enumerator, nil]
57
58
  # @raise [Mammoth::ReplicationError] when the source cannot stream CDC work
58
59
  def each(&block)
59
60
  return enum_for(:each) unless block_given?
60
61
 
61
- effective_runner.start do |payload, metadata = nil|
62
- process_payload(payload, metadata, &block)
62
+ normalizer = effective_adapter
63
+ unless normalizer.respond_to?(:each_normalized)
64
+ raise ReplicationError, "pgoutput source adapter must respond to #each_normalized"
65
+ end
66
+
67
+ normalizer.each_normalized(decoded_stream) do |work|
68
+ block.call(validate_core_work!(work))
63
69
  end
64
70
  nil
65
71
  rescue StandardError => e
@@ -70,41 +76,44 @@ module Mammoth
70
76
 
71
77
  private
72
78
 
79
+ def decoded_stream
80
+ Enumerator.new do |stream|
81
+ effective_runner.start do |payload, metadata = nil|
82
+ process_payload(payload, metadata) { |decoded| stream << stream_event(decoded, metadata) }
83
+ end
84
+ end
85
+ end
86
+
73
87
  def process_payload(payload, metadata, &block)
74
88
  parsed = parse_payload(payload)
75
89
  decoded = decode_message(parsed, metadata)
76
- process_decoded(decoded, metadata, &block)
90
+ each_decoded(decoded, &block)
77
91
  end
78
92
 
79
- # rubocop:disable Metrics/MethodLength
80
- def process_decoded(decoded, metadata, &block)
93
+ def each_decoded(decoded, &block)
81
94
  return if decoded.nil?
82
95
 
83
96
  if decoded.is_a?(Array)
84
- decoded.each { |item| process_decoded(item, metadata, &block) }
97
+ decoded.each { |item| each_decoded(item, &block) }
85
98
  return
86
99
  end
87
100
 
88
- if begin_message?(decoded)
89
- start_transaction_buffer(decoded)
90
- return
91
- end
101
+ block.call(decoded)
102
+ end
92
103
 
93
- if commit_message?(decoded)
94
- emit_transaction_buffer(decoded, metadata, &block)
95
- return
96
- end
104
+ def stream_event(decoded, metadata)
105
+ normalizer = effective_adapter
106
+ return decoded unless normalizer.respond_to?(:stream_event)
97
107
 
98
- normalize_decoded(decoded).each do |work|
99
- work = enrich_work_position(work, metadata, decoded)
100
- if transaction_buffer_active?
101
- Array(@transaction_events) << work
102
- else
103
- block.call(work)
104
- end
105
- end
108
+ normalizer.stream_event(decoded, source_position: source_position(metadata))
109
+ end
110
+
111
+ def validate_core_work!(work)
112
+ return work if work.is_a?(CDC::Core::ChangeEvent)
113
+ return work if work.is_a?(CDC::Core::TransactionEnvelope)
114
+
115
+ raise ReplicationError, "pgoutput source adapter yielded non-core work: #{work.class}"
106
116
  end
107
- # rubocop:enable Metrics/MethodLength
108
117
 
109
118
  def parse_payload(payload)
110
119
  parser = effective_parser
@@ -127,102 +136,6 @@ module Mammoth
127
136
  raise ReplicationError, "pgoutput decoder must respond to #decode or #call"
128
137
  end
129
138
 
130
- def normalize_decoded(decoded)
131
- return [] if decoded.nil?
132
- return decoded.flat_map { |item| normalize_decoded(item) } if decoded.is_a?(Array)
133
-
134
- adapter = effective_adapter
135
- result = if adapter.respond_to?(:normalize)
136
- adapter.normalize(decoded)
137
- elsif adapter.respond_to?(:call)
138
- adapter.call(decoded)
139
- else
140
- raise ReplicationError, "pgoutput source adapter must respond to #normalize or #call"
141
- end
142
-
143
- result.is_a?(Array) ? result.compact : [result].compact
144
- end
145
-
146
- def begin_message?(decoded)
147
- message_kind(decoded).include?("begin")
148
- end
149
-
150
- def commit_message?(decoded)
151
- message_kind(decoded).include?("commit")
152
- end
153
-
154
- def message_kind(decoded)
155
- (value_from(decoded, :message_type, :type, :kind) || decoded.class.name.to_s.split("::").last).to_s.downcase
156
- end
157
-
158
- def start_transaction_buffer(decoded)
159
- @transaction_events = []
160
- @transaction_id = value_from(decoded, :transaction_id, :xid, :final_lsn)
161
- @transaction_metadata = value_hash(decoded, :metadata) || {}
162
- end
163
-
164
- def emit_transaction_buffer(decoded, metadata, &block)
165
- return unless transaction_buffer_active?
166
-
167
- block.call(
168
- TransactionEnvelope.new(
169
- @transaction_events,
170
- transaction_id_for(decoded),
171
- commit_lsn_for(decoded, metadata),
172
- value_from(decoded, :commit_time, :committed_at, :timestamp),
173
- @transaction_metadata
174
- )
175
- )
176
- ensure
177
- clear_transaction_buffer
178
- end
179
-
180
- def transaction_id_for(decoded)
181
- value_from(decoded, :transaction_id, :xid) || @transaction_id || first_event_value(:transaction_id, :xid)
182
- end
183
-
184
- def commit_lsn_for(decoded, metadata)
185
- value_from(decoded, :commit_lsn, :source_position, :lsn, :end_lsn, :final_lsn) ||
186
- value_from(metadata, :commit_lsn, :source_position, :lsn, :end_lsn, :final_lsn) ||
187
- first_event_value(:commit_lsn, :source_position, :lsn)
188
- end
189
-
190
- def transaction_buffer_active?
191
- !@transaction_events.nil?
192
- end
193
-
194
- def clear_transaction_buffer
195
- @transaction_events = nil
196
- @transaction_id = nil
197
- @transaction_metadata = nil
198
- end
199
-
200
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
201
- def enrich_work_position(work, metadata, decoded)
202
- position = value_from(work, :source_position, :commit_lsn) ||
203
- value_from(metadata, :source_position, :commit_lsn, :lsn) ||
204
- value_from(decoded, :source_position, :commit_lsn, :lsn)
205
- return work unless position
206
-
207
- work_hash = work.respond_to?(:to_h) ? work.to_h : work
208
- return work unless work_hash.is_a?(Hash)
209
-
210
- key_style = work_hash.key?("operation") ? :string : :symbol
211
- source_position_key = key_style == :string ? "source_position" : :source_position
212
- commit_lsn_key = key_style == :string ? "commit_lsn" : :commit_lsn
213
- return work if work_hash[source_position_key] || work_hash[commit_lsn_key]
214
-
215
- work_hash.merge(source_position_key => position, commit_lsn_key => position)
216
- end
217
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
218
-
219
- def first_event_value(*keys)
220
- event = Array(@transaction_events).find do |event|
221
- keys.any? { |key| value_from(event, key) }
222
- end
223
- value_from(event, *keys) if event
224
- end
225
-
226
139
  def value_from(object, *keys)
227
140
  return nil if object.nil?
228
141
 
@@ -239,9 +152,8 @@ module Mammoth
239
152
  nil
240
153
  end
241
154
 
242
- def value_hash(object, key)
243
- value = value_from(object, key)
244
- value.is_a?(Hash) ? value : nil
155
+ def source_position(metadata)
156
+ value_from(metadata, :source_position, :commit_lsn, :lsn, :wal_end_lsn, :end_lsn, :final_lsn)
245
157
  end
246
158
 
247
159
  def effective_runner
@@ -375,8 +287,6 @@ module Mammoth
375
287
  rescue LoadError => e
376
288
  raise ReplicationError, "#{gem_name} is required for PostgreSQL CDC source integration: #{e.message}"
377
289
  end
378
- TransactionEnvelope = Data.define(:events, :transaction_id, :commit_lsn, :commit_time, :metadata)
379
- private_constant :TransactionEnvelope
380
290
  end
381
291
  # rubocop:enable Metrics/ClassLength
382
292
  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