julewire-ractor 1.0.0 → 1.1.0

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.
@@ -3,13 +3,11 @@
3
3
  module Julewire
4
4
  module Ractor
5
5
  module RemotePayload
6
- MISSING = Object.new.freeze
7
- private_constant :MISSING
8
-
9
6
  class << self
10
7
  def extract(payload)
8
+ validate_hash!(payload)
11
9
  {
12
- input: input_value(payload),
10
+ input: hash_value(payload, :input),
13
11
  context: hash_value(payload, :context),
14
12
  neutral: hash_value(payload, :neutral),
15
13
  attributes: hash_value(payload, :attributes),
@@ -18,11 +16,6 @@ module Julewire
18
16
  }
19
17
  end
20
18
 
21
- def input_value(payload)
22
- value = Core::Integration::Values::Read.hash_value(payload, :input, default: MISSING)
23
- value.equal?(MISSING) ? {} : value
24
- end
25
-
26
19
  def scope_snapshot(scope_payload)
27
20
  Core::Execution::ScopeSnapshot.new(
28
21
  execution: hash_value(scope_payload, :execution),
@@ -34,8 +27,13 @@ module Julewire
34
27
  end
35
28
 
36
29
  def hash_value(hash, key)
37
- value = Core::Integration::Values::Read.hash_value(hash, key)
38
- value.is_a?(Hash) ? Core::Fields::FieldSet.deep_symbolize_keys(value) : {}
30
+ validate_hash!(hash.fetch(key))
31
+ end
32
+
33
+ private
34
+
35
+ def validate_hash!(value)
36
+ Core::Integration::Protocol.validate_symbol_hash(value)
39
37
  end
40
38
  end
41
39
  end
@@ -47,11 +47,22 @@ module Julewire
47
47
  remote_emit(:emit_without_level, record, fields, &)
48
48
  end
49
49
 
50
+ def emit_integration(record, enforce_level:)
51
+ command = enforce_level ? :emit : :emit_without_level
52
+ input = Core::Records::BuildInput.validate_owned(record)
53
+ notify(command, payload: remote_emit_payload(input))
54
+ rescue StandardError => e
55
+ @child_stats.message_dropped(e)
56
+ end
57
+
50
58
  def remote_emit(command, record, fields, &)
51
59
  record = Core.emit_input(record, fields)
52
60
  record = Core::Records::LazyEmitInput.call(record, &) if block_given?
53
- record = record.to_h if Core::Records::LazyEmitInput.input?(record)
54
- notify(command, payload: remote_emit_payload(record))
61
+ input = Core::Records::BuildInput.normalize_public(record)
62
+ input = Core::Fields::FieldSet.deep_symbolize_keys(input)
63
+ notify(command, payload: remote_emit_payload(input))
64
+ rescue StandardError => e
65
+ @child_stats.message_dropped(e)
55
66
  end
56
67
  private :remote_emit
57
68
 
@@ -81,22 +92,22 @@ module Julewire
81
92
  Core::ContextStore.reset_current!
82
93
  end
83
94
 
95
+ def reset_facade! = reset!
96
+
84
97
  def emit_summary_record(scope)
85
- notify(:emit_record, payload: Core::Serialization::Serializer.call(summary_record_input(scope)))
98
+ notify(:emit_record, payload: serialize_remote(summary_record_input(scope)))
99
+ rescue StandardError => e
100
+ @child_stats.message_dropped(e)
86
101
  end
87
102
 
88
103
  private
89
104
 
90
105
  def emit_non_standard_exception_summaries? = @emit_non_standard_exception_summaries
91
106
 
92
- def summary_finalizer_failure
93
- @summary_finalizer_failure ||= ->(_error) {}
94
- end
95
-
96
107
  def build_execution_boundary
97
108
  Core::Execution::Boundary.new(
98
109
  emit_summary_record: ->(scope) { emit_summary_record(scope) },
99
- summary_finalizer_failure: summary_finalizer_failure,
110
+ summary_finalizer_failure: nil,
100
111
  emit_non_standard_exception_summaries: -> { emit_non_standard_exception_summaries? }
101
112
  )
102
113
  end
@@ -108,19 +119,23 @@ module Julewire
108
119
  end
109
120
 
110
121
  def remote_emit_payload(record)
111
- {
112
- input: Core::Serialization::Serializer.call(record),
113
- context: Core::Serialization::Serializer.call(Core::ContextStore.current.context_hash),
114
- neutral: Core::Serialization::Serializer.call(Core::ContextStore.current.neutral_hash),
115
- attributes: Core::Serialization::Serializer.call(Core::ContextStore.current.attributes_hash),
116
- carry: Core::Serialization::Serializer.call(Core::ContextStore.current.carry_hash),
117
- scope: Core::Serialization::Serializer.call(scope_payload)
118
- }
122
+ serialize_remote(
123
+ input: record,
124
+ context: Core::ContextStore.current.context_hash,
125
+ neutral: Core::ContextStore.current.neutral_hash,
126
+ attributes: Core::ContextStore.current.attributes_hash,
127
+ carry: Core::ContextStore.current.carry_hash,
128
+ scope: scope_payload
129
+ )
130
+ end
131
+
132
+ def serialize_remote(value)
133
+ RemoteSerializer.call(value)
119
134
  end
120
135
 
121
136
  def scope_payload
122
137
  scope = Core::ContextStore.current.current_scope_or_snapshot
123
- return {} unless scope
138
+ return empty_scope_payload unless scope
124
139
 
125
140
  {
126
141
  execution: scope.execution_hash,
@@ -131,31 +146,28 @@ module Julewire
131
146
  }
132
147
  end
133
148
 
149
+ def empty_scope_payload
150
+ { execution: {}, neutral: {}, attributes: {}, carry: {}, labels: {} }
151
+ end
152
+
134
153
  def notify(command, payload:)
135
154
  @request_mutex.synchronize do
136
155
  @port.send({ command: command, payload: payload })
137
156
  end
138
157
  @child_stats.message_sent
139
- nil
140
- rescue StandardError => e
141
- @child_stats.message_dropped(e)
142
- nil
143
158
  end
144
159
 
145
160
  def request(command, timeout:)
146
161
  reply = ::Ractor::Port.new
147
- waiting_for_reply = false
148
162
  @request_mutex.synchronize do
149
163
  @port.send({ command: command, payload: { timeout: timeout }, reply: reply })
150
164
  end
151
165
  @child_stats.request_sent
152
- waiting_for_reply = true
153
166
  wait_for_reply(reply, timeout)
154
167
  rescue StandardError => e
155
168
  @child_stats.request_failed(e)
156
- nil
157
169
  ensure
158
- close_reply(reply) if reply && !waiting_for_reply
170
+ close_reply(reply)
159
171
  end
160
172
 
161
173
  def effective_timeout(timeout)
@@ -163,20 +175,17 @@ module Julewire
163
175
  end
164
176
 
165
177
  def wait_for_reply(reply, timeout)
166
- timeout_token = @timeout_scheduler.schedule(reply, timeout: timeout) if timeout
167
- response = reply.receive
178
+ response = if timeout.nil?
179
+ reply.receive
180
+ else
181
+ @timeout_scheduler.with_timeout(reply, timeout: timeout) { reply.receive }
182
+ end
168
183
 
169
184
  if response.equal?(REQUEST_TIMEOUT)
170
185
  @child_stats.request_timed_out
171
- nil
172
186
  else
173
187
  response
174
188
  end
175
- rescue StandardError
176
- nil
177
- ensure
178
- @timeout_scheduler.cancel(timeout_token)
179
- close_reply(reply)
180
189
  end
181
190
 
182
191
  def close_reply(reply)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Ractor
5
+ # Serializes cross-ractor values without changing the internal Symbol-key
6
+ # protocol into a JSON-shaped String-key protocol.
7
+ class RemoteSerializer < Core::Serialization::Serializer
8
+ def serialize(value)
9
+ Core::Integration::Protocol.validate_symbol_keys(value)
10
+ super
11
+ end
12
+
13
+ private
14
+
15
+ def initialize(**)
16
+ super
17
+ @truncation_key = TRUNCATION_METADATA_KEY.to_sym
18
+ end
19
+
20
+ def key_value(key) = super.to_sym
21
+
22
+ def truncation_metadata(fields) = super(fields, key_style: :symbol)
23
+ end
24
+
25
+ private_constant :RemoteSerializer
26
+ end
27
+ 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::Integration::Protocol.validate_symbol_keys(@record)
12
12
  end
13
13
  end
14
14
  end
@@ -5,32 +5,35 @@ module Julewire
5
5
  class ReplyTimeoutScheduler
6
6
  THREAD_NAME = "julewire-ractor-reply-timeout"
7
7
 
8
- # This stays stdlib-only because RemoteRuntime creates it inside a Ractor.
9
- # Concurrent::ScheduledTask currently touches non-shareable concurrent-ruby
10
- # constants from worker ractors.
11
-
12
8
  def initialize(timeout_value:)
13
9
  @timeout_value = timeout_value
14
- @scheduler = Core::Scheduling::DeadlineScheduler.new(thread_name: THREAD_NAME, idle: :exit)
15
10
  end
16
11
 
17
12
  def schedule(reply, timeout:)
18
- @scheduler.schedule(timeout) { send_timeout(reply) }
13
+ Thread.new(Float(timeout)) do |delay|
14
+ Thread.current.name = THREAD_NAME
15
+ sleep(delay)
16
+ send_timeout(reply)
17
+ end
19
18
  end
20
19
 
21
- def cancel(token)
22
- return unless token
23
-
24
- @scheduler.cancel(token)
25
- rescue StandardError
20
+ def cancel(thread)
21
+ thread&.kill
22
+ thread&.join
26
23
  nil
27
24
  end
28
25
 
26
+ def with_timeout(reply, timeout:)
27
+ token = schedule(reply, timeout: timeout)
28
+ yield
29
+ ensure
30
+ cancel(token)
31
+ end
32
+
29
33
  private
30
34
 
31
35
  def send_timeout(reply)
32
36
  reply.send(@timeout_value)
33
- nil
34
37
  rescue StandardError
35
38
  nil
36
39
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Julewire
4
4
  module Ractor
5
- VERSION = "1.0.0"
5
+ VERSION = "1.1.0"
6
6
  end
7
7
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Ractor
5
+ class WorkerHandshake
6
+ DEFAULT_TIMEOUT = 1
7
+
8
+ class << self
9
+ def receive(setup_port:, worker:, scheduler:, timeout: DEFAULT_TIMEOUT)
10
+ PortLifecycle.with_port do |timeout_port|
11
+ scheduler.with_timeout(timeout_port, timeout: timeout) do
12
+ selected, value = ::Ractor.select(setup_port, worker, timeout_port)
13
+
14
+ if selected.equal?(timeout_port)
15
+ raise Core::Error, "ractor destination worker did not start within #{timeout} seconds"
16
+ end
17
+ return value if selected.equal?(setup_port) && value.is_a?(::Ractor::Port)
18
+
19
+ raise ArgumentError, "ractor destination worker did not start"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ private_constant :WorkerHandshake
27
+ end
28
+ end
@@ -7,7 +7,7 @@ module Julewire
7
7
  module Ractor
8
8
  class << self
9
9
  def health
10
- bridge_health
10
+ Bridge.health
11
11
  end
12
12
 
13
13
  def child_stats
@@ -24,15 +24,10 @@ module Julewire
24
24
 
25
25
  def enable_default_destination_workers!
26
26
  Core::Destinations.register(:default) { |name:, **options| Destination.new(name: name, **options) }
27
- nil
28
27
  end
29
28
 
30
29
  private
31
30
 
32
- def bridge_health
33
- Bridge.health
34
- end
35
-
36
31
  def child_runtime
37
32
  runtime = Core::RuntimeLocator.current
38
33
  runtime if runtime.respond_to?(:child_stats)
@@ -53,7 +48,7 @@ module Julewire
53
48
  def ractor(*args, name: nil, &block)
54
49
  raise ArgumentError, "block required" unless block
55
50
 
56
- Ractor::Bridge.spawn(args: args, name: name, runtime: Core::RuntimeLocator.current, &block)
51
+ Ractor::Bridge.start(args: args, name: name, runtime: Core::RuntimeLocator.current, &block)
57
52
  end
58
53
  end
59
54
  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.1.0
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.1.0
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.1.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: zeitwerk
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -65,9 +65,11 @@ files:
65
65
  - lib/julewire/ractor/port_lifecycle.rb
66
66
  - lib/julewire/ractor/remote_payload.rb
67
67
  - lib/julewire/ractor/remote_runtime.rb
68
+ - lib/julewire/ractor/remote_serializer.rb
68
69
  - lib/julewire/ractor/remote_summary_record.rb
69
70
  - lib/julewire/ractor/reply_timeout_scheduler.rb
70
71
  - lib/julewire/ractor/version.rb
72
+ - lib/julewire/ractor/worker_handshake.rb
71
73
  homepage: https://github.com/slbug/julewire
72
74
  licenses:
73
75
  - MIT
@@ -90,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
92
  - !ruby/object:Gem::Version
91
93
  version: '0'
92
94
  requirements: []
93
- rubygems_version: 4.0.14
95
+ rubygems_version: 4.0.16
94
96
  specification_version: 4
95
97
  summary: Experimental Ractor bridge for julewire-core.
96
98
  test_files: []