julewire-ractor 1.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +62 -0
- data/docs/bridge.md +159 -0
- data/docs/development.md +42 -0
- data/julewire-ractor.gemspec +37 -0
- data/lib/julewire/ractor/bridge/bridge_thread.rb +61 -0
- data/lib/julewire/ractor/bridge/runtime_validation.rb +25 -0
- data/lib/julewire/ractor/bridge/stats.rb +75 -0
- data/lib/julewire/ractor/bridge.rb +133 -0
- data/lib/julewire/ractor/child_stats.rb +69 -0
- data/lib/julewire/ractor/destination.rb +289 -0
- data/lib/julewire/ractor/destination_worker.rb +176 -0
- data/lib/julewire/ractor/fanout.rb +95 -0
- data/lib/julewire/ractor/port_lifecycle.rb +18 -0
- data/lib/julewire/ractor/remote_payload.rb +43 -0
- data/lib/julewire/ractor/remote_runtime.rb +187 -0
- data/lib/julewire/ractor/remote_summary_record.rb +15 -0
- data/lib/julewire/ractor/reply_timeout_scheduler.rb +39 -0
- data/lib/julewire/ractor/version.rb +7 -0
- data/lib/julewire/ractor.rb +59 -0
- data/lib/julewire-ractor.rb +3 -0
- metadata +96 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Ractor
|
|
5
|
+
class RemoteRuntime
|
|
6
|
+
DEFAULT_REQUEST_TIMEOUT = 1
|
|
7
|
+
REQUEST_TIMEOUT = ::Ractor.make_shareable(Object.new)
|
|
8
|
+
|
|
9
|
+
def initialize(port:, emit_non_standard_exception_summaries: false)
|
|
10
|
+
@port = port
|
|
11
|
+
@child_stats = ChildStats.new
|
|
12
|
+
@emit_non_standard_exception_summaries = emit_non_standard_exception_summaries
|
|
13
|
+
@request_mutex = Mutex.new
|
|
14
|
+
@timeout_scheduler = ReplyTimeoutScheduler.new(timeout_value: REQUEST_TIMEOUT)
|
|
15
|
+
@execution_boundary = build_execution_boundary
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def config = raise Core::Error, "Julewire.config is not available inside Julewire.ractor"
|
|
19
|
+
|
|
20
|
+
def configure = raise Core::Error, "Julewire.configure is not available inside Julewire.ractor"
|
|
21
|
+
|
|
22
|
+
def context = Core::ContextStore.current.context_proxy
|
|
23
|
+
|
|
24
|
+
def attributes = Core::ContextStore.current.attributes_proxy
|
|
25
|
+
|
|
26
|
+
def carry = Core::ContextStore.current.carry_proxy
|
|
27
|
+
|
|
28
|
+
def summary = Core::ContextStore.current.summary_proxy
|
|
29
|
+
|
|
30
|
+
def current_execution = current_scope && Core::Execution::View.new(current_scope)
|
|
31
|
+
|
|
32
|
+
def current_execution? = !!current_scope
|
|
33
|
+
|
|
34
|
+
def with_execution(...) = @execution_boundary.with_execution(...)
|
|
35
|
+
|
|
36
|
+
def start_execution(...) = @execution_boundary.start_execution(...)
|
|
37
|
+
|
|
38
|
+
def child_stats = @child_stats.to_h
|
|
39
|
+
|
|
40
|
+
def reset_child_stats! = @child_stats.reset!
|
|
41
|
+
|
|
42
|
+
def emit(record = Core::UNSET, **fields, &)
|
|
43
|
+
remote_emit(:emit, record, fields, &)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def emit_without_level(record = Core::UNSET, **fields, &)
|
|
47
|
+
remote_emit(:emit_without_level, record, fields, &)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def remote_emit(command, record, fields, &)
|
|
51
|
+
record = Core.emit_input(record, fields)
|
|
52
|
+
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))
|
|
55
|
+
end
|
|
56
|
+
private :remote_emit
|
|
57
|
+
|
|
58
|
+
def flush(timeout: Core::UNSET)
|
|
59
|
+
timeout = effective_timeout(timeout)
|
|
60
|
+
Core::Validation.validate_timeout!(timeout, name: :timeout)
|
|
61
|
+
request(:flush, timeout: timeout)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def after_fork!(**)
|
|
65
|
+
raise Core::Error, "Julewire.after_fork! is not available inside Julewire.ractor"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def health
|
|
69
|
+
raise Core::Error, "Julewire.health is not available inside Julewire.ractor"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def close(**)
|
|
73
|
+
raise Core::Error, "Julewire.close is not available inside Julewire.ractor; use Julewire.flush instead"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def labels
|
|
77
|
+
raise Core::Error, "Julewire.labels is not available inside Julewire.ractor"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def reset!
|
|
81
|
+
Core::ContextStore.reset_current!
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def emit_summary_record(scope)
|
|
85
|
+
notify(:emit_record, payload: Core::Serialization::Serializer.call(summary_record_input(scope)))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def emit_non_standard_exception_summaries? = @emit_non_standard_exception_summaries
|
|
91
|
+
|
|
92
|
+
def summary_finalizer_failure
|
|
93
|
+
@summary_finalizer_failure ||= ->(_error) {}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def build_execution_boundary
|
|
97
|
+
Core::Execution::Boundary.new(
|
|
98
|
+
emit_summary_record: ->(scope) { emit_summary_record(scope) },
|
|
99
|
+
summary_finalizer_failure: summary_finalizer_failure,
|
|
100
|
+
emit_non_standard_exception_summaries: -> { emit_non_standard_exception_summaries? }
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def current_scope = Core::ContextStore.current.current_scope
|
|
105
|
+
|
|
106
|
+
def summary_record_input(scope)
|
|
107
|
+
scope.summary_record_input
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
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
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def scope_payload
|
|
122
|
+
scope = Core::ContextStore.current.current_scope_or_snapshot
|
|
123
|
+
return {} unless scope
|
|
124
|
+
|
|
125
|
+
{
|
|
126
|
+
execution: scope.execution_hash,
|
|
127
|
+
neutral: scope.neutral_hash,
|
|
128
|
+
attributes: scope.attributes_hash,
|
|
129
|
+
carry: scope.carry_hash,
|
|
130
|
+
labels: scope.labels_hash
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def notify(command, payload:)
|
|
135
|
+
@request_mutex.synchronize do
|
|
136
|
+
@port.send({ command: command, payload: payload })
|
|
137
|
+
end
|
|
138
|
+
@child_stats.message_sent
|
|
139
|
+
nil
|
|
140
|
+
rescue StandardError => e
|
|
141
|
+
@child_stats.message_dropped(e)
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def request(command, timeout:)
|
|
146
|
+
reply = ::Ractor::Port.new
|
|
147
|
+
waiting_for_reply = false
|
|
148
|
+
@request_mutex.synchronize do
|
|
149
|
+
@port.send({ command: command, payload: { timeout: timeout }, reply: reply })
|
|
150
|
+
end
|
|
151
|
+
@child_stats.request_sent
|
|
152
|
+
waiting_for_reply = true
|
|
153
|
+
wait_for_reply(reply, timeout)
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
@child_stats.request_failed(e)
|
|
156
|
+
nil
|
|
157
|
+
ensure
|
|
158
|
+
close_reply(reply) if reply && !waiting_for_reply
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def effective_timeout(timeout)
|
|
162
|
+
timeout.equal?(Core::UNSET) ? DEFAULT_REQUEST_TIMEOUT : timeout
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def wait_for_reply(reply, timeout)
|
|
166
|
+
timeout_token = @timeout_scheduler.schedule(reply, timeout: timeout) if timeout
|
|
167
|
+
response = reply.receive
|
|
168
|
+
|
|
169
|
+
if response.equal?(REQUEST_TIMEOUT)
|
|
170
|
+
@child_stats.request_timed_out
|
|
171
|
+
nil
|
|
172
|
+
else
|
|
173
|
+
response
|
|
174
|
+
end
|
|
175
|
+
rescue StandardError
|
|
176
|
+
nil
|
|
177
|
+
ensure
|
|
178
|
+
@timeout_scheduler.cancel(timeout_token)
|
|
179
|
+
close_reply(reply)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def close_reply(reply)
|
|
183
|
+
PortLifecycle.close(reply)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Ractor
|
|
5
|
+
class RemoteSummaryRecord
|
|
6
|
+
def initialize(record)
|
|
7
|
+
@record = record
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def owned_summary_record_input
|
|
11
|
+
@owned_summary_record_input ||= Core::Fields::FieldSet.deep_symbolize_keys(@record)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Ractor
|
|
5
|
+
class ReplyTimeoutScheduler
|
|
6
|
+
THREAD_NAME = "julewire-ractor-reply-timeout"
|
|
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
|
+
def initialize(timeout_value:)
|
|
13
|
+
@timeout_value = timeout_value
|
|
14
|
+
@scheduler = Core::Scheduling::DeadlineScheduler.new(thread_name: THREAD_NAME, idle: :exit)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def schedule(reply, timeout:)
|
|
18
|
+
@scheduler.schedule(timeout) { send_timeout(reply) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def cancel(token)
|
|
22
|
+
return unless token
|
|
23
|
+
|
|
24
|
+
@scheduler.cancel(token)
|
|
25
|
+
rescue StandardError
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def send_timeout(reply)
|
|
32
|
+
reply.send(@timeout_value)
|
|
33
|
+
nil
|
|
34
|
+
rescue StandardError
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zeitwerk"
|
|
4
|
+
require "julewire/core"
|
|
5
|
+
|
|
6
|
+
module Julewire
|
|
7
|
+
module Ractor
|
|
8
|
+
class << self
|
|
9
|
+
def health
|
|
10
|
+
bridge_health
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def child_stats
|
|
14
|
+
child_runtime&.child_stats || {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reset_child_stats!
|
|
18
|
+
child_runtime&.reset_child_stats!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def fanout(destinations:, **)
|
|
22
|
+
Fanout.new(destinations: destinations, **)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def enable_default_destination_workers!
|
|
26
|
+
Core::Destinations.register(:default) { |name:, **options| Destination.new(name: name, **options) }
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def bridge_health
|
|
33
|
+
Bridge.health
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def child_runtime
|
|
37
|
+
runtime = Core::RuntimeLocator.current
|
|
38
|
+
runtime if runtime.respond_to?(:child_stats)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
loader = Zeitwerk::Loader.for_gem_extension(self)
|
|
44
|
+
loader.setup
|
|
45
|
+
Core::Destinations.register(:ractor) { |name:, **options| Ractor::Destination.new(name: name, **options) }
|
|
46
|
+
Core::Integration::Lifecycle.register_after_fork(:ractor, component: :bridge) { Ractor::Bridge.after_fork! }
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
def enable_experimental_ractor!
|
|
50
|
+
Ractor::Bridge.opt_in!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def ractor(*args, name: nil, &block)
|
|
54
|
+
raise ArgumentError, "block required" unless block
|
|
55
|
+
|
|
56
|
+
Ractor::Bridge.spawn(args: args, name: name, runtime: Core::RuntimeLocator.current, &block)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: julewire-ractor
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Alexander Grebennik
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: julewire-core
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: zeitwerk
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 2.8.1
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 2.8.1
|
|
40
|
+
description: Opt-in Ractor bridge that forwards records from worker ractors to a parent
|
|
41
|
+
Julewire runtime.
|
|
42
|
+
email:
|
|
43
|
+
- slbug@users.noreply.github.com
|
|
44
|
+
- sl.bug.sl@gmail.com
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- CHANGELOG.md
|
|
50
|
+
- LICENSE.txt
|
|
51
|
+
- README.md
|
|
52
|
+
- docs/bridge.md
|
|
53
|
+
- docs/development.md
|
|
54
|
+
- julewire-ractor.gemspec
|
|
55
|
+
- lib/julewire-ractor.rb
|
|
56
|
+
- lib/julewire/ractor.rb
|
|
57
|
+
- lib/julewire/ractor/bridge.rb
|
|
58
|
+
- lib/julewire/ractor/bridge/bridge_thread.rb
|
|
59
|
+
- lib/julewire/ractor/bridge/runtime_validation.rb
|
|
60
|
+
- lib/julewire/ractor/bridge/stats.rb
|
|
61
|
+
- lib/julewire/ractor/child_stats.rb
|
|
62
|
+
- lib/julewire/ractor/destination.rb
|
|
63
|
+
- lib/julewire/ractor/destination_worker.rb
|
|
64
|
+
- lib/julewire/ractor/fanout.rb
|
|
65
|
+
- lib/julewire/ractor/port_lifecycle.rb
|
|
66
|
+
- lib/julewire/ractor/remote_payload.rb
|
|
67
|
+
- lib/julewire/ractor/remote_runtime.rb
|
|
68
|
+
- lib/julewire/ractor/remote_summary_record.rb
|
|
69
|
+
- lib/julewire/ractor/reply_timeout_scheduler.rb
|
|
70
|
+
- lib/julewire/ractor/version.rb
|
|
71
|
+
homepage: https://github.com/slbug/julewire
|
|
72
|
+
licenses:
|
|
73
|
+
- MIT
|
|
74
|
+
metadata:
|
|
75
|
+
homepage_uri: https://github.com/slbug/julewire
|
|
76
|
+
source_code_uri: https://github.com/slbug/julewire/tree/main/gems/ractor
|
|
77
|
+
changelog_uri: https://github.com/slbug/julewire/blob/main/gems/ractor/CHANGELOG.md
|
|
78
|
+
rubygems_mfa_required: 'true'
|
|
79
|
+
rdoc_options: []
|
|
80
|
+
require_paths:
|
|
81
|
+
- lib
|
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '4.0'
|
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '0'
|
|
92
|
+
requirements: []
|
|
93
|
+
rubygems_version: 4.0.14
|
|
94
|
+
specification_version: 4
|
|
95
|
+
summary: Experimental Ractor bridge for julewire-core.
|
|
96
|
+
test_files: []
|