julewire-ractor 1.0.0 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d166b07720f0d37eb7b80d8bc7c3e5bcc3b00c83c14f2ce991e5931bbf0272b
4
- data.tar.gz: 89bcff708f1fd8c0a22843ce4c74a8e49d99f39bed8f82f328d0115ba20216e9
3
+ metadata.gz: 3d42c3786d7b68451c4353ca8305bc41e5620b5003236ddc89c3707cf2727971
4
+ data.tar.gz: 0225ef575874b54b8bd457f82bf6531821dfe137228dc32de2888d360d1b154b
5
5
  SHA512:
6
- metadata.gz: 91bd26a0399339393e0c075ae1752180ae058b449af0563b4a044073d62f5506b86dca06b6e649cc50c024a9a289bc0ca2142aea51a703fc349e45e196d0a893
7
- data.tar.gz: 12dc746f9b15eeadc5c26f3566f83e042317632fee4fbed5281265a0f9ace8ce20ced5e1605d8a425158bcaf71de2b9ee985b8c8977766cbaf6101da128c486b
6
+ metadata.gz: 94b40f17f489ae6749d27f2ed459317eca174c35d7c952d9b7bd5b207ce6d26ac63220d0f8fb0cc56600eca80d48a69278b159d1d625455ff5703e6afffc5d28
7
+ data.tar.gz: 31a6e2729f9db688b42a61c3cdb859777ca70d625a1096bd259de32f8da8f793b75774207d0e3ac16e5efd874cdfb94f9ca461b43099fea657e6b2f4e3ad4d09
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 1.0.1 - 2026-06-25
4
+
5
+ - Reserve ractor destination queue slots before send and roll them back on
6
+ send failure.
7
+ - Count impossible queue-slot over-release events for destination health
8
+ debugging.
9
+ - Require julewire-core 1.0.1.
10
+
3
11
  ## 1.0.0 - 2026-06-21
4
12
 
5
13
  - Initial release: Ruby 4 ractor bridge, child-runtime forwarding, remote
data/docs/bridge.md CHANGED
@@ -32,8 +32,9 @@ identity or class.
32
32
  The serialized envelope is a ractor concern. Core keeps only the narrow
33
33
  `emit_envelope` hook that accepts already-extracted input, context, carry, and
34
34
  a scope snapshot. Payload parsing and scope reconstruction stay in this gem.
35
- That hook is parent-runtime SPI; the child `RemoteRuntime` exposes the normal
36
- facade emit methods instead of a detached-envelope API.
35
+ That hook is parent-runtime SPI; the bridge marks ractor wire data as owned, and
36
+ the child `RemoteRuntime` exposes the normal facade emit methods instead of a
37
+ detached-envelope API.
37
38
 
38
39
  ## Available in Child
39
40
 
@@ -32,6 +32,6 @@ Gem::Specification.new do |spec|
32
32
  spec.executables = []
33
33
  spec.require_paths = ["lib"]
34
34
 
35
- spec.add_dependency "julewire-core", ">= 1.0"
35
+ spec.add_dependency "julewire-core", ">= 1.0.1"
36
36
  spec.add_dependency "zeitwerk", ">= 2.8.1"
37
37
  end
@@ -70,7 +70,7 @@ module Julewire
70
70
  Julewire::Core::RuntimeLocator.current = Julewire::Ractor::RemoteRuntime.new(
71
71
  port: bridge_port, emit_non_standard_exception_summaries: emit_non_standard_summaries
72
72
  )
73
- Julewire::Core::Propagation.restore(captured_envelope) do
73
+ Julewire::Core::Propagation.restore(captured_envelope, owned: true) do
74
74
  callable.call(*call_args)
75
75
  end
76
76
  ensure
@@ -113,7 +113,7 @@ module Julewire
113
113
  payload = RemotePayload.hash_value(message, :payload)
114
114
  arguments = RemotePayload.extract(payload)
115
115
  arguments[:enforce_level] = false unless enforce_level
116
- runtime.emit_envelope(**arguments)
116
+ runtime.emit_envelope(**arguments, owned: true)
117
117
  end
118
118
 
119
119
  def reply_to(message, response)
@@ -12,6 +12,7 @@ module Julewire
12
12
  queued
13
13
  received
14
14
  send_error
15
+ slot_underflow_ignored
15
16
  worker_accepted
