cdc-parallel 0.2.0 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e16d6352e78132e2e0f488542b17117bda8733ca15b3117f8151b6acbfc3567
4
- data.tar.gz: 3e28c6c37d5078696ab334f5c8b15409172bebb8102747c2e7558f0089d4ad64
3
+ metadata.gz: a51d304d55079509a1c2056573a9f4764924573b9a13eb1dbeda89ac94d57836
4
+ data.tar.gz: 4e154536aaca3d801d4affe02e424f332c6a1a37b738ee70cc8fea5f335645fd
5
5
  SHA512:
6
- metadata.gz: 4e71ef2eeda63a9d6f6c59d49ba140b73a62d0e37894e168060a4c796f28a7d6790b37b891c7188fff78de1e843386c0f892f0720ba3de16ea9bfe198e719382
7
- data.tar.gz: de5eb4cb7861e263402305e361d4ac49d335d9b657562cca93544c64f34f76646289212647506c87f4d6ef7eb5ec586cbb58596ad9427f8becb99ca1af82127c
6
+ metadata.gz: c3002bfcd3914510fc39e7621fba99a7868303d24f8c8e5b849d40c653f73355b9926636ed6bb31754f2f028d65af325207eb89466923bc413eb8e44538730e7
7
+ data.tar.gz: 686ef1d6c0759d8ebedbd18e7109a4ac22008e8a0501faab31db8440bef3cf1127f79b076ce46fd3d8eec46f6e524e0ac6a75b1d3e0cefbc2f5ab151e99110b8
data/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
6
6
 
7
+ ## [0.2.2] - 2026-06-03
8
+
9
+ ### Changed
10
+
11
+ - Improved processor pool shutdown so workers are signaled and confirmed stopped where practical.
12
+ - Updated transaction processing so partial event failures fail the transaction result while preserving per-event results.
13
+ - Added CI validation for RBS signatures.
14
+
15
+ ### Added
16
+
17
+ - Added regression coverage for shutdown after processed and pending work.
18
+ - Added regression coverage for timeout-bounded shutdown behavior.
19
+ - Added regression coverage for `process_many([])` returning a clean empty result.
20
+ - Added transaction pool coverage for successful and partially failed transactions.
21
+
22
+ ## [0.2.1] - 2026-06-03
23
+
24
+ ### Added
25
+
26
+ v0.2.1 - Correctness and reliability patch
27
+
28
+ - Enforced processor timeout handling.
29
+ - Fixed transaction partial-failure behavior.
30
+ - Added regression coverage for hung processors and transaction failure cases.
31
+
7
32
  ## [0.2.0] - 2026-06-03
8
33
 
9
34
  ### Added
@@ -12,6 +12,8 @@ module CDC
12
12
  def initialize(size: Etc.nprocessors, timeout: nil)
13
13
  raise ArgumentError, "size must be an Integer" unless size.is_a?(Integer)
14
14
  raise ArgumentError, "size must be greater than zero" unless size.positive?
15
+ raise ArgumentError, "timeout must be numeric" unless timeout.nil? || timeout.is_a?(Numeric)
16
+ raise ArgumentError, "timeout must be greater than zero" if timeout && !timeout.positive?
15
17
 
16
18
  super
17
19
  ::Ractor.make_shareable(self)
@@ -8,7 +8,7 @@ module CDC
8
8
  # This pays Ractor startup cost once, keeps workers alive after processor
9
9
  # failures, and provides both synchronous single-item processing and batched
10
10
  # dispatch for throughput-oriented benchmarks and runtimes.
11
- class ProcessorPool
11
+ class ProcessorPool # rubocop:disable Metrics/ClassLength
12
12
  # @param processor [CDC::Core::Processor]
13
13
  # @param size [Integer]
14
14
  # @param timeout [Float, nil]
@@ -63,6 +63,13 @@ module CDC
63
63
 
64
64
  @shutdown = true
65
65
 
66
+ signal_workers
67
+ wait_for_workers
68
+ end
69
+
70
+ private
71
+
72
+ def signal_workers
66
73
  @workers.each do |worker|
67
74
  worker.send(nil)
68
75
  rescue Ractor::ClosedError
@@ -70,7 +77,26 @@ module CDC
70
77
  end
71
78
  end
72
79
 
