cdc-parallel 0.2.2 → 0.2.4

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.
@@ -2,22 +2,58 @@
2
2
 
3
3
  module CDC
4
4
  module Parallel
5
- # Normalizes values returned by workers.
5
+ # Converts raw worker responses into `CDC::Core::ProcessorResult` objects.
6
+ #
7
+ # Ractors cannot freely share arbitrary mutable Ruby objects. Worker
8
+ # responses must therefore be normalized into shareable payloads before they
9
+ # cross back to the caller. `ResultCollector` owns that small translation
10
+ # boundary.
11
+ #
12
+ # The worker side uses {worker_success} and {worker_failure}. The caller side
13
+ # uses {normalize} to convert those payloads into the public result contract.
14
+ #
15
+ # @example Normalizing a processor return value
16
+ # response = CDC::Parallel::ResultCollector.worker_success(value)
17
+ # result = CDC::Parallel::ResultCollector.normalize(response)
18
+ #
19
+ # @example Normalizing a worker exception
20
+ # response = CDC::Parallel::ResultCollector.worker_failure(error)
21
+ # result = CDC::Parallel::ResultCollector.normalize(response)
22
+ # result.failure? #=> true
23
+ #
24
+ # @api public
6
25
  class ResultCollector
26
+ # Internal marker used to identify serialized worker failures.
27
+ #
28
+ # @return [Symbol]
7
29
  FAILURE_MARKER = :__cdc_parallel_failure__
8
30
 
9
31
  # Build a shareable success payload that can safely cross a Ractor boundary.
10
32
  #
33
+ # If the processor already returned a `CDC::Core::ProcessorResult`, that
34
+ # result is preserved. Any other shareable value will later be wrapped in a
35
+ # success result by {normalize}.
36
+ #
11
37
  # @param value [Object]
38
+ # Processor return value.
39
+ # @raise [Ractor::Error]
40
+ # Raised by Ruby when the value cannot be made shareable.
12
41
  # @return [Object]
42
+ # Shareable success payload.
13
43
  def self.worker_success(value)
14
44
  ::Ractor.make_shareable(value)
15
45
  end
16
46
 
17
47
  # Build a shareable failure payload that can safely cross a Ractor boundary.
18
48
  #
49
+ # Exceptions themselves are not used as the cross-Ractor payload. Instead,
50
+ # the class name, message, and backtrace are serialized into a simple hash
51
+ # that can be reconstructed as a {ProcessorExecutionError} by {normalize}.
52
+ #
19
53
  # @param error [Exception]
54
+ # Exception raised inside a worker Ractor.
20
55
  # @return [Hash]
56
+ # Shareable serialized failure payload.
21
57
  def self.worker_failure(error)