16
17
  worker_dropped
17
18
  ].freeze
@@ -58,15 +59,17 @@ module Julewire
58
59
  def emit(record)
59
60
  increment(:received)
60
61
  return drop(:closed_dropped, record) if closed?
61
- return drop(:queue_full_dropped, record) if queue_full?
62
+ return drop(:queue_full_dropped, record) unless reserve_slot
62
63
 
63
- @port.send({ command: :emit, record: record })
64
- @in_flight.increment
65
- increment(:queued)
64
+ begin
65
+ @port.send({ command: :emit, record: record })
66
+ increment(:queued)
67
+ rescue StandardError => e
68
+ release_slot
69
+ record_failure(e, phase: :ractor_send)
70
+ drop(:send_error, record)
71
+ end
66
72
  nil
67
- rescue StandardError => e
68
- record_failure(e, phase: :ractor_send)
69
- drop(:send_error, record)
70
73
  end
71
74
 
72
75
  def flush(timeout: nil)
@@ -227,8 +230,16 @@ module Julewire
227
230
  PortLifecycle.close(timeout_port) if defined?(timeout_port) && timeout_port
228
231
  end
229
232
 
230
- def queue_full?
231
- @max_queue.positive? && @in_flight.value >= @max_queue
233
+ def reserve_slot
234
+ return true unless @max_queue.positive?
235
+
236
+ # MRI normally settles this in one pass under the GVL; the CAS loop keeps
237
+ # the reservation honest when multiple emitters race.
238
+ loop do
239
+ current = @in_flight.value
240
+ return false if current >= @max_queue
241
+ return true if @in_flight.compare_and_set(current, current + 1)
242
+ end
232
243
  end
233
244
 
234
245
  def closed? = @closed.get
@@ -269,8 +280,23 @@ module Julewire
269
280
  nil
270
281
  end
271
282
 
283
+ def release_slot
284
+ return unless @max_queue.positive?
285
+
286
+ loop do
287
+ current = @in_flight.value
288
+ if current <= 0
289
+ # Late or duplicate ACKs can arrive after teardown/reset; keep them visible
290
+ # without treating the ignored underflow as an operator-facing defect.
291
+ increment(:slot_underflow_ignored)
292
+ return
293
+ end
294
+ return if @in_flight.compare_and_set(current, current - 1)
295
+ end
296
+ end
297
+
272
298
  def decrement_in_flight
273
- @in_flight.decrement if @in_flight.value.positive?
299
+ release_slot
274
300
  end
275
301
 
276
302
  def increment(key)
@@ -20,7 +20,10 @@ module Julewire
20
20
 
21
21
  def input_value(payload)
22
22
  value = Core::Integration::Values::Read.hash_value(payload, :input, default: MISSING)
23
- value.equal?(MISSING) ? {} : value
23
+ return {} if value.equal?(MISSING)
24
+ return Core::Fields::FieldSet.deep_symbolize_owned_keys(value) if value.is_a?(Hash)
25
+
26
+ value
24
27
  end
25
28
 
26
29
  def scope_snapshot(scope_payload)
@@ -35,7 +38,7 @@ module Julewire
35
38
 
36
39
  def hash_value(hash, key)
37
40
  value = Core::Integration::Values::Read.hash_value(hash, key)
38
- value.is_a?(Hash) ? Core::Fields::FieldSet.deep_symbolize_keys(value) : {}
41
+ value.is_a?(Hash) ? Core::Fields::FieldSet.deep_symbolize_owned_keys(value) : {}
39
42
  end
40
43
  end
41
44
  end
@@ -8,7 +8,7 @@ module Julewire
8
8
  end
9
9
 
10
10
  def owned_summary_record_input
11
- @owned_summary_record_input ||= Core::Fields::FieldSet.deep_symbolize_keys(@record)
11
+ @owned_summary_record_input ||= Core::Fields::FieldSet.deep_symbolize_owned_keys(@record)
12
12
  end
13
13
  end
14
14
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Julewire
4
4
  module Ractor
5
- VERSION = "1.0.0"
5
+ VERSION = "1.0.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: julewire-ractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Grebennik
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '1.0'
18
+ version: 1.0.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '1.0'
25
+ version: 1.0.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: zeitwerk
28
28
  requirement: !ruby/object:Gem::Requirement