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.
@@ -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