ractor-wrapper 0.4.0 → 0.5.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 +4 -4
- data/.yardopts +1 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +5 -2
- data/DESIGN.md +1024 -0
- data/README.md +59 -42
- data/lib/ractor/wrapper/version.rb +1 -1
- data/lib/ractor/wrapper.rb +576 -66
- metadata +4 -3
data/lib/ractor/wrapper.rb
CHANGED
|
@@ -149,6 +149,8 @@ class Ractor
|
|
|
149
149
|
# * Can be configured per method whether to copy or move arguments and
|
|
150
150
|
# return values.
|
|
151
151
|
# * Blocks can be run in the calling Ractor or in the object Ractor.
|
|
152
|
+
# * Blocks running in the calling Ractor can re-enter the wrapper, calling
|
|
153
|
+
# other methods on it without deadlocking.
|
|
152
154
|
# * Raises exceptions thrown by the method.
|
|
153
155
|
# * Can serialize method calls for non-thread-safe objects, or run methods
|
|
154
156
|
# concurrently in multiple worker threads for thread-safe objects.
|
|
@@ -170,6 +172,9 @@ class Ractor
|
|
|
170
172
|
# later unless they are configured to run "in place". In particular,
|
|
171
173
|
# using blocks as a syntax to define callbacks can generally not be done
|
|
172
174
|
# through a wrapper.
|
|
175
|
+
# * Re-entrant calls from a block are not safe if the wrapped method
|
|
176
|
+
# invoked the block from a nested Fiber (such as inside an Enumerator)
|
|
177
|
+
# or from a spawned Thread. Such re-entrant calls may deadlock.
|
|
173
178
|
#
|
|
174
179
|
class Wrapper
|
|
175
180
|
##
|
|
@@ -178,7 +183,10 @@ class Ractor
|
|
|
178
183
|
class Error < ::Ractor::Error; end
|
|
179
184
|
|
|
180
185
|
##
|
|
181
|
-
# Raised when a {Ractor::Wrapper} server has crashed unexpectedly.
|
|
186
|
+
# Raised when a {Ractor::Wrapper} server has crashed unexpectedly. May
|
|
187
|
+
# also be raised in the calling Ractor when an in-flight method call is
|
|
188
|
+
# suspended at a block-yield point and the server (or its worker thread)
|
|
189
|
+
# crashes before the block result can be delivered.
|
|
182
190
|
#
|
|
183
191
|
class CrashedError < Error; end
|
|
184
192
|
|
|
@@ -252,6 +260,11 @@ class Ractor
|
|
|
252
260
|
# thread-safe, a value of 2 or more allows concurrent calls. Leave at
|
|
253
261
|
# the default of 0 to handle calls sequentially without worker threads.
|
|
254
262
|
#
|
|
263
|
+
# The number of worker threads only needs to reflect the desired
|
|
264
|
+
# concurrency of independent calls. It does not need to be sized to the
|
|
265
|
+
# depth of re-entrant block calls, because suspended methods do not
|
|
266
|
+
# occupy a worker thread while waiting for a block to complete.
|
|
267
|
+
#
|
|
255
268
|
# @param value [Integer]
|
|
256
269
|
#
|
|
257
270
|
def threads=(value)
|
|
@@ -298,6 +311,12 @@ class Ractor
|
|
|
298
311
|
# the wrapper sends a message back to the caller to execute the block
|
|
299
312
|
# in its original context. This means the block will have access to its
|
|
300
313
|
# lexical scope and any other data available to the calling Ractor.
|
|
314
|
+
# Such blocks may safely re-enter the wrapper to invoke other methods
|
|
315
|
+
# on it, *unless* the wrapped method invoked the block from a nested
|
|
316
|
+
# Fiber (such as inside an Enumerator) or a spawned Thread, in which
|
|
317
|
+
# case re-entrant calls from the block may deadlock. If you need to
|
|
318
|
+
# invoke the block from a nested Fiber or a spawned Thread and the
|
|
319
|
+
# block does not need re-entrancy, prefer the `:wrapped` setting.
|
|
301
320
|
# * `:wrapped` - Blocks are executed directly in the wrapped object's
|
|
302
321
|
# context. This does not require any communication, but it means the
|
|
303
322
|
# block is removed from the caller's environment and does not have
|
|
@@ -505,7 +524,10 @@ class Ractor
|
|
|
505
524
|
# also be set via the configuration block. Defaults to the object_id.
|
|
506
525
|
# @param threads [Integer] The number of worker threads to run.
|
|
507
526
|
# Defaults to 0, which causes the wrapper to run sequentially without
|
|
508
|
-
# spawning workers.
|
|
527
|
+
# spawning workers. Sized to the desired concurrency of independent
|
|
528
|
+
# calls; does not need to account for re-entrant block calls, since
|
|
529
|
+
# suspended methods do not occupy a worker thread while waiting for a
|
|
530
|
+
# block to complete. Can also be set via the configuration block.
|
|
509
531
|
# @param arguments [:move,:copy] How to communicate method arguments by
|
|
510
532
|
# default. If not specified, defaults to `:copy`.
|
|
511
533
|
# @param results [:move,:copy,:void] How to communicate method return
|
|
@@ -641,7 +663,7 @@ class Ractor
|
|
|
641
663
|
loop do
|
|
642
664
|
reply_message = reply_port.receive
|
|
643
665
|
case reply_message
|
|
644
|
-
when
|
|
666
|
+
when FiberYieldMessage, BlockingYieldMessage
|
|
645
667
|
handle_yield(reply_message, transaction, settings, method_name, &)
|
|
646
668
|
when ReturnMessage
|
|
647
669
|
maybe_log("Received result", method_name: method_name, transaction: transaction)
|
|
@@ -657,7 +679,9 @@ class Ractor
|
|
|
657
679
|
|
|
658
680
|
##
|
|
659
681
|
# Request that the wrapper stop. All currently running calls will complete
|
|
660
|
-
# before the wrapper actually terminates
|
|
682
|
+
# before the wrapper actually terminates, including calls that are
|
|
683
|
+
# suspended waiting for a re-entrant block to return. New calls submitted
|
|
684
|
+
# after the stop request will fail with {StoppedError}.
|
|
661
685
|
#
|
|
662
686
|
# This method is idempotent and can be called multiple times (even from
|
|
663
687
|
# different ractors).
|
|
@@ -776,9 +800,39 @@ class Ractor
|
|
|
776
800
|
|
|
777
801
|
##
|
|
778
802
|
# @private
|
|
779
|
-
# Message sent from a server to request a yield block run
|
|
803
|
+
# Message sent from a server to request a yield block run, in the
|
|
804
|
+
# blocking-fallback path. The server allocates a temporary reply_port and
|
|
805
|
+
# blocks waiting for a response on it. Used when the wrapped object yields
|
|
806
|
+
# from a context where Fiber.yield is not safe (e.g., inside a nested
|
|
807
|
+
# fiber such as an Enumerator's generator, or in a spawned thread).
|
|
808
|
+
#
|
|
809
|
+
BlockingYieldMessage = ::Data.define(:args, :kwargs, :reply_port)
|
|
810
|
+
|
|
811
|
+
##
|
|
812
|
+
# @private
|
|
813
|
+
# Message sent from a server to request a yield block run, in the
|
|
814
|
+
# fiber-suspend path. The server suspends its method-handling fiber and
|
|
815
|
+
# is resumed when a FiberReturnMessage or FiberExceptionMessage tagged
|
|
816
|
+
# with the same fiber_id arrives back on the server's main port.
|
|
780
817
|
#
|
|
781
|
-
|
|
818
|
+
FiberYieldMessage = ::Data.define(:args, :kwargs, :fiber_id)
|
|
819
|
+
|
|
820
|
+
##
|
|
821
|
+
# @private
|
|
822
|
+
# Message sent from a caller back to a server, carrying the result of a
|
|
823
|
+
# block invoked via the fiber-suspend path. The fiber_id identifies which
|
|
824
|
+
# suspended fiber on the server should be resumed with this value.
|
|
825
|
+
#
|
|
826
|
+
FiberReturnMessage = ::Data.define(:value, :fiber_id)
|
|
827
|
+
|
|
828
|
+
##
|
|
829
|
+
# @private
|
|
830
|
+
# Message sent from a caller back to a server, carrying an exception
|
|
831
|
+
# raised by a block invoked via the fiber-suspend path. The fiber_id
|
|
832
|
+
# identifies which suspended fiber on the server should be resumed and
|
|
833
|
+
# have this exception raised inside it.
|
|
834
|
+
#
|
|
835
|
+
FiberExceptionMessage = ::Data.define(:exception, :fiber_id)
|
|
782
836
|
|
|
783
837
|
private
|
|
784
838
|
|
|
@@ -843,6 +897,11 @@ class Ractor
|
|
|
843
897
|
|
|
844
898
|
##
|
|
845
899
|
# Handle a call to a block directed to run in the caller environment.
|
|
900
|
+
# Dispatches the block result or exception based on which yield-message
|
|
901
|
+
# variant arrived: a FiberYieldMessage routes the response back to the
|
|
902
|
+
# server's main port (so the suspended fiber can be resumed), while a
|
|
903
|
+
# BlockingYieldMessage routes it to the temporary reply_port the server
|
|
904
|
+
# is blocked on.
|
|
846
905
|
#
|
|
847
906
|
def handle_yield(message, transaction, settings, method_name)
|
|
848
907
|
maybe_log("Yielding to block", method_name: method_name, transaction: transaction)
|
|
@@ -850,14 +909,14 @@ class Ractor
|
|
|
850
909
|
block_result = yield(*message.args, **message.kwargs)
|
|
851
910
|
block_result = nil if settings.block_results == :void
|
|
852
911
|
maybe_log("Sending block result", method_name: method_name, transaction: transaction)
|
|
853
|
-
message
|
|
912
|
+
send_block_result(message, block_result, settings)
|
|
854
913
|
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
855
914
|
maybe_log("Sending block exception", method_name: method_name, transaction: transaction)
|
|
856
915
|
begin
|
|
857
|
-
message
|
|
916
|
+
send_block_exception(message, e)
|
|
858
917
|
rescue ::StandardError
|
|
859
918
|
begin
|
|
860
|
-
message
|
|
919
|
+
send_block_exception(message, ::StandardError.new(e.inspect))
|
|
861
920
|
rescue ::StandardError
|
|
862
921
|
maybe_log("Failure to send block reply", method_name: method_name, transaction: transaction)
|
|
863
922
|
end
|
|
@@ -865,6 +924,33 @@ class Ractor
|
|
|
865
924
|
end
|
|
866
925
|
end
|
|
867
926
|
|
|
927
|
+
##
|
|
928
|
+
# Send a block return value to the appropriate destination based on the
|
|
929
|
+
# yield-message variant.
|
|
930
|
+
#
|
|
931
|
+
def send_block_result(message, value, settings)
|
|
932
|
+
case message
|
|
933
|
+
when FiberYieldMessage
|
|
934
|
+
@port.send(FiberReturnMessage.new(value: value, fiber_id: message.fiber_id),
|
|
935
|
+
move: settings.block_results == :move)
|
|
936
|
+
when BlockingYieldMessage
|
|
937
|
+
message.reply_port.send(ReturnMessage.new(value), move: settings.block_results == :move)
|
|
938
|
+
end
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
##
|
|
942
|
+
# Send a block exception to the appropriate destination based on the
|
|
943
|
+
# yield-message variant.
|
|
944
|
+
#
|
|
945
|
+
def send_block_exception(message, exception)
|
|
946
|
+
case message
|
|
947
|
+
when FiberYieldMessage
|
|
948
|
+
@port.send(FiberExceptionMessage.new(exception: exception, fiber_id: message.fiber_id))
|
|
949
|
+
when BlockingYieldMessage
|
|
950
|
+
message.reply_port.send(ExceptionMessage.new(exception))
|
|
951
|
+
end
|
|
952
|
+
end
|
|
953
|
+
|
|
868
954
|
##
|
|
869
955
|
# Prints out a log message
|
|
870
956
|
#
|
|
@@ -891,6 +977,168 @@ class Ractor
|
|
|
891
977
|
# * Either sequentially or concurrently using worker threads.
|
|
892
978
|
#
|
|
893
979
|
class Server
|
|
980
|
+
##
|
|
981
|
+
# @private
|
|
982
|
+
#
|
|
983
|
+
# Multi-queue work dispatcher for the threaded server. Routes new
|
|
984
|
+
# `CallMessage`s through a shared queue (any worker may pick them up)
|
|
985
|
+
# and routes fiber resumes (`FiberReturnMessage` / `FiberExceptionMessage`)
|
|
986
|
+
# through per-worker queues so a suspended fiber is always resumed by
|
|
987
|
+
# the same worker thread that started it (Ruby fibers cannot be resumed
|
|
988
|
+
# from a different thread than their last resumer).
|
|
989
|
+
#
|
|
990
|
+
# All public methods are thread-safe. The internal mutex guards the
|
|
991
|
+
# shared queue, all per-worker queues, the fiber→worker map, and the
|
|
992
|
+
# closed/notified state. `dequeue` blocks on a single shared
|
|
993
|
+
# `ConditionVariable`; producers `broadcast` rather than `signal` so
|
|
994
|
+
# workers waiting on per-worker queues are not starved by
|
|
995
|
+
# shared-queue activity.
|
|
996
|
+
#
|
|
997
|
+
class Dispatcher
|
|
998
|
+
CLOSED = [:closed, nil].freeze
|
|
999
|
+
TERMINATE = [:terminate, nil].freeze
|
|
1000
|
+
|
|
1001
|
+
##
|
|
1002
|
+
# @param num_workers [Integer] number of per-worker queues to allocate.
|
|
1003
|
+
# Workers are addressed by integers in the range `[0, num_workers)`.
|
|
1004
|
+
#
|
|
1005
|
+
def initialize(num_workers)
|
|
1006
|
+
@mutex = ::Mutex.new
|
|
1007
|
+
@cond = ::ConditionVariable.new
|
|
1008
|
+
@shared_queue = []
|
|
1009
|
+
@worker_queues = ::Array.new(num_workers) { [] }
|
|
1010
|
+
@fiber_to_worker = {}
|
|
1011
|
+
@closed = false
|
|
1012
|
+
@crashed = false
|
|
1013
|
+
@closed_notified = ::Array.new(num_workers, false)
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
##
|
|
1017
|
+
# Push a new `CallMessage` onto the shared queue.
|
|
1018
|
+
# @return [Boolean] `true` normally, `false` if `close` has been called.
|
|
1019
|
+
#
|
|
1020
|
+
def enqueue_call(message)
|
|
1021
|
+
@mutex.synchronize do
|
|
1022
|
+
return false if @closed
|
|
1023
|
+
@shared_queue.push(message)
|
|
1024
|
+
@cond.broadcast
|
|
1025
|
+
true
|
|
1026
|
+
end
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
##
|
|
1030
|
+
# Push a fiber-resume message (`FiberReturnMessage` /
|
|
1031
|
+
# `FiberExceptionMessage`) onto the queue of the worker that owns the
|
|
1032
|
+
# fiber identified by `message.fiber_id`.
|
|
1033
|
+
# @return [Boolean] `true` if dispatched, `false` if the fiber_id is
|
|
1034
|
+
# not registered (e.g. fiber already finished or was aborted).
|
|
1035
|
+
#
|
|
1036
|
+
def enqueue_fiber_resume(message)
|
|
1037
|
+
@mutex.synchronize do
|
|
1038
|
+
worker_num = @fiber_to_worker[message.fiber_id]
|
|
1039
|
+
return false unless worker_num
|
|
1040
|
+
@worker_queues[worker_num].push(message)
|
|
1041
|
+
@cond.broadcast
|
|
1042
|
+
true
|
|
1043
|
+
end
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
##
|
|
1047
|
+
# Block until the worker has work. Priority order:
|
|
1048
|
+
#
|
|
1049
|
+
# 1. Per-worker queue (always — these are resumes for fibers this
|
|
1050
|
+
# worker owns; they must be drained even after close).
|
|
1051
|
+
# 2. Shared queue, but only if `accept_calls` is true and the
|
|
1052
|
+
# dispatcher is not yet closed.
|
|
1053
|
+
# 3. The `CLOSED` sentinel (`[:closed, nil]`), returned exactly once
|
|
1054
|
+
# per worker, the first time the worker would otherwise have
|
|
1055
|
+
# blocked after `close` was called. Used to wake the worker so it
|
|
1056
|
+
# can transition to a draining state. Subsequent calls behave
|
|
1057
|
+
# normally and may block again.
|
|
1058
|
+
#
|
|
1059
|
+
# If `crash_close` has been called, returns `TERMINATE`
|
|
1060
|
+
# (`[:terminate, nil]`) immediately whenever the per-worker queue is
|
|
1061
|
+
# empty — signaling the worker to exit even if it has pending fibers.
|
|
1062
|
+
# The per-worker queue is still drained first so any in-flight resumes
|
|
1063
|
+
# complete normally.
|
|
1064
|
+
#
|
|
1065
|
+
# @return [Array(Symbol, Object)] one of `[:resume, msg]`,
|
|
1066
|
+
# `[:call, msg]`, `CLOSED`, or `TERMINATE`.
|
|
1067
|
+
#
|
|
1068
|
+
def dequeue(worker_num, accept_calls:)
|
|
1069
|
+
@mutex.synchronize do
|
|
1070
|
+
loop do
|
|
1071
|
+
if (msg = @worker_queues[worker_num].shift)
|
|
1072
|
+
return [:resume, msg]
|
|
1073
|
+
end
|
|
1074
|
+
return TERMINATE if @crashed
|
|
1075
|
+
if accept_calls && !@closed && (msg = @shared_queue.shift)
|
|
1076
|
+
return [:call, msg]
|
|
1077
|
+
end
|
|
1078
|
+
if @closed && !@closed_notified[worker_num]
|
|
1079
|
+
@closed_notified[worker_num] = true
|
|
1080
|
+
return CLOSED
|
|
1081
|
+
end
|
|
1082
|
+
@cond.wait(@mutex)
|
|
1083
|
+
end
|
|
1084
|
+
end
|
|
1085
|
+
end
|
|
1086
|
+
|
|
1087
|
+
##
|
|
1088
|
+
# Atomically associate `fiber_id` with `worker_num` so subsequent
|
|
1089
|
+
# `enqueue_fiber_resume` calls land on the right worker queue.
|
|
1090
|
+
#
|
|
1091
|
+
def register_fiber(fiber_id, worker_num)
|
|
1092
|
+
@mutex.synchronize { @fiber_to_worker[fiber_id] = worker_num }
|
|
1093
|
+
end
|
|
1094
|
+
|
|
1095
|
+
##
|
|
1096
|
+
# Remove the fiber→worker mapping. Idempotent.
|
|
1097
|
+
#
|
|
1098
|
+
def unregister_fiber(fiber_id)
|
|
1099
|
+
@mutex.synchronize { @fiber_to_worker.delete(fiber_id) }
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
##
|
|
1103
|
+
# Mark closed and wake all blocked workers. Drains the shared queue
|
|
1104
|
+
# and returns its previous contents so the caller can refuse them
|
|
1105
|
+
# (with `StoppedError`) on behalf of their pending callers. Idempotent
|
|
1106
|
+
# — repeated calls return `[]`.
|
|
1107
|
+
# @return [Array] the messages that were in the shared queue.
|
|
1108
|
+
#
|
|
1109
|
+
def close
|
|
1110
|
+
@mutex.synchronize do
|
|
1111
|
+
return [] if @closed
|
|
1112
|
+
@closed = true
|
|
1113
|
+
drained = @shared_queue.dup
|
|
1114
|
+
@shared_queue.clear
|
|
1115
|
+
@cond.broadcast
|
|
1116
|
+
drained
|
|
1117
|
+
end
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
##
|
|
1121
|
+
# Mark closed AND crashed: future `dequeue` calls return `TERMINATE`
|
|
1122
|
+
# whenever the per-worker queue is empty (rather than blocking),
|
|
1123
|
+
# signaling workers to exit immediately so their `ensure` blocks can
|
|
1124
|
+
# abort any pending fibers. Used on server crash, where no further
|
|
1125
|
+
# fiber-resume messages will arrive. Idempotent (sets crashed even if
|
|
1126
|
+
# already closed). Returns the drained shared queue.
|
|
1127
|
+
# @return [Array] the messages that were in the shared queue.
|
|
1128
|
+
#
|
|
1129
|
+
def crash_close
|
|
1130
|
+
@mutex.synchronize do
|
|
1131
|
+
already_closed = @closed
|
|
1132
|
+
@closed = true
|
|
1133
|
+
@crashed = true
|
|
1134
|
+
drained = already_closed ? [] : @shared_queue.dup
|
|
1135
|
+
@shared_queue.clear
|
|
1136
|
+
@cond.broadcast
|
|
1137
|
+
drained
|
|
1138
|
+
end
|
|
1139
|
+
end
|
|
1140
|
+
end
|
|
1141
|
+
|
|
894
1142
|
##
|
|
895
1143
|
# @private
|
|
896
1144
|
# Create and run a server hosted in the current Ractor
|
|
@@ -920,6 +1168,13 @@ class Ractor
|
|
|
920
1168
|
@enable_logging = enable_logging
|
|
921
1169
|
@threads_requested = threads.positive? ? threads : false
|
|
922
1170
|
@join_requests = []
|
|
1171
|
+
# Sequential mode only: maps fiber_id (Integer) => Fiber for
|
|
1172
|
+
# method-handling fibers that have suspended (via Fiber.yield) waiting
|
|
1173
|
+
# for a block result. Used to route incoming
|
|
1174
|
+
# FiberReturnMessage/FiberExceptionMessage back to the right fiber.
|
|
1175
|
+
# Threaded mode tracks pending fibers per-worker (in `worker_loop`'s
|
|
1176
|
+
# local `pending` hash) and routes via `Dispatcher`.
|
|
1177
|
+
@pending_fibers = {}
|
|
923
1178
|
end
|
|
924
1179
|
|
|
925
1180
|
##
|
|
@@ -956,15 +1211,20 @@ class Ractor
|
|
|
956
1211
|
end
|
|
957
1212
|
|
|
958
1213
|
##
|
|
959
|
-
# Start the worker threads. Each thread picks up
|
|
960
|
-
#
|
|
1214
|
+
# Start the worker threads. Each thread picks up work via the
|
|
1215
|
+
# `Dispatcher`, which routes new `CallMessage`s through a shared queue
|
|
1216
|
+
# and routes fiber-resume messages to the specific worker that owns the
|
|
1217
|
+
# suspended fiber. Called only if worker threading is enabled.
|
|
961
1218
|
#
|
|
962
1219
|
def start_workers
|
|
963
1220
|
maybe_log("Spawning #{@threads_requested} worker threads")
|
|
964
|
-
@
|
|
1221
|
+
@dispatcher = Dispatcher.new(@threads_requested)
|
|
965
1222
|
@active_workers = {}
|
|
966
|
-
(
|
|
967
|
-
@active_workers[worker_num] = ::Thread.new
|
|
1223
|
+
(0...@threads_requested).each do |worker_num|
|
|
1224
|
+
@active_workers[worker_num] = ::Thread.new do
|
|
1225
|
+
::Thread.current.name = "ractor-wrapper:#{@name}:worker:#{worker_num}"
|
|
1226
|
+
worker_loop(worker_num)
|
|
1227
|
+
end
|
|
968
1228
|
end
|
|
969
1229
|
end
|
|
970
1230
|
|
|
@@ -972,8 +1232,17 @@ class Ractor
|
|
|
972
1232
|
# This is the main loop, listening on the inbox and handling messages for
|
|
973
1233
|
# normal operation:
|
|
974
1234
|
#
|
|
975
|
-
# * If it receives a CallMessage, it either runs the method
|
|
976
|
-
# sequential mode) or
|
|
1235
|
+
# * If it receives a CallMessage, it either runs the method in a
|
|
1236
|
+
# fiber (sequential mode) or hands it to the `Dispatcher`'s shared
|
|
1237
|
+
# queue (threaded mode). In both modes the method body executes
|
|
1238
|
+
# inside a `Fiber` so it can suspend (via `Fiber.yield`) when its
|
|
1239
|
+
# caller-side block needs to make a re-entrant call back into this
|
|
1240
|
+
# wrapper.
|
|
1241
|
+
# * If it receives a FiberReturnMessage or FiberExceptionMessage, it
|
|
1242
|
+
# resumes the suspended fiber. In sequential mode the fiber lives
|
|
1243
|
+
# in `@pending_fibers` and is resumed inline. In threaded mode the
|
|
1244
|
+
# `Dispatcher` routes the message to the per-worker queue of the
|
|
1245
|
+
# fiber's owning worker.
|
|
977
1246
|
# * If it receives a StopMessage, it exits the main loop and proceeds
|
|
978
1247
|
# to the termination logic.
|
|
979
1248
|
# * If it receives a JoinMessage, it adds it to the list of join ports
|
|
@@ -988,36 +1257,128 @@ class Ractor
|
|
|
988
1257
|
maybe_log("Waiting for message in running phase")
|
|
989
1258
|
message = @port.receive
|
|
990
1259
|
case message
|
|
991
|
-
when CallMessage
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
@queue.enq(message)
|
|
995
|
-
else
|
|
996
|
-
handle_method(message)
|
|
997
|
-
end
|
|
1260
|
+
when CallMessage then dispatch_call(message)
|
|
1261
|
+
when FiberReturnMessage, FiberExceptionMessage then dispatch_fiber_resume(message)
|
|
1262
|
+
when JoinMessage then @join_requests << message.reply_port
|
|
998
1263
|
when WorkerStoppedMessage
|
|
999
1264
|
maybe_log("Received unexpected WorkerStoppedMessage")
|
|
1000
1265
|
@active_workers.delete(message.worker_num) if @threads_requested
|
|
1001
1266
|
break
|
|
1002
1267
|
when StopMessage
|
|
1003
1268
|
maybe_log("Received stop")
|
|
1269
|
+
drain_pending_fibers unless @threads_requested
|
|
1004
1270
|
break
|
|
1271
|
+
end
|
|
1272
|
+
end
|
|
1273
|
+
end
|
|
1274
|
+
|
|
1275
|
+
##
|
|
1276
|
+
# Dispatch a `CallMessage` received by the main loop. In sequential
|
|
1277
|
+
# mode the method is started inline as a new fiber; in threaded mode
|
|
1278
|
+
# it is handed to the dispatcher's shared queue for any worker to pick
|
|
1279
|
+
# up.
|
|
1280
|
+
#
|
|
1281
|
+
def dispatch_call(message)
|
|
1282
|
+
maybe_log("Received CallMessage", call_message: message)
|
|
1283
|
+
if @threads_requested
|
|
1284
|
+
@dispatcher.enqueue_call(message)
|
|
1285
|
+
else
|
|
1286
|
+
start_method_fiber(message)
|
|
1287
|
+
end
|
|
1288
|
+
end
|
|
1289
|
+
|
|
1290
|
+
##
|
|
1291
|
+
# Route a fiber-resume message (`FiberReturnMessage` /
|
|
1292
|
+
# `FiberExceptionMessage`) to its owning fiber. In sequential mode this
|
|
1293
|
+
# resumes the fiber inline; in threaded mode the dispatcher routes it to
|
|
1294
|
+
# the per-worker queue of the worker that started the fiber. Logs and
|
|
1295
|
+
# discards if the fiber is no longer registered (likely already finished
|
|
1296
|
+
# or aborted by a crashed worker).
|
|
1297
|
+
#
|
|
1298
|
+
def dispatch_fiber_resume(message)
|
|
1299
|
+
maybe_log("Routing fiber resume", fiber_id: message.fiber_id)
|
|
1300
|
+
if @threads_requested
|
|
1301
|
+
return if @dispatcher.enqueue_fiber_resume(message)
|
|
1302
|
+
maybe_log("Discarding orphan fiber resume", fiber_id: message.fiber_id)
|
|
1303
|
+
else
|
|
1304
|
+
resume_method_fiber(message)
|
|
1305
|
+
end
|
|
1306
|
+
end
|
|
1307
|
+
|
|
1308
|
+
##
|
|
1309
|
+
# Sequential-mode stopping phase. After receiving a StopMessage, continue
|
|
1310
|
+
# accepting fiber-result messages (and join requests) so that any
|
|
1311
|
+
# currently-suspended method-handling fiber can complete. New
|
|
1312
|
+
# `CallMessage`s are refused with `StoppedError`. Returns once
|
|
1313
|
+
# `@pending_fibers` is empty.
|
|
1314
|
+
#
|
|
1315
|
+
def drain_pending_fibers
|
|
1316
|
+
until @pending_fibers.empty?
|
|
1317
|
+
maybe_log("Waiting for pending fibers to complete")
|
|
1318
|
+
message = @port.receive
|
|
1319
|
+
case message
|
|
1320
|
+
when CallMessage
|
|
1321
|
+
refuse_method(message)
|
|
1322
|
+
when FiberReturnMessage, FiberExceptionMessage
|
|
1323
|
+
resume_method_fiber(message)
|
|
1324
|
+
when StopMessage
|
|
1325
|
+
maybe_log("Stop received when already stopping")
|
|
1005
1326
|
when JoinMessage
|
|
1006
1327
|
maybe_log("Received and queueing join request")
|
|
1007
1328
|
@join_requests << message.reply_port
|
|
1329
|
+
else
|
|
1330
|
+
maybe_log("Unexpected message when draining pending fibers: #{message.class.name}")
|
|
1008
1331
|
end
|
|
1009
1332
|
end
|
|
1010
1333
|
end
|
|
1011
1334
|
|
|
1012
1335
|
##
|
|
1013
|
-
#
|
|
1014
|
-
#
|
|
1015
|
-
#
|
|
1336
|
+
# Spawn a fiber to handle a CallMessage in sequential mode. If the
|
|
1337
|
+
# method-handling fiber suspends via Fiber.yield (because its caller-side
|
|
1338
|
+
# block re-entered this wrapper), the fiber is registered in
|
|
1339
|
+
# `@pending_fibers` so that the matching block-return message can later
|
|
1340
|
+
# resume it.
|
|
1341
|
+
#
|
|
1342
|
+
def start_method_fiber(message)
|
|
1343
|
+
fiber = ::Fiber.new { handle_method(message) }
|
|
1344
|
+
fiber_id = fiber.object_id
|
|
1345
|
+
@pending_fibers[fiber_id] = fiber
|
|
1346
|
+
maybe_log("Starting method fiber", call_message: message, fiber_id: fiber_id)
|
|
1347
|
+
fiber.resume
|
|
1348
|
+
@pending_fibers.delete(fiber_id) unless fiber.alive?
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
##
|
|
1352
|
+
# Resume a previously-suspended method-handling fiber, delivering the
|
|
1353
|
+
# block-result message as the return value of its Fiber.yield call.
|
|
1354
|
+
# Silently ignores unknown fiber_ids (the fiber may have been aborted).
|
|
1355
|
+
#
|
|
1356
|
+
def resume_method_fiber(message)
|
|
1357
|
+
fiber = @pending_fibers[message.fiber_id]
|
|
1358
|
+
return unless fiber
|
|
1359
|
+
maybe_log("Resuming method fiber", fiber_id: message.fiber_id)
|
|
1360
|
+
fiber.resume(message)
|
|
1361
|
+
@pending_fibers.delete(message.fiber_id) unless fiber.alive?
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
##
|
|
1365
|
+
# This signals workers to stop by closing the dispatcher, and then
|
|
1366
|
+
# waits for all workers to report in that they have stopped. It is
|
|
1367
|
+
# called only if worker threading is enabled.
|
|
1368
|
+
#
|
|
1369
|
+
# Closing the dispatcher drains any never-dispatched `CallMessage`s
|
|
1370
|
+
# from its shared queue; those are refused immediately so the
|
|
1371
|
+
# corresponding callers do not block forever. The dispatcher also
|
|
1372
|
+
# delivers a one-shot closed signal to each worker so they can
|
|
1373
|
+
# transition to a draining state.
|
|
1016
1374
|
#
|
|
1017
1375
|
# Responds to messages to indicate the wrapper is stopping and no longer
|
|
1018
1376
|
# accepting new method requests:
|
|
1019
1377
|
#
|
|
1020
1378
|
# * If it receives a CallMessage, it sends back a refusal exception.
|
|
1379
|
+
# * If it receives a FiberReturnMessage or FiberExceptionMessage, it
|
|
1380
|
+
# forwards it to the dispatcher so the owning worker can resume its
|
|
1381
|
+
# suspended fiber and complete the in-flight call.
|
|
1021
1382
|
# * If it receives a StopMessage, it does nothing (i.e. the stop
|
|
1022
1383
|
# operation is idempotent).
|
|
1023
1384
|
# * If it receives a JoinMessage, it adds it to the list of join ports
|
|
@@ -1031,13 +1392,16 @@ class Ractor
|
|
|
1031
1392
|
# stopped.
|
|
1032
1393
|
#
|
|
1033
1394
|
def stop_workers
|
|
1034
|
-
@
|
|
1395
|
+
drained = @dispatcher.close
|
|
1396
|
+
drained.each { |message| refuse_method(message) }
|
|
1035
1397
|
until @active_workers.empty?
|
|
1036
1398
|
maybe_log("Waiting for message in stopping phase")
|
|
1037
1399
|
message = @port.receive
|
|
1038
1400
|
case message
|
|
1039
1401
|
when CallMessage
|
|
1040
1402
|
refuse_method(message)
|
|
1403
|
+
when FiberReturnMessage, FiberExceptionMessage
|
|
1404
|
+
dispatch_fiber_resume(message)
|
|
1041
1405
|
when WorkerStoppedMessage
|
|
1042
1406
|
maybe_log("Acknowledged WorkerStoppedMessage: #{message.worker_num}")
|
|
1043
1407
|
@active_workers.delete(message.worker_num)
|
|
@@ -1091,9 +1455,10 @@ class Ractor
|
|
|
1091
1455
|
def crash_cleanup
|
|
1092
1456
|
maybe_log("Running crash cleanup after: #{@crash_exception.message} (#{@crash_exception.class})")
|
|
1093
1457
|
error = CrashedError.new("Server crashed: #{@crash_exception.message} (#{@crash_exception.class})")
|
|
1094
|
-
# `@
|
|
1095
|
-
# anyway just in case a crash happened during setup
|
|
1096
|
-
|
|
1458
|
+
# `@dispatcher` should not be nil in threaded mode, but we're
|
|
1459
|
+
# checking anyway just in case a crash happened during setup
|
|
1460
|
+
drain_dispatcher_after_crash(@dispatcher, error) if @threads_requested && @dispatcher
|
|
1461
|
+
abort_pending_fibers(@pending_fibers, error) unless @threads_requested
|
|
1097
1462
|
drain_inbox_after_crash(@port, error)
|
|
1098
1463
|
# `@active_workers` should not be nil in threaded mode, but we're
|
|
1099
1464
|
# checking anyway just in case a crash happened during setup
|
|
@@ -1104,22 +1469,38 @@ class Ractor
|
|
|
1104
1469
|
end
|
|
1105
1470
|
|
|
1106
1471
|
##
|
|
1107
|
-
#
|
|
1108
|
-
#
|
|
1472
|
+
# After a crash, raises +error+ inside each suspended method-handling
|
|
1473
|
+
# fiber. The exception emerges from the fiber's `Fiber.yield` call and is
|
|
1474
|
+
# caught by `handle_method`'s rescue chain, which sends an
|
|
1475
|
+
# `ExceptionMessage` to the fiber's reply_port so the caller observes a
|
|
1476
|
+
# `CrashedError`.
|
|
1109
1477
|
#
|
|
1110
|
-
def
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1478
|
+
def abort_pending_fibers(pending_fibers, error)
|
|
1479
|
+
pending_fibers.each_pair do |fiber_id, fiber|
|
|
1480
|
+
maybe_log("Aborting suspended fiber", fiber_id: fiber_id)
|
|
1481
|
+
fiber.raise(error)
|
|
1482
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
1483
|
+
maybe_log("Suppressed exception during abort_pending_fibers: #{e.message} (#{e.class})",
|
|
1484
|
+
fiber_id: fiber_id)
|
|
1485
|
+
end
|
|
1486
|
+
pending_fibers.clear
|
|
1487
|
+
end
|
|
1488
|
+
|
|
1489
|
+
##
|
|
1490
|
+
# Closes the dispatcher after a crash, then sends an error response
|
|
1491
|
+
# to the callers of any `CallMessage`s that were still queued in the
|
|
1492
|
+
# shared queue (and therefore had not yet been dispatched to a worker).
|
|
1493
|
+
# Workers themselves clean up their own in-flight fibers via their
|
|
1494
|
+
# ensure blocks.
|
|
1495
|
+
#
|
|
1496
|
+
def drain_dispatcher_after_crash(dispatcher, error)
|
|
1497
|
+
dispatcher.crash_close.each do |message|
|
|
1498
|
+
message.reply_port.send(ExceptionMessage.new(error))
|
|
1499
|
+
rescue ::Ractor::Error
|
|
1500
|
+
maybe_log("Failed to send crash error to queued caller", call_message: message)
|
|
1120
1501
|
end
|
|
1121
1502
|
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
1122
|
-
maybe_log("Suppressed exception during
|
|
1503
|
+
maybe_log("Suppressed exception during drain_dispatcher_after_crash: " \
|
|
1123
1504
|
"#{e.message} (#{e.class})")
|
|
1124
1505
|
end
|
|
1125
1506
|
|
|
@@ -1149,7 +1530,7 @@ class Ractor
|
|
|
1149
1530
|
end
|
|
1150
1531
|
when JoinMessage
|
|
1151
1532
|
send_join_reply(message.reply_port)
|
|
1152
|
-
when WorkerStoppedMessage, StopMessage
|
|
1533
|
+
when WorkerStoppedMessage, StopMessage, FiberReturnMessage, FiberExceptionMessage
|
|
1153
1534
|
# Ignore
|
|
1154
1535
|
end
|
|
1155
1536
|
end
|
|
@@ -1176,16 +1557,60 @@ class Ractor
|
|
|
1176
1557
|
# is closed, the worker will send an acknowledgement message and then
|
|
1177
1558
|
# terminate.
|
|
1178
1559
|
#
|
|
1179
|
-
def
|
|
1560
|
+
def worker_loop(worker_num)
|
|
1180
1561
|
maybe_log("Worker starting", worker_num: worker_num)
|
|
1562
|
+
pending = {}
|
|
1563
|
+
crash_exception = nil
|
|
1564
|
+
begin
|
|
1565
|
+
run_worker_dispatch_loop(worker_num, pending)
|
|
1566
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
1567
|
+
crash_exception = e
|
|
1568
|
+
raise
|
|
1569
|
+
ensure
|
|
1570
|
+
cleanup_worker(worker_num, pending, crash_exception)
|
|
1571
|
+
end
|
|
1572
|
+
end
|
|
1573
|
+
|
|
1574
|
+
##
|
|
1575
|
+
# The dispatch loop body for a worker thread. Loops on
|
|
1576
|
+
# `@dispatcher.dequeue` and routes each result to the appropriate
|
|
1577
|
+
# handler. Exits when the dispatcher signals termination, or when a
|
|
1578
|
+
# graceful close has been observed and the local pending hash is empty.
|
|
1579
|
+
#
|
|
1580
|
+
def run_worker_dispatch_loop(worker_num, pending)
|
|
1581
|
+
stopping = false
|
|
1181
1582
|
loop do
|
|
1182
|
-
maybe_log("Waiting for
|
|
1183
|
-
message = @
|
|
1184
|
-
|
|
1185
|
-
|
|
1583
|
+
maybe_log("Waiting for work", worker_num: worker_num)
|
|
1584
|
+
kind, message = @dispatcher.dequeue(worker_num, accept_calls: !stopping)
|
|
1585
|
+
case kind
|
|
1586
|
+
when :call then start_worker_fiber(message, pending, worker_num)
|
|
1587
|
+
when :resume then resume_worker_fiber(message, pending)
|
|
1588
|
+
when :closed then stopping = true
|
|
1589
|
+
when :terminate then return
|
|
1590
|
+
end
|
|
1591
|
+
return if stopping && pending.empty?
|
|
1186
1592
|
end
|
|
1187
|
-
|
|
1593
|
+
end
|
|
1594
|
+
|
|
1595
|
+
##
|
|
1596
|
+
# Worker-thread cleanup: aborts any pending fibers (so their callers
|
|
1597
|
+
# observe `CrashedError`) and reports the worker's stop to the main
|
|
1598
|
+
# loop. Always runs in the worker's ensure block — both for normal exit
|
|
1599
|
+
# and for crash exit.
|
|
1600
|
+
#
|
|
1601
|
+
def cleanup_worker(worker_num, pending, crash_exception = nil)
|
|
1188
1602
|
maybe_log("Worker stopping", worker_num: worker_num)
|
|
1603
|
+
if pending && !pending.empty?
|
|
1604
|
+
message =
|
|
1605
|
+
if crash_exception
|
|
1606
|
+
"Worker #{worker_num} crashed: #{crash_exception.message} (#{crash_exception.class})"
|
|
1607
|
+
else
|
|
1608
|
+
"Worker #{worker_num} terminated"
|
|
1609
|
+
end
|
|
1610
|
+
error = CrashedError.new(message)
|
|
1611
|
+
pending.each_key { |fiber_id| @dispatcher.unregister_fiber(fiber_id) }
|
|
1612
|
+
abort_pending_fibers(pending, error)
|
|
1613
|
+
end
|
|
1189
1614
|
begin
|
|
1190
1615
|
@port.send(WorkerStoppedMessage.new(worker_num))
|
|
1191
1616
|
rescue ::Ractor::ClosedError
|
|
@@ -1193,6 +1618,40 @@ class Ractor
|
|
|
1193
1618
|
end
|
|
1194
1619
|
end
|
|
1195
1620
|
|
|
1621
|
+
##
|
|
1622
|
+
# Start a fiber for a new `CallMessage` on this worker. Registers the
|
|
1623
|
+
# fiber with the dispatcher so future fiber-resume messages route here.
|
|
1624
|
+
# If the fiber completes synchronously (no `Fiber.yield`), it is
|
|
1625
|
+
# immediately removed from the local `pending` hash and unregistered.
|
|
1626
|
+
#
|
|
1627
|
+
def start_worker_fiber(message, pending, worker_num)
|
|
1628
|
+
fiber = ::Fiber.new { handle_method(message, worker_num: worker_num) }
|
|
1629
|
+
fiber_id = fiber.object_id
|
|
1630
|
+
pending[fiber_id] = fiber
|
|
1631
|
+
@dispatcher.register_fiber(fiber_id, worker_num)
|
|
1632
|
+
maybe_log("Starting worker fiber", call_message: message, worker_num: worker_num, fiber_id: fiber_id)
|
|
1633
|
+
fiber.resume
|
|
1634
|
+
return if fiber.alive?
|
|
1635
|
+
pending.delete(fiber_id)
|
|
1636
|
+
@dispatcher.unregister_fiber(fiber_id)
|
|
1637
|
+
end
|
|
1638
|
+
|
|
1639
|
+
##
|
|
1640
|
+
# Resume a previously-suspended fiber with a fiber-result message. If
|
|
1641
|
+
# the fiber is no longer alive (e.g. aborted), the message is silently
|
|
1642
|
+
# discarded. On completion the fiber is removed from `pending` and
|
|
1643
|
+
# unregistered from the dispatcher.
|
|
1644
|
+
#
|
|
1645
|
+
def resume_worker_fiber(message, pending)
|
|
1646
|
+
fiber = pending[message.fiber_id]
|
|
1647
|
+
return unless fiber
|
|
1648
|
+
maybe_log("Resuming worker fiber", fiber_id: message.fiber_id)
|
|
1649
|
+
fiber.resume(message)
|
|
1650
|
+
return if fiber.alive?
|
|
1651
|
+
pending.delete(message.fiber_id)
|
|
1652
|
+
@dispatcher.unregister_fiber(message.fiber_id)
|
|
1653
|
+
end
|
|
1654
|
+
|
|
1196
1655
|
##
|
|
1197
1656
|
# This is called to handle a method call request.
|
|
1198
1657
|
# It calls the method on the wrapped object, and then sends back a
|
|
@@ -1233,28 +1692,77 @@ class Ractor
|
|
|
1233
1692
|
# with the block arguments, to run the block in the caller's
|
|
1234
1693
|
# environment
|
|
1235
1694
|
#
|
|
1695
|
+
# The returned proc uses the fiber-suspend path (Fiber.yield) so the
|
|
1696
|
+
# server can continue processing other messages, including re-entrant
|
|
1697
|
+
# calls from inside the block. In threaded mode, fiber resumes are
|
|
1698
|
+
# routed back to the same worker that started the fiber via the
|
|
1699
|
+
# `Dispatcher`'s per-worker queues (Ruby fibers cannot migrate between
|
|
1700
|
+
# threads).
|
|
1701
|
+
#
|
|
1702
|
+
# Hybrid fallback: if the proc is invoked from a different fiber than
|
|
1703
|
+
# the method-handling fiber (e.g. from inside an Enumerator generator
|
|
1704
|
+
# or a spawned fiber), the fiber path would call Fiber.yield on the
|
|
1705
|
+
# wrong fiber. In that case the proc falls back to the blocking path.
|
|
1706
|
+
#
|
|
1236
1707
|
def make_block(message)
|
|
1237
1708
|
return message.block_arg unless message.block_arg == :send_block_message
|
|
1709
|
+
expected_fiber = ::Fiber.current
|
|
1238
1710
|
proc do |*args, **kwargs|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
message
|
|
1245
|
-
reply_port.receive
|
|
1246
|
-
ensure
|
|
1247
|
-
reply_port.close
|
|
1248
|
-
end
|
|
1249
|
-
case reply_message
|
|
1250
|
-
when ExceptionMessage
|
|
1251
|
-
raise reply_message.exception
|
|
1252
|
-
when ReturnMessage
|
|
1253
|
-
reply_message.value
|
|
1711
|
+
args.map! { |arg| arg.equal?(@object) ? @stub : arg }
|
|
1712
|
+
kwargs.transform_values! { |arg| arg.equal?(@object) ? @stub : arg }
|
|
1713
|
+
if ::Fiber.current.equal?(expected_fiber)
|
|
1714
|
+
fiber_yield_block(message, args, kwargs)
|
|
1715
|
+
else
|
|
1716
|
+
blocking_yield_block(message, args, kwargs)
|
|
1254
1717
|
end
|
|
1255
1718
|
end
|
|
1256
1719
|
end
|
|
1257
1720
|
|
|
1721
|
+
##
|
|
1722
|
+
# Yield to a caller-side block via the fiber-suspend path. The current
|
|
1723
|
+
# fiber's id is sent in the FiberYieldMessage so the caller knows which
|
|
1724
|
+
# FiberReturnMessage/FiberExceptionMessage to send back. The fiber then
|
|
1725
|
+
# suspends; main_loop will resume it with the reply message when one
|
|
1726
|
+
# arrives on @port.
|
|
1727
|
+
#
|
|
1728
|
+
def fiber_yield_block(message, args, kwargs)
|
|
1729
|
+
fiber_id = ::Fiber.current.object_id
|
|
1730
|
+
yield_message = FiberYieldMessage.new(args: args, kwargs: kwargs, fiber_id: fiber_id)
|
|
1731
|
+
maybe_log("Yielding to caller-side block", call_message: message, fiber_id: fiber_id)
|
|
1732
|
+
message.reply_port.send(yield_message, move: message.settings.block_arguments == :move)
|
|
1733
|
+
reply = ::Fiber.yield
|
|
1734
|
+
maybe_log("Resumed after block reply", call_message: message, fiber_id: fiber_id)
|
|
1735
|
+
case reply
|
|
1736
|
+
when FiberExceptionMessage
|
|
1737
|
+
raise reply.exception
|
|
1738
|
+
when FiberReturnMessage
|
|
1739
|
+
reply.value
|
|
1740
|
+
end
|
|
1741
|
+
end
|
|
1742
|
+
|
|
1743
|
+
##
|
|
1744
|
+
# Yield to a caller-side block via the blocking-fallback path: allocate
|
|
1745
|
+
# a temporary reply_port and block waiting for a response on it. Used
|
|
1746
|
+
# when the fiber-suspend path is not available (nested-fiber and
|
|
1747
|
+
# spawned-thread invocations).
|
|
1748
|
+
#
|
|
1749
|
+
def blocking_yield_block(message, args, kwargs)
|
|
1750
|
+
reply_port = ::Ractor::Port.new
|
|
1751
|
+
reply_message = begin
|
|
1752
|
+
yield_message = BlockingYieldMessage.new(args: args, kwargs: kwargs, reply_port: reply_port)
|
|
1753
|
+
message.reply_port.send(yield_message, move: message.settings.block_arguments == :move)
|
|
1754
|
+
reply_port.receive
|
|
1755
|
+
ensure
|
|
1756
|
+
reply_port.close
|
|
1757
|
+
end
|
|
1758
|
+
case reply_message
|
|
1759
|
+
when ExceptionMessage
|
|
1760
|
+
raise reply_message.exception
|
|
1761
|
+
when ReturnMessage
|
|
1762
|
+
reply_message.value
|
|
1763
|
+
end
|
|
1764
|
+
end
|
|
1765
|
+
|
|
1258
1766
|
##
|
|
1259
1767
|
# This is called from the main Ractor thread to report to a caller that
|
|
1260
1768
|
# the wrapper cannot handle a requested method call, likely because the
|
|
@@ -1282,12 +1790,14 @@ class Ractor
|
|
|
1282
1790
|
##
|
|
1283
1791
|
# Print out a log message
|
|
1284
1792
|
#
|
|
1285
|
-
def maybe_log(str, call_message: nil, worker_num: nil,
|
|
1793
|
+
def maybe_log(str, call_message: nil, worker_num: nil, fiber_id: nil,
|
|
1794
|
+
transaction: nil, method_name: nil)
|
|
1286
1795
|
return unless @enable_logging
|
|
1287
1796
|
transaction ||= call_message&.transaction
|
|
1288
1797
|
method_name ||= call_message&.method_name
|
|
1289
1798
|
metadata = [::Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%L"), "Ractor::Wrapper:#{@name}"]
|
|
1290
1799
|
metadata << "Worker:#{worker_num}" if worker_num
|
|
1800
|
+
metadata << "Fiber:#{fiber_id}" if fiber_id
|
|
1291
1801
|
metadata << "Transaction:#{transaction}" if transaction
|
|
1292
1802
|
metadata << "Method:#{method_name}" if method_name
|
|
1293
1803
|
metadata = metadata.join(" ")
|