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.
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Ractor
5
+ VERSION = "1.0.0"
6
+ end
7
+ 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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "julewire/ractor"
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: []