cdc-core 0.1.0 → 0.1.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 +4 -4
- data/CHANGELOG.md +15 -7
- data/README.md +105 -19
- data/lib/cdc/core/composite_processor.rb +39 -14
- data/lib/cdc/core/errors.rb +9 -0
- data/lib/cdc/core/event_metadata.rb +4 -1
- data/lib/cdc/core/event_position.rb +48 -0
- data/lib/cdc/core/null_observer.rb +12 -0
- data/lib/cdc/core/observer.rb +123 -0
- data/lib/cdc/core/operation.rb +2 -0
- data/lib/cdc/core/ordering_key.rb +40 -0
- data/lib/cdc/core/ordering_policy.rb +106 -0
- data/lib/cdc/core/ordering_scope.rb +51 -0
- data/lib/cdc/core/pipeline.rb +23 -4
- data/lib/cdc/core/processor.rb +39 -0
- data/lib/cdc/core/processor_result.rb +96 -2
- data/lib/cdc/core/router.rb +91 -0
- data/lib/cdc/core/source_adapter.rb +41 -0
- data/lib/cdc/core/version.rb +1 -1
- data/lib/cdc/core.rb +12 -3
- data/sig/cdc/core/composite_processor.rbs +12 -1
- data/sig/cdc/core/errors.rbs +12 -0
- data/sig/cdc/core/event_position.rbs +21 -0
- data/sig/cdc/core/null_observer.rbs +8 -0
- data/sig/cdc/core/observer.rbs +25 -0
- data/sig/cdc/core/ordering_key.rbs +16 -0
- data/sig/cdc/core/ordering_policy.rbs +27 -0
- data/sig/cdc/core/ordering_scope.rbs +19 -0
- data/sig/cdc/core/pipeline.rbs +10 -1
- data/sig/cdc/core/processor.rbs +31 -0
- data/sig/cdc/core/processor_result.rbs +50 -1
- data/sig/cdc/core/router.rbs +24 -0
- data/sig/cdc/core/source_adapter.rbs +10 -0
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a71182bac774b200c13556d0a887754a2788dc4e3581fc84a94288764900f365
|
|
4
|
+
data.tar.gz: a67bb53791b19347fb0c589b59603abfc6f085504597531d7c9932da3a3d8749
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a96b523f2144fd94d0ecd633bf956fdcad791a994f3a2bf9e4559da9e4c713570f52c842e3cce15cb9a1491345190087c7cd1599612f5e4ecd5f933d22a6a9d6
|
|
7
|
+
data.tar.gz: 44acef005734403fea47a88e8d142cf8f9667ef032b032af24dfc25cf25c491d1c2a6caf3fab3bb06fc7790d63b8c88a8cb9984e61803dac315cd6d8118a2957
|
data/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## [0.1.1] - 2026-06-09
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
### Added
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
- Added `CDC::Core::OrderingScope`, `OrderingPolicy`, `OrderingKey`, and `EventPosition`.
|
|
8
|
+
- Added `CDC::Core::SourceAdapter` as the shared source normalization contract.
|
|
9
|
+
- Added `CDC::Core::Router` and `UnsupportedWorkItemError`.
|
|
10
|
+
- Added optional lifecycle hooks to `CDC::Core::Processor`.
|
|
11
|
+
- Added structured failure metadata to `CDC::Core::ProcessorResult`.
|
|
12
|
+
- Added status validation and serializable projection helpers to `CDC::Core::ProcessorResult`.
|
|
13
|
+
- Added backend-agnostic observer hooks for instrumentation.
|
|
14
|
+
- Added canonical metric names and tag helpers to `CDC::Core::Observer`.
|
|
9
15
|
|
|
10
|
-
###
|
|
16
|
+
### Fixed
|
|
11
17
|
|
|
12
|
-
-
|
|
18
|
+
- Preserve explicit `false` metadata values when reading `EventMetadata` by symbol or string key.
|
|
19
|
+
- Raise `CDC::Core::InvalidOperationError` for nil operation input instead of leaking `NoMethodError`.
|
|
20
|
+
- Align API documentation examples with the current `#process`, `Pipeline.new(processor:)`, and `Filter#match?` contracts.
|
|
13
21
|
|
|
14
22
|
---
|
|
15
23
|
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# cdc-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared Change Data Capture vocabulary for Ruby.
|
|
4
4
|
|
|
5
|
-
`cdc-core` provides immutable, Ractor-safe event objects and processor contracts for building CDC systems. It intentionally does not connect to databases, parse wire protocols, decode PostgreSQL OIDs, or integrate with Rails.
|
|
5
|
+
`cdc-core` provides immutable, Ractor-safe event objects and processor contracts for building CDC systems. It intentionally does not connect to databases, parse wire protocols, decode PostgreSQL OIDs, run schedulers, or integrate with Rails.
|
|
6
6
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
@@ -10,12 +10,16 @@ Database-agnostic Change Data Capture domain primitives for Ruby.
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
+
- SourceAdapter normalization contract
|
|
13
14
|
- Immutable `ChangeEvent` objects
|
|
14
15
|
- Transaction grouping via `TransactionEnvelope`
|
|
15
16
|
- Column-level change objects
|
|
17
|
+
- Ordering vocabulary
|
|
16
18
|
- Processor and composite processor contracts
|
|
17
19
|
- Event filters
|
|
18
20
|
- Small pipeline orchestration object
|
|
21
|
+
- Router for supported work item shapes
|
|
22
|
+
- Observer hooks and canonical metric names
|
|
19
23
|
- Ractor-safe event and transaction objects
|
|
20
24
|
- RBS signatures
|
|
21
25
|
- YARD-compatible documentation
|
|
@@ -24,22 +28,88 @@ Database-agnostic Change Data Capture domain primitives for Ruby.
|
|
|
24
28
|
## Ecosystem Position
|
|
25
29
|
|
|
26
30
|
```text
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
pgoutput-decoder
|
|
34
|
-
│
|
|
35
|
-
▼
|
|
31
|
+
upstream source
|
|
32
|
+
|
|
|
33
|
+
v
|
|
34
|
+
source adapter
|
|
35
|
+
|
|
|
36
|
+
v
|
|
36
37
|
cdc-core
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
|
|
|
39
|
+
+--> cdc-parallel CPU-bound processing
|
|
40
|
+
|
|
|
41
|
+
+--> cdc-concurrent I/O-bound processing
|
|
42
|
+
|
|
|
43
|
+
+--> application sinks / processors
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
`cdc-core` is the shared vocabulary layer. It defines what a change event, transaction, processor, and
|
|
46
|
+
`cdc-core` is the shared vocabulary layer. It defines what a change event, transaction, processor, ordering policy, observer notification, and processor result mean without caring where the event came from or how it will be executed.
|
|
47
|
+
|
|
48
|
+
## Boundary Summary
|
|
49
|
+
|
|
50
|
+
`cdc-core` is for vocabulary.
|
|
51
|
+
|
|
52
|
+
Runtime gems are for execution.
|
|
53
|
+
|
|
54
|
+
Sinks are for persistence or side effects.
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
source adapter -> cdc-core vocabulary -> runtime gem -> sink
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Source Adapters
|
|
61
|
+
|
|
62
|
+
CDC::Core::SourceAdapter defines the normalization contract used to translate source-specific payloads into cdc-core vocabulary objects.
|
|
63
|
+
|
|
64
|
+
It translates source-specific payloads into:
|
|
65
|
+
|
|
66
|
+
- `CDC::Core::ChangeEvent`
|
|
67
|
+
- `CDC::Core::TransactionEnvelope`
|
|
68
|
+
- batches of core work items
|
|
69
|
+
|
|
70
|
+
The current PostgreSQL-oriented path is:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
pgoutput-client -> pgoutput-parser -> pgoutput-decoder -> source adapter -> cdc-core
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The `pgoutput*` family handles PostgreSQL transport, protocol parsing, and type decoding. The source-adapter boundary is where those source-specific details become generic `cdc-core` objects.
|
|
77
|
+
|
|
78
|
+
Other adapters can normalize logs, API payloads, application events, or other database streams into the same vocabulary.
|
|
79
|
+
|
|
80
|
+
## Downstream Runtime Gems
|
|
81
|
+
|
|
82
|
+
`cdc-parallel` and `cdc-concurrent` are downstream consumers of `cdc-core` events.
|
|
83
|
+
|
|
84
|
+
### cdc-parallel
|
|
85
|
+
|
|
86
|
+
Use `cdc-parallel` for heavy CPU-bound processing.
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
|
|
90
|
+
- transformations
|
|
91
|
+
- enrichment
|
|
92
|
+
- encoding
|
|
93
|
+
- compression
|
|
94
|
+
- scoring
|
|
95
|
+
- in-memory calculations
|
|
96
|
+
|
|
97
|
+
It is the Ractor-oriented runtime path.
|
|
98
|
+
|
|
99
|
+
### cdc-concurrent
|
|
100
|
+
|
|
101
|
+
Use `cdc-concurrent` for I/O-heavy processing.
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
|
|
105
|
+
- HTTP calls
|
|
106
|
+
- webhook delivery
|
|
107
|
+
- Redis writes
|
|
108
|
+
- search indexing
|
|
109
|
+
- object storage writes
|
|
110
|
+
- database sink writes
|
|
111
|
+
|
|
112
|
+
It is the fiber-friendly runtime path.
|
|
43
113
|
|
|
44
114
|
## Installation
|
|
45
115
|
|
|
@@ -86,7 +156,7 @@ transaction = CDC::Core::TransactionEnvelope.new(
|
|
|
86
156
|
)
|
|
87
157
|
```
|
|
88
158
|
|
|
89
|
-
A transaction envelope
|
|
159
|
+
A transaction envelope preserves database transaction boundaries. Runtime gems may use that boundary when they need ordering, batching, or parallel execution decisions.
|
|
90
160
|
|
|
91
161
|
## Processors
|
|
92
162
|
|
|
@@ -114,7 +184,7 @@ AnalyticsProcessor.new.ractor_safe?
|
|
|
114
184
|
# => true
|
|
115
185
|
```
|
|
116
186
|
|
|
117
|
-
This declares intent only. `cdc-core` does not execute processors in Ractors.
|
|
187
|
+
This declares intent only. `cdc-core` does not execute processors in Ractors. `cdc-parallel` can use this signal before moving processor work across Ractors.
|
|
118
188
|
|
|
119
189
|
## Composite Processor
|
|
120
190
|
|
|
@@ -149,10 +219,26 @@ result = pipeline.process(event)
|
|
|
149
219
|
- Parse `pgoutput`
|
|
150
220
|
- Decode PostgreSQL values
|
|
151
221
|
- Manage replication slots
|
|
222
|
+
- Implement concrete source adapters
|
|
152
223
|
- Run Ractor pools
|
|
224
|
+
- Run fiber schedulers
|
|
153
225
|
- Persist audit records
|
|
154
226
|
- Integrate with ActiveRecord
|
|
155
|
-
- Publish to Kafka, Redis, or
|
|
227
|
+
- Publish to Kafka, Redis, HTTP, or other sinks
|
|
228
|
+
|
|
229
|
+
## Documentation
|
|
230
|
+
|
|
231
|
+
The YARD documentation uses `docs/index.md` as its readme and includes the Markdown files under `docs/`.
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
--title "cdc-core API Documentation"
|
|
235
|
+
--readme docs/index.md
|
|
236
|
+
--markup markdown
|
|
237
|
+
--output-dir doc
|
|
238
|
+
lib/**/*.rb
|
|
239
|
+
-
|
|
240
|
+
docs/**/*.md
|
|
241
|
+
```
|
|
156
242
|
|
|
157
243
|
## Development
|
|
158
244
|
|
|
@@ -163,4 +249,4 @@ bundle exec steep check
|
|
|
163
249
|
|
|
164
250
|
## License
|
|
165
251
|
|
|
166
|
-
MIT.
|
|
252
|
+
[MIT](./LICENSE.txt).
|
|
@@ -10,15 +10,17 @@ module CDC
|
|
|
10
10
|
class CompositeProcessor < Processor
|
|
11
11
|
# @return [Array<Processor>] processors executed for each event
|
|
12
12
|
# @return [Boolean] whether processing stops on the first failure
|
|
13
|
-
|
|
13
|
+
# @return [Observer] observer notified of dispatch events
|
|
14
|
+
attr_reader :processors, :fail_fast, :observer
|
|
14
15
|
|
|
15
16
|
# Build a composite processor.
|
|
16
17
|
#
|
|
17
18
|
# @param processors [Array<#process>] processors to execute
|
|
18
19
|
# @param fail_fast [Boolean] whether to stop after the first failure
|
|
19
|
-
def initialize(processors, fail_fast: true) # rubocop:disable Lint/MissingSuper
|
|
20
|
+
def initialize(processors, fail_fast: true, observer: NullObserver::INSTANCE) # rubocop:disable Lint/MissingSuper
|
|
20
21
|
@processors = processors.freeze
|
|
21
22
|
@fail_fast = fail_fast
|
|
23
|
+
@observer = observer || NullObserver::INSTANCE
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
# Process an event through each configured processor.
|
|
@@ -26,18 +28,12 @@ module CDC
|
|
|
26
28
|
# @param event [ChangeEvent] event to process
|
|
27
29
|
# @return [Array<ProcessorResult>] result from each attempted processor
|
|
28
30
|
def process(event)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
result = ProcessorResult.failure(e, event:)
|
|
36
|
-
results << result
|
|
37
|
-
break if fail_fast
|
|
38
|
-
end
|
|
39
|
-
Ractor.make_shareable(results.freeze) if results.none?(&:failure?)
|
|
40
|
-
results.freeze
|
|
31
|
+
observer.dispatch_started(event)
|
|
32
|
+
results = collect_results(event)
|
|
33
|
+
final_results = results.freeze
|
|
34
|
+
observe_results(final_results)
|
|
35
|
+
Ractor.make_shareable(final_results) if results.none?(&:failure?)
|
|
36
|
+
final_results
|
|
41
37
|
end
|
|
42
38
|
|
|
43
39
|
# Processors that declared Ractor safety.
|
|
@@ -66,6 +62,35 @@ module CDC
|
|
|
66
62
|
|
|
67
63
|
result ? ProcessorResult.success(event) : ProcessorResult.skipped(event)
|
|
68
64
|
end
|
|
65
|
+
|
|
66
|
+
def collect_results(event)
|
|
67
|
+
results = [] # : Array[ProcessorResult]
|
|
68
|
+
processors.each do |processor|
|
|
69
|
+
result = process_with(processor, event)
|
|
70
|
+
results << result
|
|
71
|
+
break if fail_fast && result.failure?
|
|
72
|
+
end
|
|
73
|
+
results
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def process_with(processor, event)
|
|
77
|
+
normalize_result(processor.process(event), event)
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
ProcessorResult.failure(e, event:, processor: processor.class.name)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def observe_results(results)
|
|
83
|
+
results.each do |result|
|
|
84
|
+
case result.status
|
|
85
|
+
when :success
|
|
86
|
+
observer.dispatch_succeeded(result)
|
|
87
|
+
when :failure
|
|
88
|
+
observer.dispatch_failed(result)
|
|
89
|
+
when :skipped
|
|
90
|
+
observer.dispatch_skipped(result)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
69
94
|
end
|
|
70
95
|
end
|
|
71
96
|
end
|
data/lib/cdc/core/errors.rb
CHANGED
|
@@ -8,6 +8,15 @@ module CDC
|
|
|
8
8
|
# Raised when an operation cannot be normalized to a supported CDC action.
|
|
9
9
|
class InvalidOperationError < Error; end
|
|
10
10
|
|
|
11
|
+
# Raised when an ordering scope cannot be normalized to a supported value.
|
|
12
|
+
class InvalidOrderingScopeError < Error; end
|
|
13
|
+
|
|
14
|
+
# Raised when an ordering position cannot be normalized to a supported value.
|
|
15
|
+
class InvalidOrderingPositionError < Error; end
|
|
16
|
+
|
|
17
|
+
# Raised when a router receives an unsupported CDC work item.
|
|
18
|
+
class UnsupportedWorkItemError < Error; end
|
|
19
|
+
|
|
11
20
|
# Raised by processors when a processor-specific failure needs wrapping.
|
|
12
21
|
class ProcessorError < Error; end
|
|
13
22
|
end
|
|
@@ -24,7 +24,10 @@ module CDC
|
|
|
24
24
|
# @param key [String, Symbol] metadata key
|
|
25
25
|
# @return [Object, nil]
|
|
26
26
|
def [](key)
|
|
27
|
-
|
|
27
|
+
string_key = key.to_s
|
|
28
|
+
return data[string_key] if data.key?(string_key)
|
|
29
|
+
|
|
30
|
+
data[key]
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
# Return the normalized Ractor-shareable hash.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CDC
|
|
4
|
+
module Core
|
|
5
|
+
# Immutable representation of an event's position metadata.
|
|
6
|
+
#
|
|
7
|
+
# EventPosition is intentionally small and transport-agnostic. It captures
|
|
8
|
+
# the position strategy plus the event fields that a runtime may use to
|
|
9
|
+
# preserve ordering guarantees.
|
|
10
|
+
class EventPosition
|
|
11
|
+
# @return [Symbol] position strategy
|
|
12
|
+
# @return [Object, nil] primary position value for the chosen strategy
|
|
13
|
+
# @return [Object, nil] transaction identifier associated with the event
|
|
14
|
+
# @return [Integer, nil] sequence number within a transaction or stream
|
|
15
|
+
# @return [Time, nil] timestamp associated with the event
|
|
16
|
+
attr_reader :strategy, :value, :transaction_id, :sequence_number, :occurred_at
|
|
17
|
+
|
|
18
|
+
# Build an event position.
|
|
19
|
+
#
|
|
20
|
+
# @param strategy [#to_sym] position strategy
|
|
21
|
+
# @param value [Object, nil] primary position value
|
|
22
|
+
# @param transaction_id [Object, nil] transaction identifier
|
|
23
|
+
# @param sequence_number [Integer, nil] sequence number
|
|
24
|
+
# @param occurred_at [Time, nil] event timestamp
|
|
25
|
+
def initialize(strategy:, value:, transaction_id: nil, sequence_number: nil, occurred_at: nil)
|
|
26
|
+
@strategy = strategy.to_sym
|
|
27
|
+
@value = value
|
|
28
|
+
@transaction_id = transaction_id
|
|
29
|
+
@sequence_number = sequence_number
|
|
30
|
+
@occurred_at = occurred_at
|
|
31
|
+
Ractor.make_shareable(self)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Convert the position into a Ractor-shareable hash.
|
|
35
|
+
#
|
|
36
|
+
# @return [Hash{String=>Object,nil}]
|
|
37
|
+
def to_h
|
|
38
|
+
Ractor.make_shareable({
|
|
39
|
+
'strategy' => strategy,
|
|
40
|
+
'value' => value,
|
|
41
|
+
'transaction_id' => transaction_id,
|
|
42
|
+
'sequence_number' => sequence_number,
|
|
43
|
+
'occurred_at' => occurred_at
|
|
44
|
+
}.freeze)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CDC
|
|
4
|
+
module Core
|
|
5
|
+
# Observer interface for CDC runtime instrumentation.
|
|
6
|
+
#
|
|
7
|
+
# Observers receive lifecycle and result notifications from core runtime
|
|
8
|
+
# objects. The default implementation is a no-op so callers can opt in
|
|
9
|
+
# without taking a dependency on a metrics backend.
|
|
10
|
+
class Observer
|
|
11
|
+
# Canonical metric names emitted by core runtime hooks.
|
|
12
|
+
METRIC_NAMES = Ractor.make_shareable({
|
|
13
|
+
dispatch_started: 'cdc_core.dispatch.started',
|
|
14
|
+
dispatch_succeeded: 'cdc_core.dispatch.succeeded',
|
|
15
|
+
dispatch_failed: 'cdc_core.dispatch.failed',
|
|
16
|
+
dispatch_skipped: 'cdc_core.dispatch.skipped'
|
|
17
|
+
}.freeze)
|
|
18
|
+
|
|
19
|
+
# Build a canonical metric tag set for a CDC work item or result.
|
|
20
|
+
#
|
|
21
|
+
# @param payload [ChangeEvent, TransactionEnvelope, ProcessorResult, Array]
|
|
22
|
+
# @return [Hash{String=>Object}]
|
|
23
|
+
def self.metric_tags(payload)
|
|
24
|
+
tags = {} # : Hash[String, untyped]
|
|
25
|
+
case payload
|
|
26
|
+
when ChangeEvent
|
|
27
|
+
tags.merge!(change_event_metric_tags(payload))
|
|
28
|
+
when TransactionEnvelope
|
|
29
|
+
tags.merge!(transaction_envelope_metric_tags(payload))
|
|
30
|
+
when ProcessorResult
|
|
31
|
+
tags.merge!(processor_result_metric_tags(payload))
|
|
32
|
+
when Array
|
|
33
|
+
tags.merge!(batch_metric_tags(payload))
|
|
34
|
+
else
|
|
35
|
+
tags['kind'] = payload.class.name
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Ractor.make_shareable(tags.freeze)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Canonical metric name for the start hook.
|
|
42
|
+
#
|
|
43
|
+
# @return [String]
|
|
44
|
+
def self.started_metric_name = METRIC_NAMES.fetch(:dispatch_started)
|
|
45
|
+
|
|
46
|
+
# Canonical metric name for the success hook.
|
|
47
|
+
#
|
|
48
|
+
# @return [String]
|
|
49
|
+
def self.succeeded_metric_name = METRIC_NAMES.fetch(:dispatch_succeeded)
|
|
50
|
+
|
|
51
|
+
# Canonical metric name for the failure hook.
|
|
52
|
+
#
|
|
53
|
+
# @return [String]
|
|
54
|
+
def self.failed_metric_name = METRIC_NAMES.fetch(:dispatch_failed)
|
|
55
|
+
|
|
56
|
+
# Canonical metric name for the skip hook.
|
|
57
|
+
#
|
|
58
|
+
# @return [String]
|
|
59
|
+
def self.skipped_metric_name = METRIC_NAMES.fetch(:dispatch_skipped)
|
|
60
|
+
|
|
61
|
+
# Called before a work item is dispatched.
|
|
62
|
+
#
|
|
63
|
+
# @param _event [ChangeEvent, TransactionEnvelope, Array]
|
|
64
|
+
# @return [void]
|
|
65
|
+
def dispatch_started(_event); end
|
|
66
|
+
|
|
67
|
+
# Called after a work item is processed successfully.
|
|
68
|
+
#
|
|
69
|
+
# @param _result [ProcessorResult, Array<ProcessorResult>]
|
|
70
|
+
# @return [void]
|
|
71
|
+
def dispatch_succeeded(_result); end
|
|
72
|
+
|
|
73
|
+
# Called after a work item fails.
|
|
74
|
+
#
|
|
75
|
+
# @param _result [ProcessorResult]
|
|
76
|
+
# @return [void]
|
|
77
|
+
def dispatch_failed(_result); end
|
|
78
|
+
|
|
79
|
+
# Called when a work item is filtered or skipped.
|
|
80
|
+
#
|
|
81
|
+
# @param _result [ProcessorResult]
|
|
82
|
+
# @return [void]
|
|
83
|
+
def dispatch_skipped(_result); end
|
|
84
|
+
|
|
85
|
+
private_class_method def self.change_event_metric_tags(event)
|
|
86
|
+
{
|
|
87
|
+
'kind' => 'change_event',
|
|
88
|
+
'operation' => event.operation,
|
|
89
|
+
'schema' => event.schema,
|
|
90
|
+
'table' => event.table
|
|
91
|
+
}.tap do |tags|
|
|
92
|
+
tags['transaction_id'] = event.transaction_id if event.transaction_id
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private_class_method def self.transaction_envelope_metric_tags(transaction)
|
|
97
|
+
{
|
|
98
|
+
'kind' => 'transaction_envelope',
|
|
99
|
+
'transaction_id' => transaction.transaction_id,
|
|
100
|
+
'size' => transaction.size
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private_class_method def self.processor_result_metric_tags(result)
|
|
105
|
+
{
|
|
106
|
+
'kind' => 'processor_result',
|
|
107
|
+
'status' => result.status,
|
|
108
|
+
'retryable' => result.retryable?
|
|
109
|
+
}.tap do |tags|
|
|
110
|
+
tags['processor'] = result.processor_name if result.processor_name
|
|
111
|
+
tags['failure_reason'] = result.failure_reason if result.failure?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private_class_method def self.batch_metric_tags(batch)
|
|
116
|
+
{
|
|
117
|
+
'kind' => 'batch',
|
|
118
|
+
'size' => batch.size
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
data/lib/cdc/core/operation.rb
CHANGED
|
@@ -34,6 +34,8 @@ module CDC
|
|
|
34
34
|
value = operation.to_sym
|
|
35
35
|
return value if SUPPORTED.include?(value)
|
|
36
36
|
|
|
37
|
+
raise InvalidOperationError, "unsupported CDC operation: #{operation.inspect}"
|
|
38
|
+
rescue NoMethodError
|
|
37
39
|
raise InvalidOperationError, "unsupported CDC operation: #{operation.inspect}"
|
|
38
40
|
end
|
|
39
41
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CDC
|
|
4
|
+
module Core
|
|
5
|
+
# Immutable grouping key for ordering-related dispatch.
|
|
6
|
+
#
|
|
7
|
+
# OrderingKey captures the scope plus the components that define a
|
|
8
|
+
# particular ordered lane. It does not choose an execution strategy.
|
|
9
|
+
class OrderingKey
|
|
10
|
+
# @return [Symbol] ordering scope
|
|
11
|
+
# @return [Hash{String=>Object}] normalized key components
|
|
12
|
+
attr_reader :scope, :components
|
|
13
|
+
|
|
14
|
+
# Build an ordering key.
|
|
15
|
+
#
|
|
16
|
+
# @param scope [#to_sym] ordering scope
|
|
17
|
+
# @param components [Hash] key components
|
|
18
|
+
def initialize(scope:, components: {})
|
|
19
|
+
@scope = OrderingScope.normalize(scope)
|
|
20
|
+
@components = EventMetadata.new(components).to_h
|
|
21
|
+
Ractor.make_shareable(self)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Whether the key has no components.
|
|
25
|
+
#
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def empty? = components.empty?
|
|
28
|
+
|
|
29
|
+
# Convert the key into a Ractor-shareable hash.
|
|
30
|
+
#
|
|
31
|
+
# @return [Hash{String=>Object}]
|
|
32
|
+
def to_h
|
|
33
|
+
Ractor.make_shareable({
|
|
34
|
+
'scope' => scope,
|
|
35
|
+
'components' => components
|
|
36
|
+
}.freeze)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CDC
|
|
4
|
+
module Core
|
|
5
|
+
# Immutable description of how ordered CDC work should be grouped.
|
|
6
|
+
#
|
|
7
|
+
# OrderingPolicy defines the vocabulary for ordering guarantees without
|
|
8
|
+
# performing any scheduling itself. Runtime layers can consume the policy to
|
|
9
|
+
# route events into ordered lanes.
|
|
10
|
+
class OrderingPolicy
|
|
11
|
+
# Position strategies supported by the core contract.
|
|
12
|
+
SUPPORTED_POSITIONS = Ractor.make_shareable(
|
|
13
|
+
%i[commit_lsn transaction_id sequence_number occurred_at].freeze
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# @return [Symbol] ordering scope
|
|
17
|
+
# @return [Symbol] position strategy
|
|
18
|
+
# @return [Boolean] whether transaction boundaries should be preserved
|
|
19
|
+
attr_reader :scope, :position, :transaction_aware
|
|
20
|
+
|
|
21
|
+
# Build an ordering policy.
|
|
22
|
+
#
|
|
23
|
+
# @param scope [#to_sym] ordering scope
|
|
24
|
+
# @param position [#to_sym] position strategy
|
|
25
|
+
# @param transaction_aware [Boolean] whether transaction boundaries matter
|
|
26
|
+
def initialize(scope:, position: :commit_lsn, transaction_aware: true)
|
|
27
|
+
@scope = OrderingScope.normalize(scope)
|
|
28
|
+
@position = normalize_position(position)
|
|
29
|
+
@transaction_aware = transaction_aware == true
|
|
30
|
+
Ractor.make_shareable(self)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Whether transaction boundaries should be preserved.
|
|
34
|
+
#
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def transaction_aware? = transaction_aware
|
|
37
|
+
|
|
38
|
+
# Derive an ordering key for an event.
|
|
39
|
+
#
|
|
40
|
+
# @param event [ChangeEvent] event to classify
|
|
41
|
+
# @return [OrderingKey, nil]
|
|
42
|
+
def key_for(event)
|
|
43
|
+
return nil if scope == OrderingScope::NONE
|
|
44
|
+
|
|
45
|
+
OrderingKey.new(scope: scope, components: key_components(event))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Derive an event position for an event.
|
|
49
|
+
#
|
|
50
|
+
# @param event [ChangeEvent] event to classify
|
|
51
|
+
# @return [EventPosition]
|
|
52
|
+
def position_for(event)
|
|
53
|
+
EventPosition.new(
|
|
54
|
+
strategy: position,
|
|
55
|
+
value: event.public_send(position),
|
|
56
|
+
transaction_id: event.transaction_id,
|
|
57
|
+
sequence_number: event.sequence_number,
|
|
58
|
+
occurred_at: event.occurred_at
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Convert the policy into a Ractor-shareable hash.
|
|
63
|
+
#
|
|
64
|
+
# @return [Hash{String=>Object}]
|
|
65
|
+
def to_h
|
|
66
|
+
Ractor.make_shareable({
|
|
67
|
+
'scope' => scope,
|
|
68
|
+
'position' => position,
|
|
69
|
+
'transaction_aware' => transaction_aware
|
|
70
|
+
}.freeze)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Normalize the position strategy.
|
|
76
|
+
#
|
|
77
|
+
# @param position [#to_sym] position strategy
|
|
78
|
+
# @return [Symbol]
|
|
79
|
+
def normalize_position(position)
|
|
80
|
+
value = position.to_sym
|
|
81
|
+
return value if SUPPORTED_POSITIONS.include?(value)
|
|
82
|
+
|
|
83
|
+
raise InvalidOrderingPositionError, "unsupported CDC ordering position: #{position.inspect}"
|
|
84
|
+
rescue NoMethodError
|
|
85
|
+
raise InvalidOrderingPositionError, "unsupported CDC ordering position: #{position.inspect}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Build the components for the current scope.
|
|
89
|
+
#
|
|
90
|
+
# @param event [ChangeEvent]
|
|
91
|
+
# @return [Hash]
|
|
92
|
+
def key_components(event)
|
|
93
|
+
case scope
|
|
94
|
+
when OrderingScope::GLOBAL
|
|
95
|
+
{}
|
|
96
|
+
when OrderingScope::TRANSACTION
|
|
97
|
+
{ transaction_id: event.transaction_id }
|
|
98
|
+
when OrderingScope::RELATION
|
|
99
|
+
{ schema: event.schema, table: event.table }
|
|
100
|
+
when OrderingScope::PRIMARY_KEY
|
|
101
|
+
{ schema: event.schema, table: event.table, primary_key: event.primary_key }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|