73
- private
80
+ def wait_for_workers
81
+ if @configuration.timeout
82
+ wait_for_workers_with_timeout
83
+ else
84
+ @workers.each(&:join)
85
+ end
86
+ end
87
+
88
+ def wait_for_workers_with_timeout
89
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @configuration.timeout
90
+
91
+ @workers.each do |worker|
92
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
93
+ break unless remaining.positive?
94
+
95
+ ::Timeout.timeout(remaining, TimeoutError) { worker.join }
96
+ rescue TimeoutError
97
+ break
98
+ end
99
+ end
74
100
 
75
101
  def validate_processor!(processor)
76
102
  return if processor.class.respond_to?(:ractor_safe?) &&
@@ -80,7 +106,7 @@ module CDC
80
106
  "#{processor.class} must declare ractor_safe!"
81
107
  end
82
108
 
83
- def build_worker(processor)
109
+ def build_worker(processor) # rubocop:disable Metrics/MethodLength
84
110
  ::Ractor.new(processor) do |safe_processor|
85
111
  loop do
86
112
  message = ::Ractor.receive
@@ -96,7 +122,11 @@ module CDC
96
122
  CDC::Parallel::ResultCollector.worker_failure(e)
97
123
  end
98
124
 
99
- reply_port << [index, response]
125
+ begin
126
+ reply_port << [index, response]
127
+ rescue Ractor::ClosedError
128
+ # The caller may have timed out and closed the reply port.
129
+ end
100
130
  end
101
131
  end
102
132
  end
@@ -112,14 +142,50 @@ module CDC
112
142
 
113
143
  def collect_results(reply_port, count)
114
144
  results = Array.new(count)
145
+ return results.freeze if count.zero?
115
146
 
116
- count.times do
147
+ if @configuration.timeout
148
+ collect_results_with_timeout(reply_port, results)
149
+ else
150
+ collect_results_without_timeout(reply_port, results)
151
+ end
152
+ end
153
+
154
+ def collect_results_without_timeout(reply_port, results)
155
+ results.length.times do
117
156
  index, response = reply_port.receive
118
157
  results[index] = ResultCollector.normalize(response)
119
158
  end
120
159
 
121
160
  results.freeze
122
161
  end
162
+
163
+ def collect_results_with_timeout(reply_port, results)
164
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @configuration.timeout
165
+
166
+ results.length.times do
167
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
168
+ return timeout_results(results) unless remaining.positive?
169
+
170
+ index, response = ::Timeout.timeout(remaining, TimeoutError) { reply_port.receive }
171
+ results[index] = ResultCollector.normalize(response)
172
+ rescue TimeoutError
173
+ return timeout_results(results)
174
+ end
175
+
176
+ results.freeze
177
+ end
178
+
179
+ def timeout_results(results)
180
+ missing = results.count(&:nil?)
181
+ timeout_error = TimeoutError.new(
182
+ "processor pool timed out after #{@configuration.timeout} seconds waiting for #{missing} result(s)"
183
+ )
184
+
185
+ results.map do |result|
186
+ result || CDC::Core::ProcessorResult.failure(timeout_error)
187
+ end.freeze
188
+ end
123
189
  end
124
190
  end
125
191
  end
@@ -16,8 +16,12 @@ module CDC
16
16
  # @param transaction [CDC::Core::TransactionEnvelope]
17
17
  # @return [CDC::Core::ProcessorResult]
18
18
  def process(transaction)
19
- results = transaction.events.map { |event| @processor_pool.process(event) }.freeze
20
- ResultCollector.normalize(results)
19
+ results = @processor_pool.process_many(transaction.events).freeze
20
+ failure = results.find(&:failure?)
21
+
22
+ return CDC::Core::ProcessorResult.failure(failure.error, event: results) if failure
23
+
24
+ CDC::Core::ProcessorResult.success(results)
21
25
  end
22
26
 
23
27
  # Shut down worker resources.
@@ -3,6 +3,6 @@
3
3
  module CDC
4
4
  module Parallel
5
5
  # Current cdc-parallel version.
6
- VERSION = "0.2.0"
6
+ VERSION = "0.2.2"
7
7
  end
8
8
  end
data/lib/cdc/parallel.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "etc"
4
+ require "timeout"
4
5
 
5
6
  require_relative "parallel/version"
6
7
  require_relative "parallel/errors"
@@ -1,16 +1,15 @@
1
1
  module CDC
2
2
  module Parallel
