concurrent-ruby-edge 0.4.1 → 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/CHANGELOG.md +26 -0
- data/README.md +36 -4
- data/lib-edge/concurrent-edge.rb +4 -0
- data/lib-edge/concurrent/actor/reference.rb +3 -0
- data/lib-edge/concurrent/edge/cancellation.rb +78 -112
- data/lib-edge/concurrent/edge/channel.rb +450 -0
- data/lib-edge/concurrent/edge/erlang_actor.rb +1545 -0
- data/lib-edge/concurrent/edge/processing_actor.rb +83 -64
- data/lib-edge/concurrent/edge/promises.rb +80 -110
- data/lib-edge/concurrent/edge/throttle.rb +167 -141
- data/lib-edge/concurrent/edge/version.rb +3 -0
- metadata +8 -5
@@ -0,0 +1,1545 @@
|
|
1
|
+
if Concurrent.ruby_version :<, 2, 1, 0
|
2
|
+
raise 'ErlangActor requires at least ruby version 2.1'
|
3
|
+
end
|
4
|
+
|
5
|
+
module Concurrent
|
6
|
+
|
7
|
+
# This module provides actor abstraction that has same behaviour as Erlang actor.
|
8
|
+
#
|
9
|
+
# {include:file:docs-source/erlang_actor.out.md}
|
10
|
+
# @!macro warn.edge
|
11
|
+
module ErlangActor
|
12
|
+
|
13
|
+
# TODO (pitr-ch 04-Feb-2019): mode documentation.
|
14
|
+
# TODO (pitr-ch 21-Jan-2019): actor on promises should not call blocking calls like mailbox.pop or tell
|
15
|
+
# it's fine for a actor on thread and event based though
|
16
|
+
# TODO (pitr-ch 17-Jan-2019): blocking actor should react to signals?
|
17
|
+
# e.g. override sleep to wait for signal with a given timeout?
|
18
|
+
# what about other blocking stuff
|
19
|
+
# def sleep(time)
|
20
|
+
# raise NotImplementedError
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# def sleep(time)
|
24
|
+
# raise NotImplementedError
|
25
|
+
# finish = Concurrent.monotonic_time + time
|
26
|
+
# while true
|
27
|
+
# now = Concurrent.monotonic_time
|
28
|
+
# if now < finish
|
29
|
+
# message = @Mailbox.pop_matching(AbstractSignal, finish - now)
|
30
|
+
# else
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
# TODO (pitr-ch 28-Jan-2019): improve matching support, take inspiration and/or port Algebrick ideas, push ANY and similar further up the namespace
|
35
|
+
|
36
|
+
# The public reference of the actor which can be stored and passed around.
|
37
|
+
# Nothing else of the actor should be exposed.
|
38
|
+
# {Functions.spawn_actor} and {Environment#spawn} return the pid.
|
39
|
+
class Pid < Synchronization::Object
|
40
|
+
# TODO (pitr-ch 06-Feb-2019): when actor terminates, release it from memory keeping just pid
|
41
|
+
|
42
|
+
# The actor is asynchronously told a message.
|
43
|
+
# The method returns immediately unless
|
44
|
+
# the actor has bounded mailbox and there is no more space for the message.
|
45
|
+
# Then the method blocks current thread until there is space available.
|
46
|
+
# This is useful for backpressure.
|
47
|
+
#
|
48
|
+
# @param [Object] message
|
49
|
+
# @param [Numeric] timeout the maximum time in second to wait
|
50
|
+
# @return [self, true, false]
|
51
|
+
# self if timeout was nil, false on timing out and true if told in time.
|
52
|
+
def tell(message, timeout = nil)
|
53
|
+
@Actor.tell message, timeout
|
54
|
+
end
|
55
|
+
|
56
|
+
# Same as {#tell} but represented as a {Promises::Future}.
|
57
|
+
# @param [Object] message
|
58
|
+
# @return [Promises::Future(self)]
|
59
|
+
def tell_op(message)
|
60
|
+
@Actor.tell_op(message)
|
61
|
+
end
|
62
|
+
|
63
|
+
# The actor is asked the message and blocks until a reply is available,
|
64
|
+
# which is returned by the method.
|
65
|
+
# If the reply is a rejection then the methods raises it.
|
66
|
+
#
|
67
|
+
# If the actor does not call {Environment#reply} or {Environment#reply_resolution}
|
68
|
+
# the method will raise NoReply error.
|
69
|
+
# If the actor is terminated it will raise NoActor.
|
70
|
+
# Therefore the ask is never left unanswered and blocking.
|
71
|
+
#
|
72
|
+
# @param [Object] message
|
73
|
+
# @param [Numeric] timeout the maximum time in second to wait
|
74
|
+
# @param [Object] timeout_value the value returned on timeout
|
75
|
+
# @return [Object, timeout_value] reply to the message
|
76
|
+
# @raise [NoReply, NoActor]
|
77
|
+
def ask(message, timeout = nil, timeout_value = nil)
|
78
|
+
@Actor.ask message, timeout, timeout_value
|
79
|
+
end
|
80
|
+
|
81
|
+
# Same as {#tell} but represented as a {Promises::Future}.
|
82
|
+
# @param [Object] message
|
83
|
+
# @param [Promises::ResolvableFuture] probe
|
84
|
+
# a resolvable future which is resolved with the reply.
|
85
|
+
# @return [Promises::Future(Object)] reply to the message
|
86
|
+
def ask_op(message, probe = Promises.resolvable_future)
|
87
|
+
@Actor.ask_op message, probe
|
88
|
+
end
|
89
|
+
|
90
|
+
# @!macro erlang_actor.terminated
|
91
|
+
# @return [Promises::Future] a future which is resolved with
|
92
|
+
# the final result of the actor that is either the reason for
|
93
|
+
# termination or a value if terminated normally.
|
94
|
+
def terminated
|
95
|
+
@Actor.terminated
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [#to_s, nil] optional name of the actor
|
99
|
+
def name
|
100
|
+
@Name
|
101
|
+
end
|
102
|
+
|
103
|
+
# @return [String] string representation
|
104
|
+
def to_s
|
105
|
+
original = super
|
106
|
+
state = case terminated.state
|
107
|
+
when :pending
|
108
|
+
'running'
|
109
|
+
when :fulfilled
|
110
|
+
"terminated normally with #{terminated.value}"
|
111
|
+
when :rejected
|
112
|
+
"terminated because of #{terminated.reason}"
|
113
|
+
else
|
114
|
+
raise
|
115
|
+
end
|
116
|
+
[original[0..-2], *@Name, state].join(' ') << '>'
|
117
|
+
end
|
118
|
+
|
119
|
+
alias_method :inspect, :to_s
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
safe_initialization!
|
124
|
+
|
125
|
+
def initialize(actor, name)
|
126
|
+
@Actor = actor
|
127
|
+
@Name = name
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# An object representing instance of a monitor, created with {Environment#monitor}.
|
132
|
+
class Reference
|
133
|
+
end
|
134
|
+
|
135
|
+
# A class providing environment and methods for actor bodies to run in.
|
136
|
+
class Environment < Synchronization::Object
|
137
|
+
safe_initialization!
|
138
|
+
|
139
|
+
# @!macro erlang_actor.terminated
|
140
|
+
def terminated
|
141
|
+
@Actor.terminated
|
142
|
+
end
|
143
|
+
|
144
|
+
# @return [Pid] the pid of this actor
|
145
|
+
def pid
|
146
|
+
@Actor.pid
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [#to_s] the name od the actor if provided to spawn method
|
150
|
+
def name
|
151
|
+
pid.name
|
152
|
+
end
|
153
|
+
|
154
|
+
# @return [true, false] does this actor trap exit messages?
|
155
|
+
# @see http://www1.erlang.org/doc/man/erlang.html#process_flag-2
|
156
|
+
def traps?
|
157
|
+
@Actor.traps?
|
158
|
+
end
|
159
|
+
|
160
|
+
# When trap is set to true,
|
161
|
+
# exit signals arriving to a actor are converted to {Terminated} messages,
|
162
|
+
# which can be received as ordinary messages.
|
163
|
+
# If trap is set to false,
|
164
|
+
# the actor exits
|
165
|
+
# if it receives an exit signal other than normal
|
166
|
+
# and the exit signal is propagated to its linked actors.
|
167
|
+
# Application actors should normally not trap exits.
|
168
|
+
#
|
169
|
+
# @param [true, false] value
|
170
|
+
# @return [true, false] the old value of the flag
|
171
|
+
# @see http://www1.erlang.org/doc/man/erlang.html#process_flag-2
|
172
|
+
def trap(value = true)
|
173
|
+
@Actor.trap(value)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Helper for constructing a {#receive} rules
|
177
|
+
# @see #receive
|
178
|
+
# @example
|
179
|
+
# receive on(Numeric) { |v| v.succ },
|
180
|
+
# on(ANY) { terminate :bad_message }
|
181
|
+
def on(matcher, value = nil, &block)
|
182
|
+
@Actor.on matcher, value, &block
|
183
|
+
end
|
184
|
+
|
185
|
+
# Receive a message.
|
186
|
+
#
|
187
|
+
# @param [::Array(), ::Array(#===), ::Array<::Array(#===, Proc)>] rules
|
188
|
+
# * No rule - `receive`, `receive {|m| m.to_s}`
|
189
|
+
# * or single rule which can be combined with the supplied block -
|
190
|
+
# `receive(Numeric)`, `receive(Numeric) {|v| v.succ}`
|
191
|
+
# * or array of matcher-proc pairs -
|
192
|
+
# `receive on(Numeric) { |v| v*2 }, on(Symbol) { |c| do_command c }`
|
193
|
+
# @param [Numeric] timeout
|
194
|
+
# how long it should wait for the message
|
195
|
+
# @param [Object] timeout_value
|
196
|
+
# if rule `on(TIMEOUT) { do_something }` is not specified
|
197
|
+
# then timeout_value is returned.
|
198
|
+
# @return [Object, nothing]
|
199
|
+
# depends on type of the actor.
|
200
|
+
# On thread it blocks until message is available
|
201
|
+
# then it returns the message (or a result of a called block).
|
202
|
+
# On pool it stops executing and continues with a given block
|
203
|
+
# when message becomes available.
|
204
|
+
# @param [Hash] options
|
205
|
+
# other options specific by type of the actor
|
206
|
+
# @option options [true, false] :keep
|
207
|
+
# Keep the rules and repeatedly call the associated blocks,
|
208
|
+
# until receive is called again.
|
209
|
+
# @yield [message] block
|
210
|
+
# to process the message
|
211
|
+
# if single matcher is supplied
|
212
|
+
# @yieldparam [Object] message the received message
|
213
|
+
# @see ErlangActor Receiving chapter in the ErlangActor examples
|
214
|
+
def receive(*rules, timeout: nil, timeout_value: nil, **options, &block)
|
215
|
+
@Actor.receive(*rules, timeout: timeout, timeout_value: timeout_value, **options, &block)
|
216
|
+
end
|
217
|
+
|
218
|
+
# Creates a link between the calling actor and another actor,
|
219
|
+
# if there is not such a link already.
|
220
|
+
# If a actor attempts to create a link to itself, nothing is done. Returns true.
|
221
|
+
#
|
222
|
+
# If pid does not exist,
|
223
|
+
# the behavior of the method depends on
|
224
|
+
# if the calling actor is trapping exits or not (see {#trap}):
|
225
|
+
# * If the calling actor is not trapping exits link raises with {NoActor}.
|
226
|
+
# * Otherwise, if the calling actor is trapping exits, link returns true,
|
227
|
+
# but an exit signal with reason noproc is sent to the calling actor.
|
228
|
+
#
|
229
|
+
# @return [true]
|
230
|
+
# @raise [NoActor]
|
231
|
+
# @see http://www1.erlang.org/doc/man/erlang.html#link-1
|
232
|
+
def link(pid)
|
233
|
+
@Actor.link(pid)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Removes the link, if there is one,
|
237
|
+
# between the calling actor and the actor referred to by pid.
|
238
|
+
#
|
239
|
+
# Returns true and does not fail, even if there is no link to Id, or if Id does not exist.
|
240
|
+
#
|
241
|
+
# Once unlink(pid) has returned
|
242
|
+
# it is guaranteed
|
243
|
+
# that the link between the caller and the actor referred to by pid
|
244
|
+
# has no effect on the caller in the future (unless the link is setup again).
|
245
|
+
# If caller is trapping exits,
|
246
|
+
# an {Terminated} message due to the link might have been placed
|
247
|
+
# in the caller's message queue prior to the call, though.
|
248
|
+
#
|
249
|
+
# Note, the {Terminated} message can be the result of the link,
|
250
|
+
# but can also be the result of calling #terminate method externally.
|
251
|
+
# Therefore, it may be appropriate to cleanup the message queue
|
252
|
+
# when trapping exits after the call to unlink, as follow:
|
253
|
+
# ```ruby
|
254
|
+
# receive on(And[Terminated, -> e { e.pid == pid }], true), timeout: 0
|
255
|
+
# ```
|
256
|
+
#
|
257
|
+
# @return [true]
|
258
|
+
def unlink(pid)
|
259
|
+
@Actor.unlink(pid)
|
260
|
+
end
|
261
|
+
|
262
|
+
# @!visibility private
|
263
|
+
# @return [true, false]
|
264
|
+
def linked?(pid)
|
265
|
+
@Actor.linked? pid
|
266
|
+
end
|
267
|
+
|
268
|
+
# The calling actor starts monitoring actor with given pid.
|
269
|
+
#
|
270
|
+
# A {DownSignal} message will be sent to the monitoring actor
|
271
|
+
# if the actor with given pid dies,
|
272
|
+
# or if the actor with given pid does not exist.
|
273
|
+
#
|
274
|
+
# The monitoring is turned off either
|
275
|
+
# when the {DownSignal} message is sent, or when {#demonitor} is called.
|
276
|
+
#
|
277
|
+
# Making several calls to monitor for the same pid is not an error;
|
278
|
+
# it results in as many, completely independent, monitorings.
|
279
|
+
#
|
280
|
+
# @return [Reference]
|
281
|
+
def monitor(pid)
|
282
|
+
@Actor.monitor(pid)
|
283
|
+
end
|
284
|
+
|
285
|
+
# If MonitorRef is a reference which the calling actor obtained by calling {#monitor},
|
286
|
+
# this monitoring is turned off.
|
287
|
+
# If the monitoring is already turned off, nothing happens.
|
288
|
+
#
|
289
|
+
# Once demonitor has returned it is guaranteed that no {DownSignal} message
|
290
|
+
# due to the monitor will be placed in the caller's message queue in the future.
|
291
|
+
# A {DownSignal} message might have been placed in the caller's message queue prior to the call, though.
|
292
|
+
# Therefore, in most cases, it is advisable to remove such a 'DOWN' message from the message queue
|
293
|
+
# after monitoring has been stopped.
|
294
|
+
# `demonitor(reference, :flush)` can be used if this cleanup is wanted.
|
295
|
+
#
|
296
|
+
# The behavior of this method can be viewed as two combined operations:
|
297
|
+
# asynchronously send a "demonitor signal" to the monitored actor and
|
298
|
+
# ignore any future results of the monitor.
|
299
|
+
#
|
300
|
+
# Failure: It is an error if reference refers to a monitoring started by another actor.
|
301
|
+
# In that case it may raise an ArgumentError or go unnoticed.
|
302
|
+
#
|
303
|
+
# Options:
|
304
|
+
# * `:flush` - Remove (one) {DownSignal} message,
|
305
|
+
# if there is one, from the caller's message queue after monitoring has been stopped.
|
306
|
+
# Calling `demonitor(pid, :flush)` is equivalent to the following, but more efficient:
|
307
|
+
# ```ruby
|
308
|
+
# demonitor(pid)
|
309
|
+
# receive on(And[DownSignal, -> d { d.reference == reference}], true), timeout: 0, timeout_value: true
|
310
|
+
# ```
|
311
|
+
#
|
312
|
+
# * `info`
|
313
|
+
# The returned value is one of the following:
|
314
|
+
#
|
315
|
+
# - `true` - The monitor was found and removed.
|
316
|
+
# In this case no {DownSignal} message due to this monitor have been
|
317
|
+
# nor will be placed in the message queue of the caller.
|
318
|
+
# - `false` - The monitor was not found and could not be removed.
|
319
|
+
# This probably because someone already has placed a {DownSignal} message
|
320
|
+
# corresponding to this monitor in the caller's message queue.
|
321
|
+
#
|
322
|
+
# If the info option is combined with the flush option,
|
323
|
+
# `false` will be returned if a flush was needed; otherwise, `true`.
|
324
|
+
#
|
325
|
+
# @param [Reference] reference
|
326
|
+
# @param [:flush, :info] options
|
327
|
+
# @return [true, false]
|
328
|
+
def demonitor(reference, *options)
|
329
|
+
@Actor.demonitor(reference, *options)
|
330
|
+
end
|
331
|
+
|
332
|
+
# @!visibility private
|
333
|
+
def monitoring?(reference)
|
334
|
+
@Actor.monitoring? reference
|
335
|
+
end
|
336
|
+
|
337
|
+
# Creates an actor.
|
338
|
+
#
|
339
|
+
# @param [Object] args arguments for the actor body
|
340
|
+
# @param [:on_thread, :on_pool] type
|
341
|
+
# of the actor to be created.
|
342
|
+
# @param [Channel] channel
|
343
|
+
# The mailbox of the actor, by default it has unlimited capacity.
|
344
|
+
# Crating the actor with a bounded queue is useful to create backpressure.
|
345
|
+
# The channel can be shared with other abstractions
|
346
|
+
# but actor has to be the only consumer
|
347
|
+
# otherwise internal signals could be lost.
|
348
|
+
# @param [Environment, Module] environment
|
349
|
+
# A class which is used to run the body of the actor in.
|
350
|
+
# It can either be a child of {Environment} or a module.
|
351
|
+
# Module is extended to a new instance of environment,
|
352
|
+
# therefore if there is many actors with this module
|
353
|
+
# it is better to create a class and use it instead.
|
354
|
+
# @param [#to_s] name of the actor.
|
355
|
+
# Available by {Pid#name} or {Environment#name} and part of {Pid#to_s}.
|
356
|
+
# @param [true, false] link
|
357
|
+
# the created actor is atomically created and linked with the calling actor
|
358
|
+
# @param [true, false] monitor
|
359
|
+
# the created actor is atomically created and monitored by the calling actor
|
360
|
+
# @param [ExecutorService] executor
|
361
|
+
# The executor service to use to execute the actor on.
|
362
|
+
# Applies only to :on_pool actor type.
|
363
|
+
# @yield [*args] the body of the actor.
|
364
|
+
# When actor is spawned this block is evaluated
|
365
|
+
# until it terminates.
|
366
|
+
# The on-thread actor requires a block.
|
367
|
+
# The on-poll actor has a default `-> { start }`,
|
368
|
+
# therefore if not block is given it executes a #start method
|
369
|
+
# which needs to be provided with environment.
|
370
|
+
# @return [Pid, ::Array(Pid, Reference)] a pid or a pid-reference pair when monitor is true
|
371
|
+
# @see http://www1.erlang.org/doc/man/erlang.html#spawn-1
|
372
|
+
# @see http://www1.erlang.org/doc/man/erlang.html#spawn_link-1
|
373
|
+
# @see http://www1.erlang.org/doc/man/erlang.html#spawn_monitor-1
|
374
|
+
def spawn(*args,
|
375
|
+
type: @Actor.class,
|
376
|
+
channel: Promises::Channel.new,
|
377
|
+
environment: Environment,
|
378
|
+
name: nil,
|
379
|
+
executor: default_executor,
|
380
|
+
link: false,
|
381
|
+
monitor: false,
|
382
|
+
&body)
|
383
|
+
|
384
|
+
@Actor.spawn(*args,
|
385
|
+
type: type,
|
386
|
+
channel: channel,
|
387
|
+
environment: environment,
|
388
|
+
name: name,
|
389
|
+
executor: executor,
|
390
|
+
link: link,
|
391
|
+
monitor: monitor,
|
392
|
+
&body)
|
393
|
+
end
|
394
|
+
|
395
|
+
# Shortcut for fulfilling the reply, same as `reply_resolution true, value, nil`.
|
396
|
+
# @example
|
397
|
+
# actor = Concurrent::ErlangActor.spawn(:on_thread) { reply receive * 2 }
|
398
|
+
# actor.ask 2 #=> 4
|
399
|
+
# @param [Object] value
|
400
|
+
# @return [true, false] did the sender ask, and was it resolved
|
401
|
+
def reply(value)
|
402
|
+
# TODO (pitr-ch 08-Feb-2019): consider adding reply? which returns true,false if success, reply method will always return value
|
403
|
+
reply_resolution true, value, nil
|
404
|
+
end
|
405
|
+
|
406
|
+
# Reply to the sender of the message currently being processed
|
407
|
+
# if the actor was asked instead of told.
|
408
|
+
# The reply is stored in a {Promises::ResolvableFuture}
|
409
|
+
# so the arguments are same as for {Promises::ResolvableFuture#resolve} method.
|
410
|
+
#
|
411
|
+
# The reply may timeout, then this will fail with false.
|
412
|
+
#
|
413
|
+
# @param [true, false] fulfilled
|
414
|
+
# @param [Object] value
|
415
|
+
# @param [Object] reason
|
416
|
+
#
|
417
|
+
# @example
|
418
|
+
# actor = Concurrent::ErlangActor.spawn(:on_thread) { reply_resolution true, receive * 2, nil }
|
419
|
+
# actor.ask 2 #=> 4
|
420
|
+
#
|
421
|
+
# @return [true, false] did the sender ask, and was it resolved before it timed out?
|
422
|
+
def reply_resolution(fulfilled = true, value = nil, reason = nil)
|
423
|
+
@Actor.reply_resolution(fulfilled, value, reason)
|
424
|
+
end
|
425
|
+
|
426
|
+
# If pid **is not** provided stops the execution of the calling actor
|
427
|
+
# with the exit reason.
|
428
|
+
#
|
429
|
+
# If pid **is** provided,
|
430
|
+
# it sends an exit signal with exit reason to the actor identified by pid.
|
431
|
+
#
|
432
|
+
# The following behavior apply
|
433
|
+
# if `reason` is any object except `:normal` or `:kill`.
|
434
|
+
# If pid is not trapping exits,
|
435
|
+
# pid itself will exit with exit reason.
|
436
|
+
# If pid is trapping exits,
|
437
|
+
# the exit signal is transformed into a message {Terminated}
|
438
|
+
# and delivered to the message queue of pid.
|
439
|
+
#
|
440
|
+
# If reason is the Symbol `:normal`, pid will not exit.
|
441
|
+
# If it is trapping exits, the exit signal is transformed into a message {Terminated}
|
442
|
+
# and delivered to its message queue.
|
443
|
+
#
|
444
|
+
# If reason is the Symbol `:kill`, that is if `exit(pid, :kill)` is called,
|
445
|
+
# an untrappable exit signal is sent to pid which will unconditionally exit
|
446
|
+
# with exit reason `:killed`.
|
447
|
+
#
|
448
|
+
# Since evaluating this function causes the process to terminate, it has no return value.
|
449
|
+
#
|
450
|
+
# @param [Pid] pid
|
451
|
+
# @param [Object, :normal, :kill] reason
|
452
|
+
# @param [Object] value
|
453
|
+
# @return [nothing]
|
454
|
+
# @see http://www1.erlang.org/doc/man/erlang.html#error-1
|
455
|
+
# @see http://www1.erlang.org/doc/man/erlang.html#error-2
|
456
|
+
def terminate(pid = nil, reason, value: nil)
|
457
|
+
@Actor.terminate pid, reason, value: value
|
458
|
+
end
|
459
|
+
|
460
|
+
# @return [ExecutorService] a default executor which is picked by spawn call
|
461
|
+
def default_executor
|
462
|
+
@DefaultExecutor
|
463
|
+
end
|
464
|
+
|
465
|
+
private
|
466
|
+
|
467
|
+
def initialize(actor, executor)
|
468
|
+
super()
|
469
|
+
@Actor = actor
|
470
|
+
@DefaultExecutor = executor
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
# A module containing entry functions to actors like spawn_actor, terminate_actor.
|
475
|
+
# It can be included in environments working with actors.
|
476
|
+
# @example
|
477
|
+
# include Concurrent::ErlangActors::Functions
|
478
|
+
# actor = spawn_actor :on_pool do
|
479
|
+
# receive { |data| process data }
|
480
|
+
# end
|
481
|
+
# @see FunctionShortcuts
|
482
|
+
module Functions
|
483
|
+
# Creates an actor. Same as {Environment#spawn} but lacks link and monitor options.
|
484
|
+
# @param [Object] args
|
485
|
+
# @param [:on_thread, :on_pool] type
|
486
|
+
# @param [Channel] channel
|
487
|
+
# @param [Environment, Module] environment
|
488
|
+
# @param [#to_s] name of the actor
|
489
|
+
# @param [ExecutorService] executor of the actor
|
490
|
+
# @return [Pid]
|
491
|
+
# @see Environment#spawn
|
492
|
+
def spawn_actor(*args,
|
493
|
+
type:,
|
494
|
+
channel: Promises::Channel.new,
|
495
|
+
environment: Environment,
|
496
|
+
name: nil,
|
497
|
+
executor: default_actor_executor,
|
498
|
+
&body)
|
499
|
+
|
500
|
+
actor = ErlangActor.create type, channel, environment, name, executor
|
501
|
+
actor.run(*args, &body)
|
502
|
+
return actor.pid
|
503
|
+
end
|
504
|
+
|
505
|
+
# Same as {Environment#terminate}, but it requires pid.
|
506
|
+
# @param [Pid] pid
|
507
|
+
# @param [Object, :normal, :kill] reason
|
508
|
+
# @return [true]
|
509
|
+
def terminate_actor(pid, reason)
|
510
|
+
if reason == :kill
|
511
|
+
pid.tell Kill.new(nil)
|
512
|
+
else
|
513
|
+
pid.tell Terminate.new(nil, reason, false)
|
514
|
+
end
|
515
|
+
true
|
516
|
+
end
|
517
|
+
|
518
|
+
# @return [ExecutorService] the default executor service for actors
|
519
|
+
def default_actor_executor
|
520
|
+
default_executor
|
521
|
+
end
|
522
|
+
|
523
|
+
# @return [ExecutorService] the default executor service,
|
524
|
+
# may be shared by other abstractions
|
525
|
+
def default_executor
|
526
|
+
:io
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
# Constrains shortcuts for methods in {Functions}.
|
531
|
+
module FunctionShortcuts
|
532
|
+
# Optionally included shortcut method for {Functions#spawn_actor}
|
533
|
+
# @return [Pid]
|
534
|
+
def spawn(*args, &body)
|
535
|
+
spawn_actor(*args, &body)
|
536
|
+
end
|
537
|
+
|
538
|
+
# Optionally included shortcut method for {Functions#terminate_actor}
|
539
|
+
# @return [true]
|
540
|
+
def terminate(pid, reason)
|
541
|
+
terminate_actor(pid, reason)
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
extend Functions
|
546
|
+
extend FunctionShortcuts
|
547
|
+
extend Concern::Logging
|
548
|
+
|
549
|
+
class Token
|
550
|
+
def initialize(name)
|
551
|
+
@name = name
|
552
|
+
end
|
553
|
+
|
554
|
+
def to_s
|
555
|
+
@name
|
556
|
+
end
|
557
|
+
|
558
|
+
alias_method :inspect, :to_s
|
559
|
+
end
|
560
|
+
|
561
|
+
private_constant :Token
|
562
|
+
|
563
|
+
JUMP = Token.new 'JUMP'
|
564
|
+
TERMINATE = Token.new 'TERMINATE'
|
565
|
+
RECEIVE = Token.new 'RECEIVE'
|
566
|
+
NOTHING = Token.new 'NOTHING'
|
567
|
+
|
568
|
+
private_constant :JUMP
|
569
|
+
private_constant :TERMINATE
|
570
|
+
private_constant :RECEIVE
|
571
|
+
private_constant :NOTHING
|
572
|
+
|
573
|
+
# These constants are useful
|
574
|
+
# where the body of an actor is defined.
|
575
|
+
# For convenience they are provided in this module for including.
|
576
|
+
# @example
|
577
|
+
# include Concurrent::ErlangActor::EnvironmentConstants
|
578
|
+
# actor = Concurrent::ErlangActor.spawn(:on_thread) do
|
579
|
+
# receive on(Numeric) { |v| v.succ },
|
580
|
+
# on(ANY) { terminate :bad_message },
|
581
|
+
# on(TIMEOUT) { terminate :no_message },
|
582
|
+
# timeout: 1
|
583
|
+
# end
|
584
|
+
module EnvironmentConstants
|
585
|
+
# Unique identifier of a timeout, singleton.
|
586
|
+
TIMEOUT = Token.new 'TIMEOUT'
|
587
|
+
# A singleton which matches anything using #=== method
|
588
|
+
ANY = Promises::Channel::ANY
|
589
|
+
|
590
|
+
class AbstractLogicOperationMatcher
|
591
|
+
def self.[](*matchers)
|
592
|
+
new(*matchers)
|
593
|
+
end
|
594
|
+
|
595
|
+
def initialize(*matchers)
|
596
|
+
@matchers = matchers
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
# Combines matchers into one which matches if all match.
|
601
|
+
# @example
|
602
|
+
# And[Numeric, -> v { v >= 0 }] === 1 # => true
|
603
|
+
# And[Numeric, -> v { v >= 0 }] === -1 # => false
|
604
|
+
class And < AbstractLogicOperationMatcher
|
605
|
+
# @return [true, false]
|
606
|
+
def ===(v)
|
607
|
+
@matchers.all? { |m| m === v }
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
# Combines matchers into one which matches if any matches.
|
612
|
+
# @example
|
613
|
+
# Or[Symbol, String] === :v # => true
|
614
|
+
# Or[Symbol, String] === 'v' # => true
|
615
|
+
# Or[Symbol, String] === 1 # => false
|
616
|
+
class Or < AbstractLogicOperationMatcher
|
617
|
+
# @return [true, false]
|
618
|
+
def ===(v)
|
619
|
+
@matchers.any? { |m| m === v }
|
620
|
+
end
|
621
|
+
end
|
622
|
+
end
|
623
|
+
|
624
|
+
include EnvironmentConstants
|
625
|
+
|
626
|
+
class Run
|
627
|
+
attr_reader :future
|
628
|
+
|
629
|
+
def self.[](future)
|
630
|
+
new future
|
631
|
+
end
|
632
|
+
|
633
|
+
def initialize(future)
|
634
|
+
@future = future
|
635
|
+
end
|
636
|
+
|
637
|
+
TEST = -> v { v.future if v.is_a?(Run) }
|
638
|
+
end
|
639
|
+
private_constant :Run
|
640
|
+
|
641
|
+
class AbstractActor < Synchronization::Object
|
642
|
+
|
643
|
+
include EnvironmentConstants
|
644
|
+
include Concern::Logging
|
645
|
+
safe_initialization!
|
646
|
+
|
647
|
+
# @param [Promises::Channel] mailbox
|
648
|
+
def initialize(mailbox, environment, name, executor)
|
649
|
+
super()
|
650
|
+
@Mailbox = mailbox
|
651
|
+
@Pid = Pid.new self, name
|
652
|
+
@Linked = ::Set.new
|
653
|
+
@Monitors = {}
|
654
|
+
@Monitoring = {}
|
655
|
+
@MonitoringLateDelivery = {}
|
656
|
+
@Terminated = Promises.resolvable_future
|
657
|
+
@trap = false
|
658
|
+
@reply = nil
|
659
|
+
|
660
|
+
@Environment = if environment.is_a?(Class) && environment <= Environment
|
661
|
+
environment.new self, executor
|
662
|
+
elsif environment.is_a? Module
|
663
|
+
e = Environment.new self, executor
|
664
|
+
e.extend environment
|
665
|
+
e
|
666
|
+
else
|
667
|
+
raise ArgumentError,
|
668
|
+
"environment has to be a class inheriting from Environment or a module"
|
669
|
+
end
|
670
|
+
end
|
671
|
+
|
672
|
+
def tell_op(message)
|
673
|
+
log Logger::DEBUG, @Pid, told: message
|
674
|
+
if (mailbox = @Mailbox)
|
675
|
+
mailbox.push_op(message).then { @Pid }
|
676
|
+
else
|
677
|
+
Promises.fulfilled_future @Pid
|
678
|
+
end
|
679
|
+
end
|
680
|
+
|
681
|
+
def tell(message, timeout = nil)
|
682
|
+
log Logger::DEBUG, @Pid, told: message
|
683
|
+
if (mailbox = @Mailbox)
|
684
|
+
timed_out = mailbox.push message, timeout
|
685
|
+
timeout ? timed_out : @Pid
|
686
|
+
else
|
687
|
+
timeout ? false : @Pid
|
688
|
+
end
|
689
|
+
end
|
690
|
+
|
691
|
+
def ask(message, timeout, timeout_value)
|
692
|
+
log Logger::DEBUG, @Pid, asked: message
|
693
|
+
if @Terminated.resolved?
|
694
|
+
raise NoActor.new(@Pid)
|
695
|
+
else
|
696
|
+
probe = Promises.resolvable_future
|
697
|
+
question = Ask.new(message, probe)
|
698
|
+
if timeout
|
699
|
+
start = Concurrent.monotonic_time
|
700
|
+
in_time = tell question, timeout
|
701
|
+
# recheck it could have in the meantime terminated and drained mailbox
|
702
|
+
raise NoActor.new(@Pid) if @Terminated.resolved?
|
703
|
+
# has to be after resolved check, to catch case where it would return timeout_value
|
704
|
+
# when it was actually terminated
|
705
|
+
to_wait = if in_time
|
706
|
+
time = timeout - (Concurrent.monotonic_time - start)
|
707
|
+
time >= 0 ? time : 0
|
708
|
+
else
|
709
|
+
0
|
710
|
+
end
|
711
|
+
# TODO (pitr-ch 06-Feb-2019): allow negative timeout everywhere, interpret as 0
|
712
|
+
probe.value! to_wait, timeout_value, [true, nil, nil]
|
713
|
+
else
|
714
|
+
raise NoActor.new(@Pid) if @Terminated.resolved?
|
715
|
+
tell question
|
716
|
+
probe.reject NoActor.new(@Pid), false if @Terminated.resolved?
|
717
|
+
probe.value!
|
718
|
+
end
|
719
|
+
end
|
720
|
+
end
|
721
|
+
|
722
|
+
def ask_op(message, probe)
|
723
|
+
log Logger::DEBUG, @Pid, asked: message
|
724
|
+
if @Terminated.resolved?
|
725
|
+
probe.reject NoActor.new(@Pid), false
|
726
|
+
else
|
727
|
+
tell_op(Ask.new(message, probe)).then do
|
728
|
+
probe.reject NoActor.new(@Pid), false if @Terminated.resolved?
|
729
|
+
probe
|
730
|
+
end.flat
|
731
|
+
end
|
732
|
+
end
|
733
|
+
|
734
|
+
def terminated
|
735
|
+
@Terminated.with_hidden_resolvable
|
736
|
+
end
|
737
|
+
|
738
|
+
def pid
|
739
|
+
@Pid
|
740
|
+
end
|
741
|
+
|
742
|
+
def traps?
|
743
|
+
@trap
|
744
|
+
end
|
745
|
+
|
746
|
+
def trap(value = true)
|
747
|
+
old = @trap
|
748
|
+
# noinspection RubySimplifyBooleanInspection
|
749
|
+
@trap = !!value
|
750
|
+
old
|
751
|
+
end
|
752
|
+
|
753
|
+
def on(matcher, value = nil, &block)
|
754
|
+
raise ArgumentError, 'only one of block or value can be supplied' if block && value
|
755
|
+
[matcher, value || block]
|
756
|
+
end
|
757
|
+
|
758
|
+
def receive(*rules, timeout: nil, timeout_value: nil, **options, &block)
|
759
|
+
raise NotImplementedError
|
760
|
+
end
|
761
|
+
|
762
|
+
def link(pid)
|
763
|
+
return true if pid == @Pid
|
764
|
+
if @Linked.add? pid
|
765
|
+
pid.tell Link.new(@Pid)
|
766
|
+
if pid.terminated.resolved?
|
767
|
+
# no race since it only could get NoActor
|
768
|
+
if @trap
|
769
|
+
tell Terminate.new pid, NoActor.new(pid)
|
770
|
+
else
|
771
|
+
@Linked.delete pid
|
772
|
+
raise NoActor.new(pid)
|
773
|
+
end
|
774
|
+
end
|
775
|
+
end
|
776
|
+
true
|
777
|
+
end
|
778
|
+
|
779
|
+
def unlink(pid)
|
780
|
+
pid.tell UnLink.new(@Pid) if @Linked.delete pid
|
781
|
+
true
|
782
|
+
end
|
783
|
+
|
784
|
+
def linked?(pid)
|
785
|
+
@Linked.include? pid
|
786
|
+
end
|
787
|
+
|
788
|
+
def monitor(pid)
|
789
|
+
# *monitoring* *monitored*
|
790
|
+
# send Monitor
|
791
|
+
# terminated?
|
792
|
+
# terminate before getting Monitor
|
793
|
+
# drain signals including the Monitor
|
794
|
+
reference = Reference.new
|
795
|
+
@Monitoring[reference] = pid
|
796
|
+
if pid.terminated.resolved?
|
797
|
+
# always return no-proc when terminated
|
798
|
+
tell DownSignal.new(pid, reference, NoActor.new(pid))
|
799
|
+
else
|
800
|
+
# otherwise let it race
|
801
|
+
pid.tell Monitor.new(@Pid, reference)
|
802
|
+
# no race, it cannot get anything else than NoActor
|
803
|
+
tell DownSignal.new(pid, reference, NoActor.new(pid)) if pid.terminated.resolved?
|
804
|
+
end
|
805
|
+
reference
|
806
|
+
end
|
807
|
+
|
808
|
+
def demonitor(reference, *options)
|
809
|
+
info = options.delete :info
|
810
|
+
flush = options.delete :flush
|
811
|
+
raise ArgumentError, "bad options #{options}" unless options.empty?
|
812
|
+
|
813
|
+
pid = @Monitoring.delete reference
|
814
|
+
demonitoring = !!pid
|
815
|
+
pid.tell DeMonitor.new @Pid, reference if demonitoring
|
816
|
+
|
817
|
+
if flush
|
818
|
+
# remove (one) down message having reference from mailbox
|
819
|
+
flushed = demonitoring ? !!@Mailbox.try_pop_matching(And[DownSignal, -> m { m.reference == reference }]) : false
|
820
|
+
return info ? !flushed : true
|
821
|
+
end
|
822
|
+
|
823
|
+
if info
|
824
|
+
return false unless demonitoring
|
825
|
+
|
826
|
+
if @Mailbox.peek_matching(And[DownSignal, -> m { m.reference == reference }])
|
827
|
+
@MonitoringLateDelivery[reference] = pid # allow to deliver the message once
|
828
|
+
return false
|
829
|
+
end
|
830
|
+
end
|
831
|
+
|
832
|
+
return true
|
833
|
+
end
|
834
|
+
|
835
|
+
def monitoring?(reference)
|
836
|
+
@Monitoring.include? reference
|
837
|
+
end
|
838
|
+
|
839
|
+
def spawn(*args,
|
840
|
+
type:,
|
841
|
+
channel:,
|
842
|
+
environment:,
|
843
|
+
name:,
|
844
|
+
link:,
|
845
|
+
monitor:,
|
846
|
+
executor:,
|
847
|
+
&body)
|
848
|
+
actor = ErlangActor.create type, channel, environment, name, executor
|
849
|
+
pid = actor.pid
|
850
|
+
link pid if link
|
851
|
+
ref = (monitor pid if monitor)
|
852
|
+
actor.run(*args, &body)
|
853
|
+
monitor ? [pid, ref] : pid
|
854
|
+
end
|
855
|
+
|
856
|
+
def reply_resolution(fulfilled, value, reason)
|
857
|
+
return false unless @reply
|
858
|
+
return !!@reply.resolve(fulfilled, value, reason, false)
|
859
|
+
end
|
860
|
+
|
861
|
+
def terminate(pid = nil, reason, value: nil)
|
862
|
+
if pid
|
863
|
+
# has to send it to itself even if pid equals self.pid
|
864
|
+
if reason == :kill
|
865
|
+
pid.tell Kill.new(@Pid)
|
866
|
+
else
|
867
|
+
pid.tell Terminate.new(@Pid, reason, false)
|
868
|
+
end
|
869
|
+
else
|
870
|
+
terminate_self(reason, value)
|
871
|
+
end
|
872
|
+
end
|
873
|
+
|
874
|
+
private
|
875
|
+
|
876
|
+
def canonical_rules(rules, timeout, timeout_value, given_block)
|
877
|
+
block = given_block || -> v { v }
|
878
|
+
case rules.size
|
879
|
+
when 0
|
880
|
+
rules.push(on(ANY, &block))
|
881
|
+
when 1
|
882
|
+
matcher = rules.first
|
883
|
+
if matcher.is_a?(::Array) && matcher.size == 2
|
884
|
+
return ArgumentError.new 'a block cannot be given if full rules are used' if given_block
|
885
|
+
else
|
886
|
+
rules.replace([on(matcher, &block)])
|
887
|
+
end
|
888
|
+
else
|
889
|
+
return ArgumentError.new 'a block cannot be given if full rules are used' if given_block
|
890
|
+
end
|
891
|
+
|
892
|
+
if timeout
|
893
|
+
# TIMEOUT rule has to be first, to prevent any picking it up ANY
|
894
|
+
has_timeout = nil
|
895
|
+
i = rules.size
|
896
|
+
rules.reverse_each do |r, _|
|
897
|
+
i -= 1
|
898
|
+
if r == TIMEOUT
|
899
|
+
has_timeout = i
|
900
|
+
break
|
901
|
+
end
|
902
|
+
end
|
903
|
+
|
904
|
+
rules.unshift(has_timeout ? rules[has_timeout] : on(TIMEOUT, timeout_value))
|
905
|
+
end
|
906
|
+
nil
|
907
|
+
end
|
908
|
+
|
909
|
+
def eval_task(message, job)
|
910
|
+
if job.is_a? Proc
|
911
|
+
@Environment.instance_exec message, &job
|
912
|
+
else
|
913
|
+
job
|
914
|
+
end
|
915
|
+
end
|
916
|
+
|
917
|
+
def send_exit_messages(reason)
|
918
|
+
@Linked.each do |pid|
|
919
|
+
pid.tell Terminate.new(@Pid, reason)
|
920
|
+
end.clear
|
921
|
+
@Monitors.each do |reference, pid|
|
922
|
+
pid.tell DownSignal.new(@Pid, reference, reason)
|
923
|
+
end.clear
|
924
|
+
end
|
925
|
+
|
926
|
+
def asked?
|
927
|
+
!!@reply
|
928
|
+
end
|
929
|
+
|
930
|
+
def clean_reply(reason = NoReply)
|
931
|
+
if @reply
|
932
|
+
@reply.reject(reason, false)
|
933
|
+
@reply = nil
|
934
|
+
end
|
935
|
+
end
|
936
|
+
|
937
|
+
def consume_signal(message)
|
938
|
+
if AbstractSignal === message
|
939
|
+
case message
|
940
|
+
when Ask
|
941
|
+
@reply = message.probe
|
942
|
+
message.message
|
943
|
+
when Link
|
944
|
+
@Linked.add message.from
|
945
|
+
NOTHING
|
946
|
+
when UnLink
|
947
|
+
@Linked.delete message.from
|
948
|
+
NOTHING
|
949
|
+
when Monitor
|
950
|
+
@Monitors[message.reference] = message.from
|
951
|
+
NOTHING
|
952
|
+
when DeMonitor
|
953
|
+
@Monitors.delete message.reference
|
954
|
+
NOTHING
|
955
|
+
when Kill
|
956
|
+
terminate :killed
|
957
|
+
when DownSignal
|
958
|
+
if @Monitoring.delete(message.reference) || @MonitoringLateDelivery.delete(message.reference)
|
959
|
+
# put into a queue
|
960
|
+
return Down.new(message.from, message.reference, message.info)
|
961
|
+
end
|
962
|
+
|
963
|
+
# ignore down message if no longer monitoring, and following case
|
964
|
+
#
|
965
|
+
# *monitoring* *monitored*
|
966
|
+
# send Monitor
|
967
|
+
# terminate
|
968
|
+
# terminated?
|
969
|
+
# drain signals # generates second DOWN which is dropped here
|
970
|
+
# already reported as :noproc
|
971
|
+
NOTHING
|
972
|
+
when Terminate
|
973
|
+
consume_exit message
|
974
|
+
else
|
975
|
+
raise "unknown message #{message}"
|
976
|
+
end
|
977
|
+
else
|
978
|
+
# regular message
|
979
|
+
message
|
980
|
+
end
|
981
|
+
end
|
982
|
+
|
983
|
+
def consume_exit(exit_message)
|
984
|
+
from, reason = exit_message
|
985
|
+
if !exit_message.link_terminated || @Linked.delete(from)
|
986
|
+
case reason
|
987
|
+
when :normal
|
988
|
+
if @trap
|
989
|
+
Terminated.new from, reason
|
990
|
+
else
|
991
|
+
if from == @Pid
|
992
|
+
terminate :normal
|
993
|
+
else
|
994
|
+
NOTHING # do nothing
|
995
|
+
end
|
996
|
+
end
|
997
|
+
else
|
998
|
+
if @trap
|
999
|
+
Terminated.new from, reason
|
1000
|
+
else
|
1001
|
+
terminate reason
|
1002
|
+
end
|
1003
|
+
end
|
1004
|
+
else
|
1005
|
+
# *link* *exiting*
|
1006
|
+
# send Link
|
1007
|
+
# terminate
|
1008
|
+
# terminated?
|
1009
|
+
# drain signals # generates second Terminated which is dropped here
|
1010
|
+
# already processed exit message, do nothing
|
1011
|
+
NOTHING
|
1012
|
+
end
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
def initial_signal_consumption
|
1016
|
+
while true
|
1017
|
+
message = @Mailbox.try_pop
|
1018
|
+
break unless message
|
1019
|
+
consume_signal(message) == NOTHING or raise 'it was not consumable signal'
|
1020
|
+
end
|
1021
|
+
end
|
1022
|
+
|
1023
|
+
def terminate_self(reason, value)
|
1024
|
+
raise NotImplementedError
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
def after_termination(final_reason)
|
1028
|
+
log Logger::DEBUG, @Pid, terminated: final_reason
|
1029
|
+
clean_reply NoActor.new(@Pid)
|
1030
|
+
while true
|
1031
|
+
message = @Mailbox.try_pop NOTHING
|
1032
|
+
break if message == NOTHING
|
1033
|
+
case message
|
1034
|
+
when Monitor
|
1035
|
+
# The actor is terminated so we must return NoActor,
|
1036
|
+
# even though we still know the reason.
|
1037
|
+
# Otherwise it would return different reasons non-deterministically.
|
1038
|
+
message.from.tell DownSignal.new(@Pid, message.reference, NoActor.new(@Pid))
|
1039
|
+
when Link
|
1040
|
+
# same as for Monitor
|
1041
|
+
message.from.tell NoActor.new(@Pid)
|
1042
|
+
when Ask
|
1043
|
+
message.probe.reject(NoActor.new(@Pid), false)
|
1044
|
+
else
|
1045
|
+
# normal messages and other signals are thrown away
|
1046
|
+
end
|
1047
|
+
end
|
1048
|
+
@Mailbox = nil
|
1049
|
+
end
|
1050
|
+
end
|
1051
|
+
|
1052
|
+
private_constant :AbstractActor
|
1053
|
+
|
1054
|
+
class OnPool < AbstractActor
|
1055
|
+
|
1056
|
+
def initialize(channel, environment, name, executor)
|
1057
|
+
super channel, environment, name, executor
|
1058
|
+
@Executor = executor
|
1059
|
+
@behaviour = []
|
1060
|
+
@keep_behaviour = false
|
1061
|
+
end
|
1062
|
+
|
1063
|
+
def run(*args, &body)
|
1064
|
+
body ||= -> { start }
|
1065
|
+
|
1066
|
+
initial_signal_consumption
|
1067
|
+
inner_run(*args, &body).
|
1068
|
+
run(Run::TEST).
|
1069
|
+
then(&method(:after_termination)).
|
1070
|
+
rescue { |e| log Logger::ERROR, e }
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
def receive(*rules, timeout: nil, timeout_value: nil, keep: false, &given_block)
|
1074
|
+
clean_reply
|
1075
|
+
err = canonical_rules rules, timeout, timeout_value, given_block
|
1076
|
+
raise err if err
|
1077
|
+
|
1078
|
+
@keep_behaviour = keep
|
1079
|
+
@timeout = timeout
|
1080
|
+
@behaviour = rules
|
1081
|
+
throw JUMP, [RECEIVE]
|
1082
|
+
end
|
1083
|
+
|
1084
|
+
private
|
1085
|
+
|
1086
|
+
def terminate_self(reason, value)
|
1087
|
+
throw JUMP, [TERMINATE, reason, value]
|
1088
|
+
end
|
1089
|
+
|
1090
|
+
def inner_run(*args, &body)
|
1091
|
+
first = !!body
|
1092
|
+
future_body = -> message, _actor do
|
1093
|
+
kind, reason, value =
|
1094
|
+
if message.is_a?(::Array) && message.first == TERMINATE
|
1095
|
+
message
|
1096
|
+
else
|
1097
|
+
begin
|
1098
|
+
catch(JUMP) do
|
1099
|
+
[NOTHING,
|
1100
|
+
:normal,
|
1101
|
+
first ? @Environment.instance_exec(*args, &body) : apply_behaviour(message)]
|
1102
|
+
end
|
1103
|
+
rescue => e
|
1104
|
+
[TERMINATE, e, nil]
|
1105
|
+
end
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
case kind
|
1109
|
+
when TERMINATE
|
1110
|
+
send_exit_messages reason
|
1111
|
+
@Terminated.resolve(reason == :normal, value, reason)
|
1112
|
+
reason
|
1113
|
+
when RECEIVE
|
1114
|
+
Run[inner_run]
|
1115
|
+
when NOTHING
|
1116
|
+
if @behaviour.empty?
|
1117
|
+
send_exit_messages reason
|
1118
|
+
@Terminated.resolve(reason == :normal, value, reason)
|
1119
|
+
reason
|
1120
|
+
else
|
1121
|
+
Run[inner_run]
|
1122
|
+
end
|
1123
|
+
else
|
1124
|
+
raise "bad kind: #{kind.inspect}"
|
1125
|
+
end
|
1126
|
+
end
|
1127
|
+
|
1128
|
+
if first
|
1129
|
+
Promises.future_on(@Executor, nil, self, &future_body)
|
1130
|
+
else
|
1131
|
+
internal_receive.run(Run::TEST).then(self, &future_body)
|
1132
|
+
end
|
1133
|
+
end
|
1134
|
+
|
1135
|
+
def internal_receive
|
1136
|
+
raise if @behaviour.empty?
|
1137
|
+
rules_matcher = Or[*@behaviour.map(&:first)]
|
1138
|
+
matcher = -> m { m.is_a?(Ask) ? rules_matcher === m.message : rules_matcher === m }
|
1139
|
+
start = nil
|
1140
|
+
message_future = case @timeout
|
1141
|
+
when 0
|
1142
|
+
Promises.fulfilled_future @Mailbox.try_pop_matching(matcher, TIMEOUT)
|
1143
|
+
when Numeric
|
1144
|
+
pop = @Mailbox.pop_op_matching(matcher)
|
1145
|
+
start = Concurrent.monotonic_time
|
1146
|
+
# FIXME (pitr-ch 30-Jan-2019): the scheduled future should be cancelled
|
1147
|
+
(Promises.schedule(@timeout) { TIMEOUT } | pop).then(pop) do |message, p|
|
1148
|
+
if message == TIMEOUT && !p.resolve(true, TIMEOUT, nil, false)
|
1149
|
+
# timeout raced with probe resolution, take the value instead
|
1150
|
+
p.value
|
1151
|
+
else
|
1152
|
+
message
|
1153
|
+
end
|
1154
|
+
end
|
1155
|
+
when nil
|
1156
|
+
@Mailbox.pop_op_matching(matcher)
|
1157
|
+
else
|
1158
|
+
raise
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
message_future.then(start, self) do |message, s, _actor|
|
1162
|
+
log Logger::DEBUG, pid, got: message
|
1163
|
+
catch(JUMP) do
|
1164
|
+
if (message = consume_signal(message)) == NOTHING
|
1165
|
+
@timeout = [@timeout + s - Concurrent.monotonic_time, 0].max if s
|
1166
|
+
Run[internal_receive]
|
1167
|
+
else
|
1168
|
+
message
|
1169
|
+
end
|
1170
|
+
end
|
1171
|
+
end
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
def apply_behaviour(message)
|
1175
|
+
@behaviour.each do |rule, job|
|
1176
|
+
if rule === message
|
1177
|
+
@behaviour = [] unless @keep_behaviour
|
1178
|
+
return eval_task(message, job)
|
1179
|
+
end
|
1180
|
+
end
|
1181
|
+
raise 'should not reach'
|
1182
|
+
end
|
1183
|
+
end
|
1184
|
+
|
1185
|
+
private_constant :OnPool
|
1186
|
+
|
1187
|
+
class OnThread < AbstractActor
|
1188
|
+
def initialize(channel, environment, name, executor)
|
1189
|
+
super channel, environment, name, executor
|
1190
|
+
@Thread = nil
|
1191
|
+
end
|
1192
|
+
|
1193
|
+
TERMINATE = Module.new
|
1194
|
+
private_constant :TERMINATE
|
1195
|
+
|
1196
|
+
def run(*args, &body)
|
1197
|
+
initial_signal_consumption
|
1198
|
+
@Thread = Thread.new(@Terminated, self) do |terminated, _actor| # sync point
|
1199
|
+
Thread.abort_on_exception = true
|
1200
|
+
|
1201
|
+
final_reason = begin
|
1202
|
+
reason, value = catch(TERMINATE) do
|
1203
|
+
[:normal, @Environment.instance_exec(*args, &body)]
|
1204
|
+
end
|
1205
|
+
send_exit_messages reason
|
1206
|
+
terminated.resolve(reason == :normal, value, reason)
|
1207
|
+
reason
|
1208
|
+
rescue => e
|
1209
|
+
send_exit_messages e
|
1210
|
+
terminated.reject e
|
1211
|
+
e
|
1212
|
+
end
|
1213
|
+
|
1214
|
+
after_termination final_reason
|
1215
|
+
@Thread = nil
|
1216
|
+
end
|
1217
|
+
end
|
1218
|
+
|
1219
|
+
def receive(*rules, timeout: nil, timeout_value: nil, &given_block)
|
1220
|
+
clean_reply
|
1221
|
+
|
1222
|
+
err = canonical_rules rules, timeout, timeout_value, given_block
|
1223
|
+
raise err if err
|
1224
|
+
|
1225
|
+
rules_matcher = Or[*rules.map(&:first)]
|
1226
|
+
matcher = -> m { m.is_a?(Ask) ? rules_matcher === m.message : rules_matcher === m }
|
1227
|
+
while true
|
1228
|
+
message = @Mailbox.pop_matching(matcher, timeout, TIMEOUT)
|
1229
|
+
log Logger::DEBUG, pid, got: message
|
1230
|
+
unless (message = consume_signal(message)) == NOTHING
|
1231
|
+
rules.each do |rule, job|
|
1232
|
+
return eval_task(message, job) if rule === message
|
1233
|
+
end
|
1234
|
+
end
|
1235
|
+
end
|
1236
|
+
end
|
1237
|
+
|
1238
|
+
private
|
1239
|
+
|
1240
|
+
def terminate_self(reason, value)
|
1241
|
+
throw TERMINATE, [reason, value]
|
1242
|
+
end
|
1243
|
+
end
|
1244
|
+
|
1245
|
+
private_constant :OnThread
|
1246
|
+
|
1247
|
+
class AbstractSignal < Synchronization::Object
|
1248
|
+
safe_initialization!
|
1249
|
+
end
|
1250
|
+
|
1251
|
+
private_constant :AbstractSignal
|
1252
|
+
|
1253
|
+
class Ask < AbstractSignal
|
1254
|
+
attr_reader :message, :probe
|
1255
|
+
|
1256
|
+
def initialize(message, probe)
|
1257
|
+
super()
|
1258
|
+
@message = message
|
1259
|
+
@probe = probe
|
1260
|
+
raise ArgumentError, 'probe is not Resolvable' unless probe.is_a? Promises::Resolvable
|
1261
|
+
end
|
1262
|
+
end
|
1263
|
+
|
1264
|
+
private_constant :Ask
|
1265
|
+
|
1266
|
+
module HasFrom
|
1267
|
+
|
1268
|
+
# @return [Pid]
|
1269
|
+
attr_reader :from
|
1270
|
+
|
1271
|
+
# @!visibility private
|
1272
|
+
def initialize(from)
|
1273
|
+
# noinspection RubySuperCallWithoutSuperclassInspection
|
1274
|
+
super()
|
1275
|
+
@from = from
|
1276
|
+
end
|
1277
|
+
|
1278
|
+
# @return [true, false]
|
1279
|
+
def ==(o)
|
1280
|
+
o.class == self.class && o.from == @from
|
1281
|
+
end
|
1282
|
+
|
1283
|
+
alias_method :eql?, :==
|
1284
|
+
|
1285
|
+
# @return [Integer]
|
1286
|
+
def hash
|
1287
|
+
@from.hash
|
1288
|
+
end
|
1289
|
+
end
|
1290
|
+
|
1291
|
+
private_constant :HasFrom
|
1292
|
+
|
1293
|
+
module HasReason
|
1294
|
+
include HasFrom
|
1295
|
+
|
1296
|
+
# @return [Object]
|
1297
|
+
attr_reader :reason
|
1298
|
+
|
1299
|
+
# @!visibility private
|
1300
|
+
def initialize(from, reason)
|
1301
|
+
# noinspection RubySuperCallWithoutSuperclassInspection
|
1302
|
+
super from
|
1303
|
+
@reason = reason
|
1304
|
+
end
|
1305
|
+
|
1306
|
+
# @return [::Array(Pid, Object)]
|
1307
|
+
def to_ary
|
1308
|
+
[@from, @reason]
|
1309
|
+
end
|
1310
|
+
|
1311
|
+
# @return [true, false]
|
1312
|
+
def ==(o)
|
1313
|
+
# noinspection RubySuperCallWithoutSuperclassInspection
|
1314
|
+
super(o) && o.reason == self.reason
|
1315
|
+
end
|
1316
|
+
|
1317
|
+
# @return [Integer]
|
1318
|
+
def hash
|
1319
|
+
[@from, @reason].hash
|
1320
|
+
end
|
1321
|
+
end
|
1322
|
+
|
1323
|
+
private_constant :HasReason
|
1324
|
+
|
1325
|
+
module HasReference
|
1326
|
+
include HasFrom
|
1327
|
+
|
1328
|
+
# @return [Reference]
|
1329
|
+
attr_reader :reference
|
1330
|
+
|
1331
|
+
# @!visibility private
|
1332
|
+
def initialize(from, reference)
|
1333
|
+
# noinspection RubySuperCallWithoutSuperclassInspection
|
1334
|
+
super from
|
1335
|
+
@reference = reference
|
1336
|
+
end
|
1337
|
+
|
1338
|
+
# @return [::Array(Pid, Reference)]
|
1339
|
+
def to_ary
|
1340
|
+
[@from, @reference]
|
1341
|
+
end
|
1342
|
+
|
1343
|
+
# @return [true, false]
|
1344
|
+
def ==(o)
|
1345
|
+
# noinspection RubySuperCallWithoutSuperclassInspection
|
1346
|
+
super(o) && o.reference == self.reference
|
1347
|
+
end
|
1348
|
+
|
1349
|
+
# @return [Integer]
|
1350
|
+
def hash
|
1351
|
+
[@from, @reference].hash
|
1352
|
+
end
|
1353
|
+
end
|
1354
|
+
|
1355
|
+
private_constant :HasReference
|
1356
|
+
|
1357
|
+
class Terminate < AbstractSignal
|
1358
|
+
include HasReason
|
1359
|
+
|
1360
|
+
attr_reader :link_terminated
|
1361
|
+
|
1362
|
+
def initialize(from, reason, link_terminated = true)
|
1363
|
+
super from, reason
|
1364
|
+
@link_terminated = link_terminated
|
1365
|
+
end
|
1366
|
+
end
|
1367
|
+
|
1368
|
+
private_constant :Terminate
|
1369
|
+
|
1370
|
+
class Kill < AbstractSignal
|
1371
|
+
include HasFrom
|
1372
|
+
end
|
1373
|
+
|
1374
|
+
private_constant :Kill
|
1375
|
+
|
1376
|
+
class Link < AbstractSignal
|
1377
|
+
include HasFrom
|
1378
|
+
end
|
1379
|
+
|
1380
|
+
private_constant :Link
|
1381
|
+
|
1382
|
+
class UnLink < AbstractSignal
|
1383
|
+
include HasFrom
|
1384
|
+
end
|
1385
|
+
|
1386
|
+
private_constant :UnLink
|
1387
|
+
|
1388
|
+
class Monitor < AbstractSignal
|
1389
|
+
include HasReference
|
1390
|
+
end
|
1391
|
+
|
1392
|
+
private_constant :Monitor
|
1393
|
+
|
1394
|
+
class DeMonitor < AbstractSignal
|
1395
|
+
include HasReference
|
1396
|
+
end
|
1397
|
+
|
1398
|
+
private_constant :DeMonitor
|
1399
|
+
|
1400
|
+
# A message send when actor terminates.
|
1401
|
+
class Terminated
|
1402
|
+
# @return [Pid]
|
1403
|
+
attr_reader :from
|
1404
|
+
# @return [Object]
|
1405
|
+
attr_reader :reason
|
1406
|
+
|
1407
|
+
# @!visibility private
|
1408
|
+
def initialize(from, reason)
|
1409
|
+
# noinspection RubySuperCallWithoutSuperclassInspection
|
1410
|
+
@from = from
|
1411
|
+
@reason = reason
|
1412
|
+
end
|
1413
|
+
|
1414
|
+
# @return [::Array(Pid, Object)]
|
1415
|
+
def to_ary
|
1416
|
+
[@from, @reason]
|
1417
|
+
end
|
1418
|
+
|
1419
|
+
# @return [true, false]
|
1420
|
+
def ==(o)
|
1421
|
+
o.class == self.class && o.from == @from && o.reason == self.reason
|
1422
|
+
end
|
1423
|
+
|
1424
|
+
alias_method :eql?, :==
|
1425
|
+
|
1426
|
+
# @return [Integer]
|
1427
|
+
def hash
|
1428
|
+
[@from, @reason].hash
|
1429
|
+
end
|
1430
|
+
end
|
1431
|
+
|
1432
|
+
class DownSignal < AbstractSignal
|
1433
|
+
include HasReference
|
1434
|
+
|
1435
|
+
# @return [Object]
|
1436
|
+
attr_reader :info
|
1437
|
+
|
1438
|
+
# @!visibility private
|
1439
|
+
def initialize(from, reference, info)
|
1440
|
+
super from, reference
|
1441
|
+
@info = info
|
1442
|
+
end
|
1443
|
+
|
1444
|
+
# @return [::Array(Pis, Reference, Object)]
|
1445
|
+
def to_ary
|
1446
|
+
[@from, @reference, @info]
|
1447
|
+
end
|
1448
|
+
|
1449
|
+
# @return [true, false]
|
1450
|
+
def ==(o)
|
1451
|
+
super(o) && o.info == self.info
|
1452
|
+
end
|
1453
|
+
|
1454
|
+
# @return [Integer]
|
1455
|
+
def hash
|
1456
|
+
to_ary.hash
|
1457
|
+
end
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
private_constant :DownSignal
|
1461
|
+
|
1462
|
+
# A message send by a monitored actor when terminated.
|
1463
|
+
class Down
|
1464
|
+
# @return [Pid]
|
1465
|
+
attr_reader :from
|
1466
|
+
# @return [Reference]
|
1467
|
+
attr_reader :reference
|
1468
|
+
# @return [Object]
|
1469
|
+
attr_reader :info
|
1470
|
+
|
1471
|
+
# @!visibility private
|
1472
|
+
def initialize(from, reference, info)
|
1473
|
+
@from = from
|
1474
|
+
@reference = reference
|
1475
|
+
@info = info
|
1476
|
+
end
|
1477
|
+
|
1478
|
+
# @return [::Array(Pis, Reference, Object)]
|
1479
|
+
def to_ary
|
1480
|
+
[@from, @reference, @info]
|
1481
|
+
end
|
1482
|
+
|
1483
|
+
# @return [true, false]
|
1484
|
+
def ==(o)
|
1485
|
+
o.class == self.class && o.from == @from && o.reference == @reference && o.info == @info
|
1486
|
+
end
|
1487
|
+
|
1488
|
+
alias_method :eql?, :==
|
1489
|
+
|
1490
|
+
# @return [Integer]
|
1491
|
+
def hash
|
1492
|
+
to_ary.hash
|
1493
|
+
end
|
1494
|
+
end
|
1495
|
+
|
1496
|
+
# Abstract error class for ErlangActor errors.
|
1497
|
+
class Error < Concurrent::Error
|
1498
|
+
end
|
1499
|
+
|
1500
|
+
# An error used when actor tries to link or monitor terminated actor.
|
1501
|
+
class NoActor < Error
|
1502
|
+
# @return [Pid]
|
1503
|
+
attr_reader :pid
|
1504
|
+
|
1505
|
+
# @param [Pid] pid
|
1506
|
+
# @return [self]
|
1507
|
+
def initialize(pid = nil)
|
1508
|
+
super(pid.to_s)
|
1509
|
+
@pid = pid
|
1510
|
+
end
|
1511
|
+
|
1512
|
+
# @return [true, false]
|
1513
|
+
def ==(o)
|
1514
|
+
o.class == self.class && o.pid == self.pid
|
1515
|
+
end
|
1516
|
+
|
1517
|
+
alias_method :eql?, :==
|
1518
|
+
|
1519
|
+
# @return [Integer]
|
1520
|
+
def hash
|
1521
|
+
pid.hash
|
1522
|
+
end
|
1523
|
+
end
|
1524
|
+
|
1525
|
+
# An error used when actor is asked but no reply was given or
|
1526
|
+
# when the actor terminates before it gives a reply.
|
1527
|
+
class NoReply < Error
|
1528
|
+
end
|
1529
|
+
|
1530
|
+
# @!visibility private
|
1531
|
+
def self.create(type, channel, environment, name, executor)
|
1532
|
+
actor = KLASS_MAP.fetch(type).new(channel, environment, name, executor)
|
1533
|
+
ensure
|
1534
|
+
log Logger::DEBUG, actor.pid, created: caller[1] if actor
|
1535
|
+
end
|
1536
|
+
|
1537
|
+
KLASS_MAP = {
|
1538
|
+
on_thread: OnThread,
|
1539
|
+
on_pool: OnPool,
|
1540
|
+
OnThread => OnThread,
|
1541
|
+
OnPool => OnPool,
|
1542
|
+
}
|
1543
|
+
private_constant :KLASS_MAP
|
1544
|
+
end
|
1545
|
+
end
|