22
58
  ::Ractor.make_shareable(
23
59
  {
@@ -29,9 +65,14 @@ module CDC
29
65
  )
30
66
  end
31
67
 
32
- # Normalize a worker return value into a ProcessorResult.
68
+ # Normalize a worker return value into a `CDC::Core::ProcessorResult`.
69
+ #
70
+ # Failure payloads become failed processor results containing a
71
+ # {ProcessorExecutionError}. Existing processor results are returned
72
+ # unchanged. Other values are wrapped in a successful processor result.
33
73
  #
34
74
  # @param value [Object]
75
+ # Raw worker response.
35
76
  # @return [CDC::Core::ProcessorResult]
36
77
  def self.normalize(value)
37
78
  if worker_failure?(value)
@@ -49,6 +90,12 @@ module CDC
49
90
  end
50
91
  end
51
92
 
93
+ # Return whether a value is a serialized worker failure payload.
94
+ #
95
+ # @param value [Object]
96
+ # Raw worker response.
97
+ # @return [Boolean]
98
+ # @api private
52
99
  def self.worker_failure?(value)
53
100
  value.is_a?(Hash) && value[:type] == FAILURE_MARKER
54
101
  end
@@ -2,10 +2,32 @@
2
2
 
3
3
  module CDC
4
4
  module Parallel
5
- # Routes supported CDC objects to the correct runtime pool.
5
+ # Routes normalized `cdc-core` work items to the matching parallel runtime
6
+ # primitive.
7
+ #
8
+ # `Router` is deliberately small. It does not inspect source-specific
9
+ # payloads, apply filters, decode database values, or decide scheduling
10
+ # policy. Its responsibility is only to look at the already-normalized
11
+ # `cdc-core` object shape and forward it to the pool that knows how to
12
+ # process that shape.
13
+ #
14
+ # @example Routing a single event
15
+ # router.process(change_event)
16
+ #
17
+ # @example Routing a transaction envelope
18
+ # router.process(transaction_envelope)
19
+ #
20
+ # @see CDC::Parallel::ProcessorPool
21
+ # @see CDC::Parallel::TransactionPool
22
+ # @api public
6
23
  class Router
24
+ # Create a router for event and transaction work items.
25
+ #
7
26
  # @param processor_pool [ProcessorPool]
27
+ # Pool used for individual `CDC::Core::ChangeEvent` objects.
8
28
  # @param transaction_pool [TransactionPool]
29
+ # Pool used for `CDC::Core::TransactionEnvelope` objects.
30
+ # @return [void]
9
31
  def initialize(processor_pool:, transaction_pool:)
10
32
  @processor_pool = processor_pool
11
33
  @transaction_pool = transaction_pool
@@ -14,6 +36,9 @@ module CDC
14
36
  # Process a supported CDC work item.
15
37
  #
16
38
  # @param item [CDC::Core::ChangeEvent, CDC::Core::TransactionEnvelope]
39
+ # Normalized CDC work item.
40
+ # @raise [UnsupportedWorkItemError]
41
+ # Raised when the item is not a supported `cdc-core` work item shape.
17
42
  # @return [CDC::Core::ProcessorResult]
18
43
  def process(item)
19
44
  case item
@@ -2,22 +2,101 @@
2
2
 
3
3
  module CDC
4
4
  module Parallel
5
- # High-level Ractor runtime facade for cdc-core processors.
5
+ # High-level Ractor runtime facade for `cdc-core` processors.
6
+ #
7
+ # `Runtime` is the primary public entry point for applications that want to
8
+ # execute normalized CDC work items with `cdc-parallel`. It wires together a
9
+ # {ProcessorPool}, a {TransactionPool}, and a {Router} so callers can submit
10
+ # either a single `CDC::Core::ChangeEvent` or a
11
+ # `CDC::Core::TransactionEnvelope` through one object.
12
+ #
13
+ # Use this class when you want the default cdc-parallel behavior:
14
+ #
15
+ # * validate that the processor declared `ractor_safe!`
16
+ # * boot a fixed set of worker Ractors
17
+ # * route events and transaction envelopes to the right pool
18
+ # * return `CDC::Core::ProcessorResult` objects
19
+ # * shut down all worker resources together
20
+ #
21
+ # @example Processing a change event
22
+ # runtime = CDC::Parallel::Runtime.new(
23
+ # processor: AnalyticsProcessor.new,
24
+ # size: 4,
25
+ # timeout: 5
26
+ # )
27
+ #
28
+ # result = runtime.process(change_event)
29
+ # result.success? #=> true
30
+ #
31
+ # runtime.shutdown
32
+ #
33
+ # @example Processing a transaction envelope
34
+ # result = runtime.process_transaction(transaction)
35
+ #
36
+ # @note `Runtime` is an execution facade, not a source adapter. It expects
37
+ # work that has already been normalized into `cdc-core` primitives.
38
+ # @see CDC::Parallel::ProcessorPool
39
+ # @see CDC::Parallel::TransactionPool
40
+ # @see CDC::Parallel::Router
41
+ # @api public
6
42
  class Runtime
43
+ # Create a runtime with event and transaction pools.
44
+ #
7
45
  # @param processor [CDC::Core::Processor]
46
+ # Ractor-safe processor used for both event and transaction processing.
8
47
  # @param size [Integer]
9
- # @param timeout [Float, nil]
48
+ # Number of worker Ractors per internal pool.
49
+ # @param timeout [Numeric, nil]
50
+ # Optional timeout in seconds for result collection and shutdown waits.
51
+ # @param supervision [Boolean]
52
+ # Whether worker Ractors should be respawned after unexpected death.
53
+ # @param max_respawns [Integer]
54
+ # Maximum crash count inside `respawn_window` before a worker slot enters
55
+ # cooldown.
56
+ # @param respawn_window [Numeric]
57
+ # Time window, in seconds, used by the crash-loop circuit breaker.
58
+ # @param respawn_cooldown [Numeric]
59
+ # Cooldown, in seconds, before a crash-looping slot is revived again.
60
+ # @raise [UnsafeProcessorError]
61
+ # Raised when the processor class has not declared `ractor_safe!`.
62
+ # @raise [ArgumentError]
63
+ # Raised when size or timeout is invalid.
10
64
  # @return [void]
11
- def initialize(processor:, size: Etc.nprocessors, timeout: nil)
12
- @processor_pool = ProcessorPool.new(processor:, size:, timeout:)
13
- @transaction_pool = TransactionPool.new(processor:, size:, timeout:)
65
+ def initialize(
66
+ processor:,
67
+ size: Etc.nprocessors,
68
+ timeout: nil,
69
+ supervision: true,
70
+ max_respawns: 3,
71
+ respawn_window: 60,
72
+ respawn_cooldown: 5
73
+ )
74
+ @processor = processor
75
+ @processor.start
76
+
77
+ pool_options = build_pool_options(
78
+ processor:, size:, timeout:, supervision:,
79
+ max_respawns:, respawn_window:, respawn_cooldown:
80
+ )
81
+
82
+ @processor_pool = ProcessorPool.new(**pool_options)
83
+ @transaction_pool = TransactionPool.new(**pool_options)
14
84
  @router = Router.new(processor_pool: @processor_pool, transaction_pool: @transaction_pool)
15
85
  @shutdown = false
16
86
  end
17
87
 
18
- # Process a ChangeEvent or TransactionEnvelope.
88
+ # Process a supported normalized CDC work item.
89
+ #
90
+ # Supported items are `CDC::Core::ChangeEvent` and
91
+ # `CDC::Core::TransactionEnvelope`. Unsupported objects raise
92
+ # {UnsupportedWorkItemError} from the router.
19
93
  #
20
94
  # @param item [CDC::Core::ChangeEvent, CDC::Core::TransactionEnvelope]
95
+ # Normalized CDC work item.
96
+ # @raise [ShutdownError]
97
+ # Raised when called after {#shutdown}.
98
+ # @raise [UnsupportedWorkItemError]
99
+ # Raised for objects that are not supported CDC work item shapes.
21
100
  # @return [CDC::Core::ProcessorResult]
22
101
  def process(item)
23
102
  raise ShutdownError, "runtime has been shut down" if @shutdown
@@ -25,7 +104,11 @@ module CDC
25
104
  @router.process(item)
26
105
  end
27
106
 
28
- # Alias for transaction-oriented processing.
107
+ # Process a transaction envelope.
108
+ #
109
+ # This method is a readability alias for transaction-oriented call sites.
110
+ # It delegates to {#process}, so it has the same validation, shutdown, and
111
+ # result behavior.
29
112
  #
30
113
  # @param transaction [CDC::Core::TransactionEnvelope]
31
114
  # @return [CDC::Core::ProcessorResult]
@@ -35,6 +118,11 @@ module CDC
35
118
 
36
119
  # Shut down all runtime resources.
37
120
  #
121
+ # Shutdown is idempotent and cascades to the internal event and transaction
122
+ # pools. The processor's `flush` and `stop` lifecycle hooks are called once
123
+ # after all pools have been drained. After shutdown, {#process} raises
124
+ # {ShutdownError}.
125
+ #
38
126
  # @return [void]
39
127
  def shutdown
40
128
  return if @shutdown
@@ -42,6 +130,46 @@ module CDC
42
130
  @shutdown = true
43
131
  @processor_pool.shutdown
44
132
  @transaction_pool.shutdown
133
+ @processor.flush
134
+ @processor.stop
135
+ end
136
+
137
+ private
138
+
139
+ # Build keyword arguments shared by the event and transaction pools.
140
+ #
141
+ # The runtime owns the processor lifecycle, so internal pools receive
142
+ # `manage_lifecycle: false` to avoid duplicate `start`, `flush`, or `stop`
143
+ # calls for the same processor instance.
144
+ #
145
+ # @param processor [CDC::Core::Processor]
146
+ # @param size [Integer]
147
+ # @param timeout [Numeric, nil]
148
+ # @param supervision [Boolean]
149
+ # @param max_respawns [Integer]
150
+ # @param respawn_window [Numeric]
151
+ # @param respawn_cooldown [Numeric]
152
+ # @return [Hash]
153
+ # @api private
154
+ def build_pool_options(
155
+ processor:,
156
+ size:,
157
+ timeout:,
158
+ supervision:,
159
+ max_respawns:,
160
+ respawn_window:,
161
+ respawn_cooldown:
162
+ )
163
+ {
164
+ processor:,
165
+ size:,
166
+ timeout:,
167
+ supervision:,
168
+ max_respawns:,
169
+ respawn_window:,
170
+ respawn_cooldown:,
171
+ manage_lifecycle: false
172
+ }
45
173
  end
46
174
  end
47
175
  end
@@ -2,30 +2,112 @@
2
2
 
3
3
  module CDC
4
4
  module Parallel
5
- # Processes a TransactionEnvelope as a single ordering-preserving unit.
5
+ # Processes a `CDC::Core::TransactionEnvelope` as one transaction-oriented
6
+ # work unit.
7
+ #
8
+ # `TransactionPool` uses {ProcessorPool} to process the events inside an
9
+ # envelope and then collapses the event-level results into one
10
+ # `CDC::Core::ProcessorResult` for the whole transaction.
11
+ #
12
+ # This class preserves the transaction boundary at the API level: callers
13
+ # submit a transaction envelope and receive a single success or failure
14
+ # result. Event results inside the transaction are still produced by the
15
+ # configured processor and are returned as the success value when every event
16
+ # succeeds.
17
+ #
18
+ # @example Processing a transaction envelope
19
+ # pool = CDC::Parallel::TransactionPool.new(
20
+ # processor: AuditProcessor.new,
21
+ # size: 4
22
+ # )
23
+ #
24
+ # result = pool.process(transaction)
25
+ # result.success? #=> true
26
+ #
27
+ # @note This class preserves the transaction as a result boundary. More
28
+ # advanced ordering, checkpointing, retry, and atomic sink semantics belong
29
+ # to higher-level runtime/sink contracts.
30
+ # @see CDC::Parallel::ProcessorPool
31
+ # @api public
6
32
  class TransactionPool
33
+ # Create a transaction pool.
34
+ #
7
35
  # @param processor [CDC::Core::Processor]
36
+ # Ractor-safe processor used for each event inside the transaction.
8
37
  # @param size [Integer]
9
- # @param timeout [Float, nil]
10
- def initialize(processor:, size: Etc.nprocessors, timeout: nil)
11
- @processor_pool = ProcessorPool.new(processor:, size:, timeout:)
38
+ # Number of worker Ractors in the underlying processor pool.
39
+ # @param timeout [Numeric, nil]
40
+ # Optional timeout in seconds for result collection and shutdown waits.
41
+ # @param supervision [Boolean]
42
+ # Whether worker Ractors should be respawned after unexpected death.
43
+ # @param max_respawns [Integer]
44
+ # Maximum crash count inside `respawn_window` before a worker slot enters
45
+ # cooldown.
46
+ # @param respawn_window [Numeric]
47
+ # Time window, in seconds, used by the crash-loop circuit breaker.
48
+ # @param respawn_cooldown [Numeric]
49
+ # Cooldown, in seconds, before a crash-looping slot is revived again.
50
+ # @param manage_lifecycle [Boolean]
51
+ # When `true` (default), the pool calls `processor.start` during
52
+ # initialization and `processor.flush` + `processor.stop` during shutdown.
53
+ # Set to `false` when a higher-level runtime owns the processor lifecycle.
54
+ # @raise [UnsafeProcessorError]
55
+ # Raised when the processor class has not declared `ractor_safe!`.
56
+ # @return [void]
57
+ def initialize(
58
+ processor:,
59
+ size: Etc.nprocessors,
60
+ timeout: nil,
61
+ supervision: true,
62
+ max_respawns: 3,
63
+ respawn_window: 60,
64
+ respawn_cooldown: 5,
65
+ manage_lifecycle: true
66
+ )
67
+ @processor_pool = ProcessorPool.new(
68
+ processor:,
69
+ size:,
70
+ timeout:,
71
+ supervision:,
72
+ max_respawns:,
73
+ respawn_window:,
74
+ respawn_cooldown:,
75
+ manage_lifecycle:
76
+ )
12
77
  end
13
78
 
14
79
  # Process all events inside a transaction envelope.
15
80
  #
81
+ # The returned result is successful only when every event result succeeds.
82
+ # If any event fails, the transaction result is a failure using the first
83
+ # failure error and the complete event-result list as context.
84
+ #
16
85
  # @param transaction [CDC::Core::TransactionEnvelope]
86
+ # Transaction envelope whose `events` will be processed.
17
87
  # @return [CDC::Core::ProcessorResult]
88
+ # Success containing the ordered event results, or failure containing the
89
+ # first event error.
18
90
  def process(transaction)
19
91
  results = @processor_pool.process_many(transaction.events).freeze
20
92
  failure = results.find(&:failure?)
21
93
 
22
- return CDC::Core::ProcessorResult.failure(failure.error, event: results) if failure
94
+ if failure
95
+ error = failure.error || ProcessorExecutionError.new(
96
+ original_class: "CDC::Core::ProcessorResult",
97
+ original_message: "failed processor result did not include an error"
98
+ )
99
+
100
+ return CDC::Core::ProcessorResult.failure(error, event: results)
101
+ end
23
102
 
24
103
  CDC::Core::ProcessorResult.success(results)
25
104
  end
26
105
 
27
106
  # Shut down worker resources.
28
107
  #
108
+ # Delegates to the underlying {ProcessorPool}. Shutdown is idempotent
109
+ # because the underlying pool is idempotent.
110
+ #
29
111
  # @return [void]
30
112
  def shutdown
31
113
  @processor_pool.shutdown
@@ -3,6 +3,11 @@
3
3
  module CDC
4
4
  module Parallel
5
5
  # Current cdc-parallel version.
6
- VERSION = "0.2.2"
6
+ #
7
+ # This constant is used by RubyGems and by applications that need to inspect
8
+ # the loaded runtime version at boot time.
9
+ #
10
+ # @return [String]
11
+ VERSION = "0.2.4"
7
12
  end
8
13
  end
data/lib/cdc/parallel.rb CHANGED
@@ -12,8 +12,46 @@ require_relative "parallel/transaction_pool"
12
12
  require_relative "parallel/router"
13
13
  require_relative "parallel/runtime"
14
14
 
15
+ # Namespace for Change Data Capture ecosystem components.
16
+ #
17
+ # The `cdc-parallel` gem adds the {CDC::Parallel} runtime namespace under
18
+ # this shared ecosystem root.
19
+ #
20
+ # @api public
15
21
  module CDC
16
- # Optional parallel Change Data Capture runtime for cdc-core processors.
22
+ # Optional parallel Change Data Capture runtime for `cdc-core` processors.
23
+ #
24
+ # `CDC::Parallel` is the CPU-bound execution layer of the CDC Ecosystem. It
25
+ # consumes work items that have already been normalized into `cdc-core`
26
+ # vocabulary objects and executes Ractor-safe processors across pre-warmed
27
+ # worker Ractors.
28
+ #
29
+ # The namespace intentionally avoids `CDC::Ractor` so it does not collide with
30
+ # Ruby's core `::Ractor` constant.
31
+ #
32
+ # ## Boundary
33
+ #
34
+ # `cdc-parallel` does not connect to PostgreSQL, parse `pgoutput`, decode
35
+ # values, normalize source payloads, or persist sink data. Those concerns live
36
+ # upstream in `pgoutput-*` libraries, source adapters, and downstream sinks.
37
+ #
38
+ # ## Main entry points
39
+ #
40
+ # * {CDC::Parallel::Runtime} - high-level facade for events and transactions
41
+ # * {CDC::Parallel::ProcessorPool} - low-level Ractor worker pool
42
+ # * {CDC::Parallel::TransactionPool} - transaction-envelope wrapper
43
+ # * {CDC::Parallel::Router} - work item router
44
+ #
45
+ # @example
46
+ # runtime = CDC::Parallel::Runtime.new(
47
+ # processor: AnalyticsProcessor.new,
48
+ # size: 4
49
+ # )
50
+ #
51
+ # result = runtime.process(change_event)
52
+ # runtime.shutdown
53
+ #
54
+ # @api public
17
55
  module Parallel
18
56
  end
19
57
  end
@@ -1,8 +1,20 @@
1
1
  module CDC
2
2
  module Parallel
3
+ # Private Data superclass used to keep YARD documentation stable for Configuration.
4
+ class ConfigurationData < Data
5
+ attr_reader size: Integer
6
+ attr_reader timeout: Numeric?
7
+ end
8
+
3
9
  # Immutable configuration for Ractor runtimes.
4
- class Configuration
5
- def initialize: (?size: untyped, ?timeout: untyped?) -> void
10
+ class Configuration < ConfigurationData
11
+ attr_reader size: Integer
12
+
13
+ attr_reader timeout: Numeric?
14
+
15
+ def self.new: (?size: Integer, ?timeout: Numeric?) -> instance
16
+
17
+ def initialize: (?size: Integer, ?timeout: Numeric?) -> void
6
18
  end
7
19
  end
8
20
  end
@@ -18,19 +18,19 @@ module CDC
18
18
 
19
19
  # Raised when processor execution fails inside a worker Ractor.
20
20
  class ProcessorExecutionError < Error
21
- @original_class: untyped
21
+ @original_class: String
22
22
 
23
- @original_message: untyped
23
+ @original_message: String
24
24
 
25
- @original_backtrace: untyped
25
+ @original_backtrace: Array[String]
26
26
 
27
- attr_reader original_class: untyped
27
+ attr_reader original_class: String
28
28
 
29
- attr_reader original_message: untyped
29
+ attr_reader original_message: String
30
30
 
31
- attr_reader original_backtrace: untyped
31
+ attr_reader original_backtrace: Array[String]
32
32
 
33
- def initialize: (original_class: untyped, original_message: untyped, ?original_backtrace: untyped) -> void
33
+ def initialize: (original_class: String?, original_message: String, ?original_backtrace: Array[String]) -> void
34
34
  end
35
35
 
36
36
  # Raised when a worker does not return a result before timeout.