cdc-core 0.1.2 → 0.1.3
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 +14 -0
- data/README.md +12 -4
- data/lib/cdc/core/change_event.rb +10 -5
- data/lib/cdc/core/column_change.rb +4 -4
- data/lib/cdc/core/composite_processor.rb +3 -3
- data/lib/cdc/core/event_metadata.rb +14 -9
- data/lib/cdc/core/event_position.rb +1 -1
- data/lib/cdc/core/filter.rb +8 -8
- data/lib/cdc/core/observer.rb +24 -19
- data/lib/cdc/core/ordering_key.rb +8 -3
- data/lib/cdc/core/ordering_policy.rb +12 -9
- data/lib/cdc/core/pipeline.rb +5 -5
- data/lib/cdc/core/processor.rb +7 -7
- data/lib/cdc/core/processor_chain.rb +1 -1
- data/lib/cdc/core/processor_result.rb +18 -14
- data/lib/cdc/core/router.rb +6 -6
- data/lib/cdc/core/source_adapter.rb +2 -2
- data/lib/cdc/core/transaction_envelope.rb +9 -4
- data/lib/cdc/core/version.rb +1 -1
- data/sig/cdc/core/change_event.rbs +34 -216
- data/sig/cdc/core/column_change.rbs +4 -43
- data/sig/cdc/core/composite_processor.rbs +11 -54
- data/sig/cdc/core/errors.rbs +1 -7
- data/sig/cdc/core/event_metadata.rbs +7 -39
- data/sig/cdc/core/event_position.rbs +12 -12
- data/sig/cdc/core/filter.rbs +11 -62
- data/sig/cdc/core/null_observer.rbs +1 -2
- data/sig/cdc/core/observer.rbs +11 -13
- data/sig/cdc/core/operation.rbs +7 -29
- data/sig/cdc/core/ordering_key.rbs +5 -7
- data/sig/cdc/core/ordering_policy.rbs +10 -15
- data/sig/cdc/core/ordering_scope.rbs +7 -5
- data/sig/cdc/core/pipeline.rbs +9 -54
- data/sig/cdc/core/processor.rbs +5 -56
- data/sig/cdc/core/processor_chain.rbs +8 -35
- data/sig/cdc/core/processor_result.rbs +32 -125
- data/sig/cdc/core/router.rbs +12 -13
- data/sig/cdc/core/source_adapter.rbs +2 -4
- data/sig/cdc/core/transaction_envelope.rbs +17 -71
- data/sig/cdc/core/version.rbs +1 -2
- data/sig/cdc/core.rbs +28 -7
- data/sig/cdc_core.rbs +4 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9ffbf7ae1292484005aad90e904713835e5b3fc762e56e40484169c48d509156
|
|
4
|
+
data.tar.gz: d7edeb40f694be694535497ceb20ed663830a336d6862faae328cfafa01644d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9b513c8241e683b3bc28f8d249339553bc91e8a93770384aaafc641df6617f98618c16374a5d1bdda7ab06e7609389c9e758586244a501e8dba72071eaed7c6
|
|
7
|
+
data.tar.gz: 39be75beb53af0cbeb3c4a5047045ff329e4510ba20c2f5ed8409f72d3e5c6f42b45873fa419da24eea23f0d5ce4e78573913731a760de4544cf4789ac1dc6ee
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.3] - 2026-06-11
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added generated API documentation badges and deployment-safe documentation links.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Fixed RBS validation issues and warnings across core signatures and typed metadata defaults.
|
|
12
|
+
- Fixed edge-case ordering policy behavior for unknown internal scopes.
|
|
13
|
+
- Corrected README and docs examples for current processor composition constructors.
|
|
14
|
+
- Improved YARD tag descriptions across core API comments.
|
|
15
|
+
- Polished API and architecture documentation wording, links, and Ruby snippet style.
|
|
16
|
+
|
|
3
17
|
## [0.1.2] - 2026-06-10
|
|
4
18
|
|
|
5
19
|
- Added `CDC::Core::ProcessorChain` that feeds the successful value from one processor into the next processor
|
data/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# cdc-core
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/cdc-core)
|
|
4
|
+
[](https://github.com/kanutocd/cdc-core/actions)
|
|
5
|
+
[](https://www.ruby-lang.org/en/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
3
10
|
Shared Change Data Capture vocabulary for Ruby.
|
|
4
11
|
|
|
5
12
|
`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.
|
|
@@ -114,11 +121,11 @@ It is the fiber-friendly runtime path.
|
|
|
114
121
|
## Installation
|
|
115
122
|
|
|
116
123
|
```ruby
|
|
117
|
-
gem
|
|
124
|
+
gem 'cdc-core'
|
|
118
125
|
```
|
|
119
126
|
|
|
120
127
|
```ruby
|
|
121
|
-
require
|
|
128
|
+
require 'cdc/core'
|
|
122
129
|
```
|
|
123
130
|
|
|
124
131
|
## Change Events
|
|
@@ -310,9 +317,10 @@ docs/**/*.md
|
|
|
310
317
|
|
|
311
318
|
```bash
|
|
312
319
|
bundle exec rake
|
|
313
|
-
bundle exec
|
|
320
|
+
bundle exec rake rbs:validate
|
|
321
|
+
bundle exec yard doc
|
|
314
322
|
```
|
|
315
323
|
|
|
316
324
|
## License
|
|
317
325
|
|
|
318
|
-
[MIT](
|
|
326
|
+
[MIT](LICENSE.txt)
|
|
@@ -9,6 +9,11 @@ module CDC
|
|
|
9
9
|
# as operation, schema, table, before/after values, primary key, LSN, and
|
|
10
10
|
# metadata.
|
|
11
11
|
class ChangeEvent
|
|
12
|
+
EMPTY_METADATA = Ractor.make_shareable(
|
|
13
|
+
{} # : Hash[untyped, untyped]
|
|
14
|
+
.freeze
|
|
15
|
+
)
|
|
16
|
+
|
|
12
17
|
# @return [Symbol] normalized CDC operation
|
|
13
18
|
# @return [String] database schema name
|
|
14
19
|
# @return [String] database table name
|
|
@@ -38,7 +43,7 @@ module CDC
|
|
|
38
43
|
# @param metadata [Hash, EventMetadata] additional event metadata
|
|
39
44
|
def initialize(operation:, schema:, table:, old_values: nil, new_values: nil, primary_key: nil,
|
|
40
45
|
transaction_id: nil, commit_lsn: nil, sequence_number: nil, occurred_at: nil,
|
|
41
|
-
metadata:
|
|
46
|
+
metadata: EMPTY_METADATA)
|
|
42
47
|
@operation = Operation.normalize(operation)
|
|
43
48
|
@schema = String(schema).freeze
|
|
44
49
|
@table = String(table).freeze
|
|
@@ -64,7 +69,7 @@ module CDC
|
|
|
64
69
|
|
|
65
70
|
# Fully qualified table name in schema.table form.
|
|
66
71
|
#
|
|
67
|
-
# @return [String]
|
|
72
|
+
# @return [String] fully qualified table name
|
|
68
73
|
def qualified_table_name = "#{schema}.#{table}".freeze
|
|
69
74
|
|
|
70
75
|
# Compute changed columns by comparing old and new values.
|
|
@@ -83,7 +88,7 @@ module CDC
|
|
|
83
88
|
|
|
84
89
|
# Convert the event into a Ractor-shareable hash.
|
|
85
90
|
#
|
|
86
|
-
# @return [Hash{String=>Object,nil}]
|
|
91
|
+
# @return [Hash{String=>Object,nil}] Ractor-shareable event representation
|
|
87
92
|
def to_h
|
|
88
93
|
Ractor.make_shareable({
|
|
89
94
|
'operation' => operation,
|
|
@@ -104,8 +109,8 @@ module CDC
|
|
|
104
109
|
|
|
105
110
|
# Convert a hash into immutable EventMetadata storage, preserving nil.
|
|
106
111
|
#
|
|
107
|
-
# @param hash [Hash, nil]
|
|
108
|
-
# @return [Hash, nil]
|
|
112
|
+
# @param hash [Hash, nil] values to normalize
|
|
113
|
+
# @return [Hash, nil] normalized shareable values, or nil when no values were provided
|
|
109
114
|
def freeze_hash_or_nil(hash)
|
|
110
115
|
return nil if hash.nil?
|
|
111
116
|
|
|
@@ -27,14 +27,14 @@ module CDC
|
|
|
27
27
|
|
|
28
28
|
# Whether the old and new values differ.
|
|
29
29
|
#
|
|
30
|
-
# @return [Boolean]
|
|
30
|
+
# @return [Boolean] true when the old and new values differ
|
|
31
31
|
def changed?
|
|
32
32
|
old_value != new_value
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Convert the change into a Ractor-shareable hash.
|
|
36
36
|
#
|
|
37
|
-
# @return [Hash{String=>Object,nil}]
|
|
37
|
+
# @return [Hash{String=>Object,nil}] Ractor-shareable column change representation
|
|
38
38
|
def to_h
|
|
39
39
|
Ractor.make_shareable({ 'name' => name, 'old_value' => old_value, 'new_value' => new_value }.freeze)
|
|
40
40
|
end
|
|
@@ -43,8 +43,8 @@ module CDC
|
|
|
43
43
|
|
|
44
44
|
# Convert a value into a Ractor-shareable representation.
|
|
45
45
|
#
|
|
46
|
-
# @param value [Object, nil]
|
|
47
|
-
# @return [Object, String, nil]
|
|
46
|
+
# @param value [Object, nil] value to normalize
|
|
47
|
+
# @return [Object, String, nil] shareable value or inspect string fallback
|
|
48
48
|
def make_value_shareable(value)
|
|
49
49
|
Ractor.make_shareable(value)
|
|
50
50
|
rescue Ractor::Error
|
|
@@ -39,14 +39,14 @@ module CDC
|
|
|
39
39
|
|
|
40
40
|
# Processors that declared Ractor safety.
|
|
41
41
|
#
|
|
42
|
-
# @return [Array<Processor>]
|
|
42
|
+
# @return [Array<Processor>] processors that declared Ractor safety
|
|
43
43
|
def ractor_safe_processors
|
|
44
44
|
processors.select(&:ractor_safe?).freeze
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# Processors that should remain sequential in the core runtime.
|
|
48
48
|
#
|
|
49
|
-
# @return [Array<Processor>]
|
|
49
|
+
# @return [Array<Processor>] processors that should run sequentially
|
|
50
50
|
def sequential_processors
|
|
51
51
|
processors.reject(&:ractor_safe?).freeze
|
|
52
52
|
end
|
|
@@ -57,7 +57,7 @@ module CDC
|
|
|
57
57
|
#
|
|
58
58
|
# @param result [Object] raw processor result
|
|
59
59
|
# @param event [ChangeEvent] processed event
|
|
60
|
-
# @return [ProcessorResult]
|
|
60
|
+
# @return [ProcessorResult] normalized processor result
|
|
61
61
|
def normalize_result(result, event)
|
|
62
62
|
return result if result.is_a?(ProcessorResult)
|
|
63
63
|
|
|
@@ -8,13 +8,18 @@ module CDC
|
|
|
8
8
|
# are recursively converted into Ractor-shareable objects. Values that Ruby
|
|
9
9
|
# cannot make shareable are stored as frozen #inspect strings.
|
|
10
10
|
class EventMetadata
|
|
11
|
+
EMPTY_DATA = Ractor.make_shareable(
|
|
12
|
+
{} # : Hash[untyped, untyped]
|
|
13
|
+
.freeze
|
|
14
|
+
)
|
|
15
|
+
|
|
11
16
|
# @return [Hash{String=>Object}] normalized metadata
|
|
12
17
|
attr_reader :data
|
|
13
18
|
|
|
14
19
|
# Build metadata from a hash-like structure.
|
|
15
20
|
#
|
|
16
21
|
# @param data [Hash] metadata values
|
|
17
|
-
def initialize(data =
|
|
22
|
+
def initialize(data = EMPTY_DATA)
|
|
18
23
|
@data = deep_shareable_hash(data)
|
|
19
24
|
Ractor.make_shareable(self)
|
|
20
25
|
end
|
|
@@ -22,7 +27,7 @@ module CDC
|
|
|
22
27
|
# Fetch a metadata value by string or symbol key.
|
|
23
28
|
#
|
|
24
29
|
# @param key [String, Symbol] metadata key
|
|
25
|
-
# @return [Object, nil]
|
|
30
|
+
# @return [Object, nil] metadata value for the given key
|
|
26
31
|
def [](key)
|
|
27
32
|
string_key = key.to_s
|
|
28
33
|
return data[string_key] if data.key?(string_key)
|
|
@@ -32,7 +37,7 @@ module CDC
|
|
|
32
37
|
|
|
33
38
|
# Return the normalized Ractor-shareable hash.
|
|
34
39
|
#
|
|
35
|
-
# @return [Hash{String=>Object}]
|
|
40
|
+
# @return [Hash{String=>Object}] normalized Ractor-shareable metadata
|
|
36
41
|
def to_h
|
|
37
42
|
data
|
|
38
43
|
end
|
|
@@ -41,8 +46,8 @@ module CDC
|
|
|
41
46
|
|
|
42
47
|
# Recursively normalize and freeze a hash.
|
|
43
48
|
#
|
|
44
|
-
# @param hash [Hash]
|
|
45
|
-
# @return [Hash{String=>Object}]
|
|
49
|
+
# @param hash [Hash] metadata hash to normalize
|
|
50
|
+
# @return [Hash{String=>Object}] recursively normalized shareable hash
|
|
46
51
|
def deep_shareable_hash(hash)
|
|
47
52
|
converted = hash.each_with_object(
|
|
48
53
|
{} # : Hash[String, untyped]
|
|
@@ -54,16 +59,16 @@ module CDC
|
|
|
54
59
|
|
|
55
60
|
# Normalize metadata keys to frozen strings.
|
|
56
61
|
#
|
|
57
|
-
# @param key [Object]
|
|
58
|
-
# @return [String]
|
|
62
|
+
# @param key [Object] metadata key to normalize
|
|
63
|
+
# @return [String] frozen string key
|
|
59
64
|
def normalize_key(key)
|
|
60
65
|
key.to_s.freeze
|
|
61
66
|
end
|
|
62
67
|
|
|
63
68
|
# Normalize a metadata value into a shareable representation.
|
|
64
69
|
#
|
|
65
|
-
# @param value [Object]
|
|
66
|
-
# @return [Object]
|
|
70
|
+
# @param value [Object] metadata value to normalize
|
|
71
|
+
# @return [Object] Ractor-shareable value or inspect string fallback
|
|
67
72
|
def normalize_value(value)
|
|
68
73
|
case value
|
|
69
74
|
when Hash
|
|
@@ -33,7 +33,7 @@ module CDC
|
|
|
33
33
|
|
|
34
34
|
# Convert the position into a Ractor-shareable hash.
|
|
35
35
|
#
|
|
36
|
-
# @return [Hash{String=>Object,nil}]
|
|
36
|
+
# @return [Hash{String=>Object,nil}] Ractor-shareable position representation
|
|
37
37
|
def to_h
|
|
38
38
|
Ractor.make_shareable({
|
|
39
39
|
'strategy' => strategy,
|
data/lib/cdc/core/filter.rb
CHANGED
|
@@ -10,31 +10,31 @@ module CDC
|
|
|
10
10
|
class Filter
|
|
11
11
|
# Match every event.
|
|
12
12
|
#
|
|
13
|
-
# @return [Filter]
|
|
13
|
+
# @return [Filter] filter that matches every event
|
|
14
14
|
def self.all = new { |_event| true }
|
|
15
15
|
|
|
16
16
|
# Match events from a schema.
|
|
17
17
|
#
|
|
18
18
|
# @param name [#to_s] schema name
|
|
19
|
-
# @return [Filter]
|
|
19
|
+
# @return [Filter] filter matching the given schema
|
|
20
20
|
def self.schema(name) = new { |event| event.schema == name.to_s }
|
|
21
21
|
|
|
22
22
|
# Match events from a table regardless of schema.
|
|
23
23
|
#
|
|
24
24
|
# @param name [#to_s] table name
|
|
25
|
-
# @return [Filter]
|
|
25
|
+
# @return [Filter] filter matching the given table
|
|
26
26
|
def self.table(name) = new { |event| event.table == name.to_s }
|
|
27
27
|
|
|
28
28
|
# Match events from a fully qualified schema.table name.
|
|
29
29
|
#
|
|
30
30
|
# @param name [#to_s] qualified table name
|
|
31
|
-
# @return [Filter]
|
|
31
|
+
# @return [Filter] filter matching the given qualified table name
|
|
32
32
|
def self.qualified_table(name) = new { |event| event.qualified_table_name == name.to_s }
|
|
33
33
|
|
|
34
34
|
# Match events by operation.
|
|
35
35
|
#
|
|
36
36
|
# @param operation [#to_sym] CDC operation
|
|
37
|
-
# @return [Filter]
|
|
37
|
+
# @return [Filter] filter matching the given operation
|
|
38
38
|
def self.operation(operation) = new { |event| event.operation == Operation.normalize(operation) }
|
|
39
39
|
|
|
40
40
|
# Build a custom filter.
|
|
@@ -51,7 +51,7 @@ module CDC
|
|
|
51
51
|
# Whether this filter matches an event.
|
|
52
52
|
#
|
|
53
53
|
# @param event [ChangeEvent] event to test
|
|
54
|
-
# @return [Boolean]
|
|
54
|
+
# @return [Boolean] true when the predicate matches exactly
|
|
55
55
|
def match?(event)
|
|
56
56
|
@predicate.call(event) == true
|
|
57
57
|
end
|
|
@@ -60,7 +60,7 @@ module CDC
|
|
|
60
60
|
# Compose this filter with another filter using logical AND.
|
|
61
61
|
#
|
|
62
62
|
# @param other [Filter] other filter
|
|
63
|
-
# @return [Filter]
|
|
63
|
+
# @return [Filter] filter that matches only when both filters match
|
|
64
64
|
def &(other)
|
|
65
65
|
self.class.new { |event| match?(event) && other.match?(event) }
|
|
66
66
|
end
|
|
@@ -68,7 +68,7 @@ module CDC
|
|
|
68
68
|
# Compose this filter with another filter using logical OR.
|
|
69
69
|
#
|
|
70
70
|
# @param other [Filter] other filter
|
|
71
|
-
# @return [Filter]
|
|
71
|
+
# @return [Filter] filter that matches when either filter matches
|
|
72
72
|
def |(other)
|
|
73
73
|
self.class.new { |event| match?(event) || other.match?(event) }
|
|
74
74
|
end
|
data/lib/cdc/core/observer.rb
CHANGED
|
@@ -18,8 +18,8 @@ module CDC
|
|
|
18
18
|
|
|
19
19
|
# Build a canonical metric tag set for a CDC work item or result.
|
|
20
20
|
#
|
|
21
|
-
# @param payload [ChangeEvent, TransactionEnvelope, ProcessorResult, Array]
|
|
22
|
-
# @return [Hash{String=>Object}]
|
|
21
|
+
# @param payload [ChangeEvent, TransactionEnvelope, ProcessorResult, Array] payload to describe
|
|
22
|
+
# @return [Hash{String=>Object}] canonical metric tags for the payload
|
|
23
23
|
def self.metric_tags(payload)
|
|
24
24
|
tags = {} # : Hash[String, untyped]
|
|
25
25
|
case payload
|
|
@@ -40,46 +40,46 @@ module CDC
|
|
|
40
40
|
|
|
41
41
|
# Canonical metric name for the start hook.
|
|
42
42
|
#
|
|
43
|
-
# @return [String]
|
|
43
|
+
# @return [String] canonical dispatch-started metric name
|
|
44
44
|
def self.started_metric_name = METRIC_NAMES.fetch(:dispatch_started)
|
|
45
45
|
|
|
46
46
|
# Canonical metric name for the success hook.
|
|
47
47
|
#
|
|
48
|
-
# @return [String]
|
|
48
|
+
# @return [String] canonical dispatch-succeeded metric name
|
|
49
49
|
def self.succeeded_metric_name = METRIC_NAMES.fetch(:dispatch_succeeded)
|
|
50
50
|
|
|
51
51
|
# Canonical metric name for the failure hook.
|
|
52
52
|
#
|
|
53
|
-
# @return [String]
|
|
53
|
+
# @return [String] canonical dispatch-failed metric name
|
|
54
54
|
def self.failed_metric_name = METRIC_NAMES.fetch(:dispatch_failed)
|
|
55
55
|
|
|
56
56
|
# Canonical metric name for the skip hook.
|
|
57
57
|
#
|
|
58
|
-
# @return [String]
|
|
58
|
+
# @return [String] canonical dispatch-skipped metric name
|
|
59
59
|
def self.skipped_metric_name = METRIC_NAMES.fetch(:dispatch_skipped)
|
|
60
60
|
|
|
61
61
|
# Called before a work item is dispatched.
|
|
62
62
|
#
|
|
63
|
-
# @param _event [ChangeEvent, TransactionEnvelope, Array]
|
|
64
|
-
# @return [void]
|
|
63
|
+
# @param _event [ChangeEvent, TransactionEnvelope, Array] work item about to be dispatched
|
|
64
|
+
# @return [void] no return value
|
|
65
65
|
def dispatch_started(_event); end
|
|
66
66
|
|
|
67
67
|
# Called after a work item is processed successfully.
|
|
68
68
|
#
|
|
69
|
-
# @param _result [ProcessorResult, Array<ProcessorResult>]
|
|
70
|
-
# @return [void]
|
|
69
|
+
# @param _result [ProcessorResult, Array<ProcessorResult>] successful processor result or results
|
|
70
|
+
# @return [void] no return value
|
|
71
71
|
def dispatch_succeeded(_result); end
|
|
72
72
|
|
|
73
73
|
# Called after a work item fails.
|
|
74
74
|
#
|
|
75
|
-
# @param _result [ProcessorResult]
|
|
76
|
-
# @return [void]
|
|
75
|
+
# @param _result [ProcessorResult] failed processor result
|
|
76
|
+
# @return [void] no return value
|
|
77
77
|
def dispatch_failed(_result); end
|
|
78
78
|
|
|
79
79
|
# Called when a work item is filtered or skipped.
|
|
80
80
|
#
|
|
81
|
-
# @param _result [ProcessorResult]
|
|
82
|
-
# @return [void]
|
|
81
|
+
# @param _result [ProcessorResult] skipped processor result
|
|
82
|
+
# @return [void] no return value
|
|
83
83
|
def dispatch_skipped(_result); end
|
|
84
84
|
|
|
85
85
|
private_class_method def self.change_event_metric_tags(event)
|
|
@@ -102,14 +102,19 @@ module CDC
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
private_class_method def self.processor_result_metric_tags(result)
|
|
105
|
-
{
|
|
105
|
+
tags = {
|
|
106
106
|
'kind' => 'processor_result',
|
|
107
107
|
'status' => result.status,
|
|
108
108
|
'retryable' => result.retryable?
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
} # : Hash[String, untyped]
|
|
110
|
+
|
|
111
|
+
processor_name = result.processor_name
|
|
112
|
+
tags['processor'] = processor_name if processor_name
|
|
113
|
+
|
|
114
|
+
failure_reason = result.failure_reason
|
|
115
|
+
tags['failure_reason'] = failure_reason if result.failure? && failure_reason
|
|
116
|
+
|
|
117
|
+
tags
|
|
113
118
|
end
|
|
114
119
|
|
|
115
120
|
private_class_method def self.batch_metric_tags(batch)
|
|
@@ -7,6 +7,11 @@ module CDC
|
|
|
7
7
|
# OrderingKey captures the scope plus the components that define a
|
|
8
8
|
# particular ordered lane. It does not choose an execution strategy.
|
|
9
9
|
class OrderingKey
|
|
10
|
+
EMPTY_COMPONENTS = Ractor.make_shareable(
|
|
11
|
+
{} # : Hash[untyped, untyped]
|
|
12
|
+
.freeze
|
|
13
|
+
)
|
|
14
|
+
|
|
10
15
|
# @return [Symbol] ordering scope
|
|
11
16
|
# @return [Hash{String=>Object}] normalized key components
|
|
12
17
|
attr_reader :scope, :components
|
|
@@ -15,7 +20,7 @@ module CDC
|
|
|
15
20
|
#
|
|
16
21
|
# @param scope [#to_sym] ordering scope
|
|
17
22
|
# @param components [Hash] key components
|
|
18
|
-
def initialize(scope:, components:
|
|
23
|
+
def initialize(scope:, components: EMPTY_COMPONENTS)
|
|
19
24
|
@scope = OrderingScope.normalize(scope)
|
|
20
25
|
@components = EventMetadata.new(components).to_h
|
|
21
26
|
Ractor.make_shareable(self)
|
|
@@ -23,12 +28,12 @@ module CDC
|
|
|
23
28
|
|
|
24
29
|
# Whether the key has no components.
|
|
25
30
|
#
|
|
26
|
-
# @return [Boolean]
|
|
31
|
+
# @return [Boolean] true when the key has no components
|
|
27
32
|
def empty? = components.empty?
|
|
28
33
|
|
|
29
34
|
# Convert the key into a Ractor-shareable hash.
|
|
30
35
|
#
|
|
31
|
-
# @return [Hash{String=>Object}]
|
|
36
|
+
# @return [Hash{String=>Object}] Ractor-shareable ordering key representation
|
|
32
37
|
def to_h
|
|
33
38
|
Ractor.make_shareable({
|
|
34
39
|
'scope' => scope,
|
|
@@ -32,23 +32,26 @@ module CDC
|
|
|
32
32
|
|
|
33
33
|
# Whether transaction boundaries should be preserved.
|
|
34
34
|
#
|
|
35
|
-
# @return [Boolean]
|
|
35
|
+
# @return [Boolean] true when transaction boundaries should be preserved
|
|
36
36
|
def transaction_aware? = transaction_aware
|
|
37
37
|
|
|
38
38
|
# Derive an ordering key for an event.
|
|
39
39
|
#
|
|
40
40
|
# @param event [ChangeEvent] event to classify
|
|
41
|
-
# @return [OrderingKey, nil]
|
|
41
|
+
# @return [OrderingKey, nil] ordering key for the event, or nil when no key applies
|
|
42
42
|
def key_for(event)
|
|
43
43
|
return nil if scope == OrderingScope::NONE
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
components = key_components(event)
|
|
46
|
+
return nil if components.nil?
|
|
47
|
+
|
|
48
|
+
OrderingKey.new(scope: scope, components: components)
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
# Derive an event position for an event.
|
|
49
52
|
#
|
|
50
53
|
# @param event [ChangeEvent] event to classify
|
|
51
|
-
# @return [EventPosition]
|
|
54
|
+
# @return [EventPosition] position metadata for the event
|
|
52
55
|
def position_for(event)
|
|
53
56
|
EventPosition.new(
|
|
54
57
|
strategy: position,
|
|
@@ -61,7 +64,7 @@ module CDC
|
|
|
61
64
|
|
|
62
65
|
# Convert the policy into a Ractor-shareable hash.
|
|
63
66
|
#
|
|
64
|
-
# @return [Hash{String=>Object}]
|
|
67
|
+
# @return [Hash{String=>Object}] Ractor-shareable policy representation
|
|
65
68
|
def to_h
|
|
66
69
|
Ractor.make_shareable({
|
|
67
70
|
'scope' => scope,
|
|
@@ -75,7 +78,7 @@ module CDC
|
|
|
75
78
|
# Normalize the position strategy.
|
|
76
79
|
#
|
|
77
80
|
# @param position [#to_sym] position strategy
|
|
78
|
-
# @return [Symbol]
|
|
81
|
+
# @return [Symbol] normalized supported position strategy
|
|
79
82
|
def normalize_position(position)
|
|
80
83
|
value = position.to_sym
|
|
81
84
|
return value if SUPPORTED_POSITIONS.include?(value)
|
|
@@ -87,12 +90,12 @@ module CDC
|
|
|
87
90
|
|
|
88
91
|
# Build the components for the current scope.
|
|
89
92
|
#
|
|
90
|
-
# @param event [ChangeEvent]
|
|
91
|
-
# @return [Hash]
|
|
93
|
+
# @param event [ChangeEvent] event to classify
|
|
94
|
+
# @return [Hash, nil] key components for the current scope, or nil for an unknown internal scope
|
|
92
95
|
def key_components(event)
|
|
93
96
|
case scope
|
|
94
97
|
when OrderingScope::GLOBAL
|
|
95
|
-
{}
|
|
98
|
+
{} # : Hash[untyped, untyped]
|
|
96
99
|
when OrderingScope::TRANSACTION
|
|
97
100
|
{ transaction_id: event.transaction_id }
|
|
98
101
|
when OrderingScope::RELATION
|
data/lib/cdc/core/pipeline.rb
CHANGED
|
@@ -28,7 +28,7 @@ module CDC
|
|
|
28
28
|
# Process one event through the pipeline.
|
|
29
29
|
#
|
|
30
30
|
# @param event [ChangeEvent] event to process
|
|
31
|
-
# @return [ProcessorResult]
|
|
31
|
+
# @return [ProcessorResult] result for the event
|
|
32
32
|
def process(event)
|
|
33
33
|
observer.dispatch_started(event)
|
|
34
34
|
return ProcessorResult.skipped(event, metadata: { reason: 'filtered' }) unless matches?(event)
|
|
@@ -45,7 +45,7 @@ module CDC
|
|
|
45
45
|
# Process many events in order.
|
|
46
46
|
#
|
|
47
47
|
# @param events [Enumerable<ChangeEvent>] events to process
|
|
48
|
-
# @return [Array<ProcessorResult>]
|
|
48
|
+
# @return [Array<ProcessorResult>] results in input order
|
|
49
49
|
def process_many(events)
|
|
50
50
|
events.map { |event| process(event) }.freeze
|
|
51
51
|
end
|
|
@@ -54,8 +54,8 @@ module CDC
|
|
|
54
54
|
|
|
55
55
|
# Check whether every filter matches an event.
|
|
56
56
|
#
|
|
57
|
-
# @param event [ChangeEvent]
|
|
58
|
-
# @return [Boolean]
|
|
57
|
+
# @param event [ChangeEvent] event to test
|
|
58
|
+
# @return [Boolean] true when every filter matches
|
|
59
59
|
def matches?(event)
|
|
60
60
|
filters.all? { |filter| filter.match?(event) }
|
|
61
61
|
end
|
|
@@ -64,7 +64,7 @@ module CDC
|
|
|
64
64
|
#
|
|
65
65
|
# @param result [Object] raw processor result
|
|
66
66
|
# @param event [ChangeEvent] processed event
|
|
67
|
-
# @return [ProcessorResult]
|
|
67
|
+
# @return [ProcessorResult] normalized processor result
|
|
68
68
|
def normalize_result(result, event)
|
|
69
69
|
return result if result.is_a?(ProcessorResult)
|
|
70
70
|
|
data/lib/cdc/core/processor.rb
CHANGED
|
@@ -10,21 +10,21 @@ module CDC
|
|
|
10
10
|
class Processor
|
|
11
11
|
# Mark this processor class as safe to execute in Ractor-aware runtimes.
|
|
12
12
|
#
|
|
13
|
-
# @return [true]
|
|
13
|
+
# @return [true] marker value confirming the class was marked Ractor-safe
|
|
14
14
|
def self.ractor_safe!
|
|
15
15
|
@ractor_safe = true
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# Whether this processor class has declared Ractor safety.
|
|
19
19
|
#
|
|
20
|
-
# @return [Boolean]
|
|
20
|
+
# @return [Boolean] true when the class has declared Ractor safety
|
|
21
21
|
def self.ractor_safe?
|
|
22
22
|
@ractor_safe == true
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
# Whether this processor instance is Ractor-safe.
|
|
26
26
|
#
|
|
27
|
-
# @return [Boolean]
|
|
27
|
+
# @return [Boolean] true when the instance's class has declared Ractor safety
|
|
28
28
|
def ractor_safe?
|
|
29
29
|
self.class.ractor_safe?
|
|
30
30
|
end
|
|
@@ -34,7 +34,7 @@ module CDC
|
|
|
34
34
|
# Runtime layers can call this before dispatch begins. The default
|
|
35
35
|
# implementation is a no-op.
|
|
36
36
|
#
|
|
37
|
-
# @return [self]
|
|
37
|
+
# @return [self] started processor instance
|
|
38
38
|
def start
|
|
39
39
|
self
|
|
40
40
|
end
|
|
@@ -44,7 +44,7 @@ module CDC
|
|
|
44
44
|
# Runtime layers can call this during shutdown. The default implementation
|
|
45
45
|
# is a no-op.
|
|
46
46
|
#
|
|
47
|
-
# @return [self]
|
|
47
|
+
# @return [self] stopped processor instance
|
|
48
48
|
def stop
|
|
49
49
|
self
|
|
50
50
|
end
|
|
@@ -54,7 +54,7 @@ module CDC
|
|
|
54
54
|
# Runtime layers can call this before shutdown or checkpoints. The
|
|
55
55
|
# default implementation is a no-op.
|
|
56
56
|
#
|
|
57
|
-
# @return [self]
|
|
57
|
+
# @return [self] flushed processor instance
|
|
58
58
|
def flush
|
|
59
59
|
self
|
|
60
60
|
end
|
|
@@ -63,7 +63,7 @@ module CDC
|
|
|
63
63
|
#
|
|
64
64
|
# The default implementation assumes the processor is healthy.
|
|
65
65
|
#
|
|
66
|
-
# @return [Boolean]
|
|
66
|
+
# @return [Boolean] true when the processor can accept work
|
|
67
67
|
def healthy?
|
|
68
68
|
true
|
|
69
69
|
end
|
|
@@ -34,7 +34,7 @@ module CDC
|
|
|
34
34
|
# value is the final ProcessorResult produced by the chain.
|
|
35
35
|
#
|
|
36
36
|
# @param input [Object] initial input for the first processor
|
|
37
|
-
# @return [ProcessorResult] final processor result or the first failed
|
|
37
|
+
# @return [ProcessorResult] final processor result or the first failed or skipped result
|
|
38
38
|
def process(input)
|
|
39
39
|
observer.dispatch_started(input)
|
|
40
40
|
current_input = input
|