3
- # Executes one Ractor-safe processor in isolated Ractor workers.
4
- #
5
- # This v0.1 implementation intentionally uses one-shot worker Ractors for
6
- # deterministic synchronous semantics while preserving the public pool API.
7
- # The parallel-pool dependency is kept as the runtime foundation for later
8
- # async/throughput-focused versions.
3
+ # Executes one Ractor-safe processor in pre-warmed persistent Ractor workers.
9
4
  class ProcessorPool
10
5
  @processor: untyped
11
6
 
12
7
  @configuration: untyped
13
8
 
9
+ @workers: untyped
10
+
11
+ @next_worker: Integer
12
+
14
13
  @shutdown: untyped
15
14
 
16
15
  # @param processor [CDC::Core::Processor]
@@ -19,11 +18,17 @@ module CDC
19
18
  # @return [void]
20
19
  def initialize: (processor: untyped, ?size: untyped, ?timeout: untyped?) -> void
21
20
 
22
- # Process one ChangeEvent.
21
+ # Process one work item synchronously.
23
22
  #
24
- # @param event [CDC::Core::ChangeEvent]
23
+ # @param item [Object]
25
24
  # @return [CDC::Core::ProcessorResult]
26
- def process: (untyped event) -> untyped
25
+ def process: (untyped item) -> untyped
26
+
27
+ # Process many work items using the pre-warmed worker pool.
28
+ #
29
+ # @param items [Array<Object>]
30
+ # @return [Array<CDC::Core::ProcessorResult>]
31
+ def process_many: (untyped items) -> untyped
27
32
 
28
33
  # Shut down the pool.
29
34
  #
@@ -32,9 +37,25 @@ module CDC
32
37
 
33
38
  private
34
39
 
40
+ def signal_workers: () -> untyped
41
+
42
+ def wait_for_workers: () -> untyped
43
+
44
+ def wait_for_workers_with_timeout: () -> untyped
45
+
35
46
  def validate_processor!: (untyped processor) -> (nil | untyped)
36
47
 
37
- def take: (untyped worker) -> untyped
48
+ def build_worker: (untyped processor) -> untyped
49
+
50
+ def next_worker: () -> untyped
51
+
52
+ def collect_results: (untyped reply_port, Integer count) -> untyped
53
+
54
+ def collect_results_without_timeout: (untyped reply_port, untyped results) -> untyped
55
+
56
+ def collect_results_with_timeout: (untyped reply_port, untyped results) -> untyped
57
+
58
+ def timeout_results: (untyped results) -> untyped
38
59
  end
39
60
  end
40
61
  end
@@ -4,6 +4,12 @@ module CDC
4
4
  class ResultCollector
5
5
  FAILURE_MARKER: :__cdc_parallel_failure__
6
6
 
7
+ # Build a shareable success payload that can safely cross a Ractor boundary.
8
+ #
9
+ # @param value [Object]
10
+ # @return [Object]
11
+ def self.worker_success: (untyped value) -> untyped
12
+
7
13
  # Build a shareable failure payload that can safely cross a Ractor boundary.
8
14
  #
9
15
  # @param error [Exception]
@@ -1,6 +1,6 @@
1
1
  module CDC
2
2
  module Parallel
3
3
  # Current cdc-parallel version.
4
- VERSION: "0.1.0"
4
+ VERSION: "0.2.0"
5
5
  end
6
6
  end
@@ -2,7 +2,7 @@ module CDC
2
2
  module Core
3
3
  class ProcessorResult
4
4
  def self.success: (untyped event) -> ProcessorResult
5
- def self.failure: (untyped error) -> ProcessorResult
5
+ def self.failure: (untyped error, ?event: untyped) -> ProcessorResult
6
6
  end
7
7
 
8
8
  class ChangeEvent
@@ -11,4 +11,4 @@ module CDC
11
11
  class TransactionEnvelope
12
12
  end
13
13
  end
14
- end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Timeout
2
+ def self.timeout: (untyped sec, untyped klass) { () -> untyped } -> untyped
3
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cdc-parallel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
@@ -73,6 +73,7 @@ files:
73
73
  - sig/shims/cdc_core.rbs
74
74
  - sig/shims/data_define.rbs
75
75
  - sig/shims/etc.rbs
76
+ - sig/shims/timeout.rbs
76
77
  homepage: https://kanutocd.github.io/cdc-parallel/
77
78
  licenses:
78
79
  - MIT