concurrent-ruby-edge 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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