Add ErlangActor implementation

This commit is contained in:
Petr Chalupa 2019-02-07 22:00:54 +01:00
parent 85763d2c4c
commit a5a3024405
12 changed files with 2668 additions and 16 deletions

View File

@ -0,0 +1,145 @@
## Examples
The simplest example is to use the actor as an asynchronous execution.
Although, `Promises.future { 1 + 1 }` is better suited for that purpose.
```ruby
actor = Concurrent::ErlangActor.spawn(:on_thread, name: 'addition') { 1 + 1 }
actor.terminated.value!
```
Let's send some messages and maintain some internal state
which is what actors are good for.
```ruby
actor = Concurrent::ErlangActor.spawn(:on_thread, name: 'sum') do
sum = 0 # internal state
# receive and sum the messages until the actor gets :done
while true
message = receive
break if message == :done
# if the message is asked and not only told,
# reply with a current sum
reply sum += message
end
sum
end
```
The actor can be either told a message asynchronously,
or asked. The ask method will block until actor replies.
```ruby
# tell returns immediately returning the actor
actor.tell(1).tell(1)
# blocks, waiting for the answer
actor.ask 10
# stop the actor
actor.tell :done
actor.terminated.value!
```
### Receiving
Simplest message receive.
```ruby
actor = Concurrent::ErlangActor.spawn(:on_thread) { receive }
actor.tell :m
actor.terminated.value!
```
which also works for actor on pool,
because if no block is given it will use a default block `{ |v| v }`
```ruby
actor = Concurrent::ErlangActor.spawn(:on_pool) { receive { |v| v } }
# can simply be following
actor = Concurrent::ErlangActor.spawn(:on_pool) { receive }
actor.tell :m
actor.terminated.value!
```
TBA
### Actor types
There are two types of actors.
The type is specified when calling spawn as a first argument,
`Concurrent::ErlangActor.spawn(:on_thread, ...` or
`Concurrent::ErlangActor.spawn(:on_pool, ...`.
The main difference is in how receive method returns.
- `:on_thread` it blocks the thread until message is available,
then it returns or calls the provided block first.
- However, `:on_pool` it has to free up the thread on the receive
call back to the pool. Therefore the call to receive ends the
execution of current scope. The receive has to be given block
or blocks that act as a continuations and are called
when there is message available.
Let's have a look at how the bodies of actors differ between the types:
```ruby
ping = Concurrent::ErlangActor.spawn(:on_thread) { reply receive }
ping.ask 42
```
It first calls receive, which blocks the thread of the actor.
When it returns the received message is passed an an argument to reply,
which replies the same value back to the ask method.
Then the actor terminates normally, because there is nothing else to do.
However when running on pool a block with code which should be evaluated
after the message is received has to be provided.
```ruby
ping = Concurrent::ErlangActor.spawn(:on_pool) { receive { |m| reply m } }
ping.ask 42
```
It starts by calling receive which will remember the given block for later
execution when a message is available and stops executing the current scope.
Later when a message becomes available the previously provided block is given
the message and called. The result of the block is the final value of the
normally terminated actor.
The direct blocking style of `:on_thread` is simpler to write and more straight
forward however it has limitations. Each `:on_thread` actor creates a Thread
taking time and resources.
There is also a limited number of threads the Ruby process can create
so you may hit the limit and fail to create more threads and therefore actors.
Since the `:on_pool` actor runs on a poll of threads, its creations
is faster and cheaper and it does not create new threads.
Therefore there is no limit (only RAM) on how many actors can be created.
To simplify, if you need only few actors `:on_thread` is fine.
However if you will be creating hundreds of actors or
they will be short-lived `:on_pool` should be used.
### Erlang behaviour
The actor matches Erlang processes in behaviour.
Therefore it supports the usual Erlang actor linking, monitoring, exit behaviour, etc.
```ruby
actor = Concurrent::ErlangActor.spawn(:on_thread) do
spawn(link: true) do # equivalent of spawn_link in Erlang
terminate :err # equivalent of exit in Erlang
end
trap # equivalent of process_flag(trap_exit, true)
receive
end
actor.terminated.value!
```
### TODO
* receives
* More erlang behaviour examples
* Back pressure with bounded mailbox
* _op methods
* types of actors

View File

@ -0,0 +1,7 @@
require 'concurrent-edge'
def do_stuff(*args)
sleep 0.01
:stuff
end

View File

@ -0,0 +1,132 @@
## Examples
The simplest example is to use the actor as an asynchronous execution.
Although, `Promises.future { 1 + 1 }` is better suited for that purpose.
```ruby
actor = Concurrent::ErlangActor.spawn(:on_thread, name: 'addition') { 1 + 1 }
# => #<Concurrent::ErlangActor::Pid:0x000002 addition>
actor.terminated.value! # => 2
```
Let's send some messages and maintain some internal state
which is what actors are good for.
```ruby
actor = Concurrent::ErlangActor.spawn(:on_thread, name: 'sum') do
sum = 0 # internal state
# receive and sum the messages until the actor gets :done
while true
message = receive
break if message == :done
# if the message is asked and not only told,
# reply with a current sum
reply sum += message
end
sum
end
# => #<Concurrent::ErlangActor::Pid:0x000003 sum>
```
The actor can be either told a message asynchronously,
or asked. The ask method will block until actor replies.
```ruby
# tell returns immediately returning the actor
actor.tell(1).tell(1)
# => #<Concurrent::ErlangActor::Pid:0x000003 sum>
# blocks, waiting for the answer
actor.ask 10 # => 12
# stop the actor
actor.tell :done
# => #<Concurrent::ErlangActor::Pid:0x000003 sum>
actor.terminated.value! # => 12
```
### Actor types
There are two types of actors.
The type is specified when calling spawn as a first argument,
`Concurrent::ErlangActor.spawn(:on_thread, ...` or
`Concurrent::ErlangActor.spawn(:on_pool, ...`.
The main difference is in how receive method returns.
- `:on_thread` it blocks the thread until message is available,
then it returns or calls the provided block first.
- However, `:on_pool` it has to free up the thread on the receive
call back to the pool. Therefore the call to receive ends the
execution of current scope. The receive has to be given block
or blocks that act as a continuations and are called
when there is message available.
Let's have a look at how the bodies of actors differ between the types:
```ruby
ping = Concurrent::ErlangActor.spawn(:on_thread) { reply receive }
# => #<Concurrent::ErlangActor::Pid:0x000004>
ping.ask 42 # => 42
```
It first calls receive, which blocks the thread of the actor.
When it returns the received message is passed an an argument to reply,
which replies the same value back to the ask method.
Then the actor terminates normally, because there is nothing else to do.
However when running on pool a block with code which should be evaluated
after the message is received has to be provided.
```ruby
ping = Concurrent::ErlangActor.spawn(:on_pool) { receive { |m| reply m } }
# => #<Concurrent::ErlangActor::Pid:0x000005>
ping.ask 42 # => 42
```
It starts by calling receive which will remember the given block for later
execution when a message is available and stops executing the current scope.
Later when a message becomes available the previously provided block is given
the message and called. The result of the block is the final value of the
normally terminated actor.
The direct blocking style of `:on_thread` is simpler to write and more straight
forward however it has limitations. Each `:on_thread` actor creates a Thread
taking time and resources.
There is also a limited number of threads the Ruby process can create
so you may hit the limit and fail to create more threads and therefore actors.
Since the `:on_pool` actor runs on a poll of threads, its creations
is faster and cheaper and it does not create new threads.
Therefore there is no limit (only RAM) on how many actors can be created.
To simplify, if you need only few actors `:on_thread` is fine.
However if you will be creating hundreds of actors or
they will be short-lived `:on_pool` should be used.
### Erlang behaviour
The actor matches Erlang processes in behaviour.
Therefore it supports the usual Erlang actor linking, monitoring, exit behaviour, etc.
```ruby
actor = Concurrent::ErlangActor.spawn(:on_thread) do
spawn(link: true) do # equivalent of spawn_link in Erlang
terminate :err # equivalent of exit in Erlang
end
trap # equivalent of process_flag(trap_exit, true)
receive
end
# => #<Concurrent::ErlangActor::Pid:0x000006>
actor.terminated.value!
# => #<Concurrent::ErlangActor::Exit:0x000007
# @from=#<Concurrent::ErlangActor::Pid:0x000008>,
# @link_terminated=true,
# @reason=:err>
```
### TODO
* More erlang behaviour examples
* Back pressure with bounded mailbox
* _op methods
* types of actors

View File

@ -13,3 +13,4 @@ require 'concurrent/edge/throttle'
require 'concurrent/edge/channel'
require 'concurrent/edge/processing_actor'
require 'concurrent/edge/erlang_actor'

View File

@ -55,6 +55,9 @@ module Concurrent
message message, future
end
# @!visibility privated
alias_method :ask_op, :ask
# Sends the message synchronously and blocks until the message
# is processed. Raises on error.
#

View File

@ -49,6 +49,10 @@ module Concurrent
def any.===(other)
true
end
def any.to_s
'ANY'
end
end
# Create channel.
@ -164,7 +168,7 @@ module Concurrent
#
# @!macro channel.warn.blocks
# @!macro channel.param.timeout
# @!macro promises.param.timeout_value
# @param [Object] timeout_value a value returned by the method when it times out
# @return [Object, nil] message or nil when timed out
def pop(timeout = nil, timeout_value = nil)
pop_matching ANY, timeout, timeout_value

File diff suppressed because it is too large Load Diff

View File

@ -15,9 +15,9 @@ module Concurrent
# values[-5, 5] # => [49996, 49997, 49998, 49999, 50000]
# @!macro warn.edge
class ProcessingActor < Synchronization::Object
# TODO (pitr-ch 18-Dec-2016): (un)linking, bidirectional, sends special message, multiple link calls has no effect,
# TODO (pitr-ch 21-Dec-2016): Make terminated a cancellation token?
# link_spawn atomic, Can it be fixed by sending exit when linked dead actor?
# TODO (pitr-ch 29-Jan-2019): simplify as much as possible, maybe even do not delegate to mailbox, no ask linking etc
# TODO (pitr-ch 03-Feb-2019): remove completely
safe_initialization!
@ -60,12 +60,8 @@ module Concurrent
# @yieldparam [Object] *args
# @yieldreturn [Promises::Future(Object)] a future representing next step of execution
# @return [ProcessingActor]
# @example
# # TODO (pitr-ch 19-Jan-2017): actor with limited mailbox
def self.act_listening(channel, *args, &process)
actor, _, terminated = ProcessingActor.new channel
Promises.future(actor, *args, &process).run.tangle(terminated)
actor
ProcessingActor.new channel, *args, &process
end
# # Receives a message when available, used in the actor's process.
@ -162,19 +158,21 @@ module Concurrent
end
# @return [String] string representation.
def inspect
format '%s termination:%s>', super[0..-2], termination.state
def to_s
format '%s termination: %s>', super[0..-2], termination.state
end
alias_method :inspect, :to_s
def to_ary
[self, @Mailbox, @Terminated]
[@Mailbox, @Terminated]
end
private
def initialize(channel = Promises::Channel.new)
def initialize(channel, *args, &process)
@Mailbox = channel
@Terminated = Promises.resolvable_future
@Terminated = Promises.future(self, *args, &process).run
super()
end

View File

@ -12,7 +12,7 @@ module Concurrent
# Asks the actor with its value.
# @return [Future] new future with the response form the actor
def then_ask(actor)
self.then(actor) { |v, a| a.ask(v) }.flat
self.then(actor) { |v, a| a.ask_op(v) }.flat
end
end

View File

@ -0,0 +1,976 @@
RSpec.describe 'Concurrent' do
describe 'ErlangActor', edge: true do
shared_examples 'erlang actor' do
# TODO (pitr-ch 06-Feb-2019): include constants instead
ANY ||= Concurrent::ErlangActor::ANY
TIMEOUT ||= Concurrent::ErlangActor::TIMEOUT
identity = -> v { v }
specify "run to termination" do
expect(Concurrent::ErlangActor.spawn(type) do
:v
end.terminated.value!).to eq :v
end
specify '#receive' do
id = -> v { v }
succ = -> v { v.succ }
[[[:v], -> { receive }, :v],
[[:v], -> { receive on(ANY, &id) }, :v],
[[:v, 1], -> { receive Numeric }, 1],
[[:v, 1], -> { receive(Numeric, &succ) }, 2],
[[:v], -> { receive Numeric, timeout: 0 }, nil],
[[:v], -> { receive(Numeric, timeout: 0, &succ) }, nil],
[[:v], -> { receive Numeric, timeout: 0, timeout_value: :timeout }, :timeout],
[[:v], -> { receive(Numeric, timeout: 0, timeout_value: :timeout, &succ) }, :timeout],
[[:v, 1], -> { receive Numeric, timeout: 1 }, 1],
[[:v, 1], -> { receive(Numeric, timeout: 1, &succ) }, 2],
[[:v, 1], -> { receive Numeric, timeout: 1, timeout_value: :timeout }, 1],
[[:v, 1], -> { receive(Numeric, timeout: 1, timeout_value: :timeout, &succ) }, 2],
[[:v], -> { receive on(Numeric, &id), on(TIMEOUT, nil), timeout: 0 }, nil],
[[:v], -> { receive on(Numeric, &succ), on(TIMEOUT, nil), timeout: 0 }, nil],
[[:v], -> { receive on(Numeric, &id), on(TIMEOUT, :timeout), timeout: 0 }, :timeout],
[[:v], -> { receive on(Numeric, &succ), on(TIMEOUT, :timeout), timeout: 0 }, :timeout],
[[:v, 1], -> { receive on(Numeric, &id), on(TIMEOUT, nil), timeout: 1 }, 1],
[[:v, 1], -> { receive on(Numeric, &succ), on(TIMEOUT, nil), timeout: 1 }, 2],
[[:v, 1], -> { receive on(Numeric, &id), on(TIMEOUT, :timeout), timeout: 1 }, 1],
[[:v, 1], -> { receive on(Numeric, &succ), on(TIMEOUT, :timeout), timeout: 1 }, 2],
].each_with_index do |(messages, body, result), i|
a = Concurrent::ErlangActor.spawn(type, &body)
messages.each { |m| a.tell m }
expect(a.terminated.value!).to eq(result), "body: #{body}"
end
end
specify 'pid has name' do
actor = Concurrent::ErlangActor.spawn(type, name: 'test') {}
expect(actor.to_s).to match(/test/)
expect(actor.inspect).to match(/test/)
end
specify "receives message" do
actor = Concurrent::ErlangActor.spawn(type,
&{ on_thread: -> { receive },
on_pool: -> { receive on(ANY, &identity) } }.fetch(type))
actor.tell :v
expect(actor.terminated.value!).to eq :v
end
specify "receives message with matchers" do
body = { on_thread:
-> do
[receive(on(Symbol, &identity)),
receive(on(Numeric, &:succ)),
receive(on(Numeric, :got_it), timeout: 0, timeout_value: :nothing)]
end,
on_pool:
-> do
@arr = []
receive(on(Symbol) do |v1|
@arr.push v1
receive(on(Numeric) do |v2|
@arr << v2.succ
receive(on(Numeric, :got_it), on(TIMEOUT) { @arr << :nothing; @arr }, timeout: 0)
end)
end)
end }
actor = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
actor.tell 'junk'
actor.tell 1
actor.tell :v
expect(actor.terminated.value!).to eq [:v, 2, :nothing]
end
describe "monitoring" do
specify "(de)monitor" do
body_receive = { on_thread:
-> { receive },
on_pool:
-> { receive { |v| v } } }
body = { on_thread:
-> do
actor = receive
reference = monitor actor
monitored = monitoring? reference
demonitor reference
result = [monitored, monitoring?(reference)]
actor.tell :finish
result
end,
on_pool:
-> do
receive do |actor|
reference = monitor actor
monitored = monitoring? reference
demonitor reference
result = [monitored, monitoring?(reference)]
actor.tell :finish
result
end
end }
a1 = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
a2 = Concurrent::ErlangActor.spawn(type, &body_receive.fetch(type))
a1.tell a2
expect(a1.terminated.value!).to eq [true, false]
expect(a2.terminated.value!).to eq :finish
end
specify "demonitor" do
body = { on_thread:
-> do
actor = receive
reference = monitor actor
monitored = monitoring? reference
actor.tell :done
actor.terminated.wait
demonitor = demonitor reference, :flush, :info
[monitored, monitoring?(reference), demonitor, receive(timeout: 0)]
end,
on_pool:
-> do
receive do |actor|
reference = monitor actor
monitored = monitoring? reference
actor.tell :done
actor.terminated.wait
demonitor = demonitor reference, :flush, :info
results = [monitored, monitoring?(reference), demonitor]
receive(on(ANY) { |v| [*results, v] },
on(TIMEOUT) { [*results, nil] },
timeout: 0)
end
end }
a1 = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
body = { on_thread: -> { receive },
on_pool: -> { receive(&identity) } }
a2 = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
a1.tell a2
a1.terminated.wait
expect(a1.terminated.value!).to eq [true, false, false, nil]
expect(a2.terminated.value!).to eq :done
end
specify "demonitor should leave the down message in the inbox if it's already there" do
body = { on_thread:
-> do
actor = receive
reference = monitor actor
monitored = monitoring? reference
actor.tell :done
actor.terminated.wait
demonitor = demonitor reference, :info
[reference, monitored, monitoring?(reference), demonitor, receive(timeout: 0)]
end,
on_pool:
-> do
receive do |actor|
reference = monitor actor
monitored = monitoring? reference
actor.tell :done
actor.terminated.wait
demonitor = demonitor reference, :info
results = [reference, monitored, monitoring?(reference), demonitor]
receive(on(ANY) { |v| [*results, v] },
on(TIMEOUT) { [*results, nil] },
timeout: 0)
end
end }
a1 = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
body = { on_thread: -> { receive },
on_pool: -> { receive(&identity) } }
a2 = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
a1.tell a2
reference, monitored, monitoring, demonitor, message = a1.terminated.value!
expect(monitored).to eq true
expect(monitoring).to eq false
expect(demonitor).to eq false
expect(message).to eq Concurrent::ErlangActor::Down.new(a2, reference, :normal)
expect(a2.terminated.value!).to eq :done
end
specify "notifications 1" do
body = { on_thread:
-> do
b = spawn { [:done, receive] }
ref = monitor b
b.tell 42
[b, ref, receive]
end,
on_pool:
-> do
b = spawn { receive on(ANY) { |v| [:done, v] } }
ref = monitor b
b.tell 42
receive on(ANY) { |v| [b, ref, v] }
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
b, ref, down = a.terminated.value!
expect(down).to eq Concurrent::ErlangActor::Down.new(b, ref, :normal)
expect(b.terminated.value!).to eq [:done, 42]
end
specify "notifications 2" do
body = { on_thread:
-> do
b = spawn { :done }
b.terminated.wait
ref = monitor b
[b, ref, receive(timeout: 0.01, timeout_value: :timeout)]
end,
on_pool:
-> do
b = spawn { :done }
b.terminated.wait
ref = monitor b
receive(timeout: 0.01) { |v| [b, ref, v] }
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
b, ref, down = a.terminated.value!
expect(down).to eq Concurrent::ErlangActor::Down.new(b, ref, Concurrent::ErlangActor::NoActor.new(b))
expect(b.terminated.value!).to eq :done
end
# FIXME (pitr-ch 20-Jan-2019): test concurrent exit and monitor(), same for link
end
describe 'linking' do
body_receive_test_linked = { on_thread:
-> { linked?(receive) },
on_pool:
-> { receive { |a| linked? a } } }
specify 'links' do
body1 = { on_thread:
-> do
actor = receive
link actor
linked = linked? actor
actor.tell pid
linked
end,
on_pool:
-> do
receive do |actor|
link actor
linked = linked? actor
actor.tell pid
linked
end
end }
a1 = Concurrent::ErlangActor.spawn(type, &body1.fetch(type))
a2 = Concurrent::ErlangActor.spawn(type, &body_receive_test_linked.fetch(type))
a1.tell a2
expect(a1.terminated.value!).to be_truthy
expect(a2.terminated.value!).to be_truthy
end
specify 'unlinks' do
body1 = { on_thread:
-> do
actor = receive
link actor
unlink actor
linked = linked? actor
actor.tell pid
linked
end,
on_pool:
-> do
receive do |actor|
link actor
unlink actor
linked = linked? actor
actor.tell pid
linked
end
end }
a1 = Concurrent::ErlangActor.spawn(type, &body1.fetch(type))
a2 = Concurrent::ErlangActor.spawn(type, &body_receive_test_linked.fetch(type))
a1.tell a2
expect(a1.terminated.value!).to be_falsey
expect(a2.terminated.value!).to be_falsey
end
specify 'link dead' do
a = Concurrent::ErlangActor.spawn(type) do
b = spawn { :done }
b.terminated.wait
link b
end
expect { a.terminated.value! }.to raise_error Concurrent::ErlangActor::NoActor
end
specify 'link dead when trapping' do
body1 = { on_thread:
-> do
b = spawn { :done }
b.terminated.wait
sleep 0.01
trap
link b
[b, receive]
end,
on_pool:
-> do
b = spawn { :done }
b.terminated.wait
sleep 0.01
trap
link b
receive { |v| [b, v] }
end }
a = Concurrent::ErlangActor.spawn(type, &body1.fetch(type))
b, captured = a.terminated.value!
expect(captured).to eq Concurrent::ErlangActor::Exit.new(b, Concurrent::ErlangActor::NoActor.new(b))
end
describe 'exit/1 when linked' do
# https://learnyousomeerlang.com/errors-and-processes#links
specify 1 do
body = { on_thread:
-> do
b = spawn(link: true) { :ok }
[receive(timeout: 0.01), b]
end,
on_pool:
-> do
b = spawn(link: true) { :ok }
receive(on(ANY) { |v| [v, b] },
on(TIMEOUT) { |v| [nil, b] },
timeout: 0.01)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
message, b = a.terminated.value!
expect(message).to eq nil
expect(b.terminated.value!).to eq :ok
end
specify 2 do
body = { on_thread:
-> do
b = spawn(link: true) { :ok }
trap
[receive, b]
end,
on_pool:
-> do
b = spawn(link: true) { :ok }
trap
receive(on(ANY) { |v| [v, b] },
on(TIMEOUT) { |v| [nil, b] },
timeout: 0.01)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
message, b = a.terminated.value!
expect(message).to eq Concurrent::ErlangActor::Exit.new(b, :normal)
expect(b.terminated.value!).to eq :ok
end
specify 3 do
body = { on_thread:
-> do
spawn(link: true) { terminate :boom }
receive(timeout: 0.01)
end,
on_pool:
-> do
spawn(link: true) { terminate :boom }
receive(on(ANY) { |v| [v, b] },
on(TIMEOUT) { |v| [nil, b] },
timeout: 0.01)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
expect(a.terminated.reason).to eq :boom
end
specify 4 do
body = { on_thread:
-> do
b = spawn(link: true) { terminate :boom }
trap
[receive(timeout: 0.01), b]
end,
on_pool:
-> do
b = spawn(link: true) { terminate :boom }
trap
receive(on(ANY) { |v| [v, b] },
on(TIMEOUT) { |v| [nil, b] },
timeout: 0.01)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
trapped_exit, b = a.terminated.value!
expect(trapped_exit).to eq Concurrent::ErlangActor::Exit.new(b, :boom)
expect(b.terminated.reason).to eq :boom
end
specify 5 do
body = { on_thread:
-> do
b = spawn(link: true) { terminate :normal, value: :ok }
[receive(timeout: 0.01), b]
end,
on_pool:
-> do
b = spawn(link: true) { terminate :normal, value: :ok }
receive(on(ANY) { |v| [v, b] },
on(TIMEOUT) { |v| [nil, b] },
timeout: 0.01)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
message, b = a.terminated.value!
expect(message).to eq nil
expect(b.terminated.value!).to eq :ok
end
specify 6 do
body = { on_thread:
-> do
b = spawn(link: true) { terminate :normal, value: :ok }
trap
[receive, b]
end,
on_pool:
-> do
b = spawn(link: true) { terminate :normal, value: :ok }
trap
receive(on(ANY) { |v| [v, b] },
on(TIMEOUT) { |v| [nil, b] },
timeout: 0.01)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
message, b = a.terminated.value!
expect(message).to eq Concurrent::ErlangActor::Exit.new(b, :normal)
expect(b.terminated.value!).to eq :ok
end
specify 7 do
body = { on_thread:
-> do
spawn(link: true) { raise 'err' }
receive(timeout: 0.01)
end,
on_pool:
-> do
spawn(link: true) { raise 'err' }
receive(on(ANY) { |v| [v, b] },
on(TIMEOUT) { |v| [nil, b] },
timeout: 0.01)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
expect { a.terminated.value! }.to raise_error(RuntimeError, 'err')
end
specify 8 do
body = { on_thread:
-> do
b = spawn(link: true) { raise 'err' }
trap
[receive(timeout: 0.01), b]
end,
on_pool:
-> do
b = spawn(link: true) { raise 'err' }
trap
receive(on(ANY) { |v| [v, b] },
on(TIMEOUT) { |v| [nil, b] },
timeout: 0.01)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
trapped_exit, b = a.terminated.value!
expect(trapped_exit).to be_a Concurrent::ErlangActor::Exit
expect(trapped_exit.from).to eq b
expect(trapped_exit.reason).to eq b.terminated.reason
expect(trapped_exit.reason).to be_a RuntimeError
expect(trapped_exit.reason.message).to eq 'err'
end
specify 9 do
body = { on_thread:
-> do
b = spawn(link: true) { throw :uncaught }
trap
[receive(timeout: 0.01), b]
end,
on_pool:
-> do
b = spawn(link: true) { throw :uncaught }
trap
receive(on(ANY) { |v| [v, b] },
on(TIMEOUT) { |v| [nil, b] },
timeout: 0.01)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
trapped_exit, b = a.terminated.value!
expect(trapped_exit).to be_a Concurrent::ErlangActor::Exit
expect(trapped_exit.from).to eq b
expect(trapped_exit.reason).to eq b.terminated.reason
expect(trapped_exit.reason).to be_a UncaughtThrowError
expect(trapped_exit.reason.message).to eq 'uncaught throw :uncaught'
end
end
describe 'exit/2 when linked' do
# https://learnyousomeerlang.com/errors-and-processes#links
specify 1 do
body = { on_thread:
-> do
terminate pid, :normal # sends the signal to mailbox
# TODO (pitr-ch 17-Jan-2019): does erlang require receive to process signals?
receive(timeout: 0.01)
:continued
end,
on_pool:
-> do
terminate pid, :normal # sends the signal to mailbox
receive(on(ANY, :continued),
on(TIMEOUT, :timeout),
timeout: 1)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
expect(a.terminated.value!).to eq nil
end
specify 2 do
body = { on_thread:
-> do
terminate pid, :normal
trap
receive(timeout: 0.01)
end,
on_pool:
-> do
terminate pid, :normal
trap
receive(on(ANY, &identity), on(TIMEOUT, nil), timeout: 0.01)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
captured_exit = a.terminated.value!
expect(captured_exit).to eq Concurrent::ErlangActor::Exit.new(a, :normal)
end
specify 3 do
body = { on_thread:
-> do
b = spawn(link: true) { receive timeout: 0.01, timeout_value: :timeout }
terminate b, :normal
b
end,
on_pool:
-> do
b = spawn(link: true) do
receive(on(ANY, :not_happening),
on(TIMEOUT, :timeout),
timeout: 0.01)
end
terminate b, :normal
b
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
b = a.terminated.value!
expect(b.terminated.value!).to eq :timeout
end
specify 4 do
body = { on_thread:
-> do
b = spawn(link: true) { trap; receive timeout: 0.01, timeout_value: :timeout }
terminate b, :normal
b
end,
on_pool:
-> do
b = spawn(link: true) do
trap
receive(on(ANY, &identity),
on(TIMEOUT, :timeout),
timeout: 0.01)
end
terminate b, :normal
b
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
b = a.terminated.value!
expect(b.terminated.value!).to eq Concurrent::ErlangActor::Exit.new(a, :normal)
end
specify 5 do
body = { on_thread:
-> do
b = spawn(link: true) { receive timeout: 0.01; terminate :continued }
terminate b, :normal
trap
[b, receive(timeout: 0.02)]
end,
on_pool:
-> do
b = spawn(link: true) do
receive(on(ANY, :not_happening),
on(TIMEOUT) { terminate :continued },
timeout: 0.01)
end
terminate b, :normal
trap
receive(on(ANY) { |v| [b, v] }, on(TIMEOUT, :timeout), timeout: 1)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
b, captured = a.terminated.value!
expect(b.terminated.reason).to eq :continued
# normal is never send from b to a back
expect(captured).to eq Concurrent::ErlangActor::Exit.new(b, :continued)
end
specify 6 do
body = { on_thread:
-> do
b = spawn(link: true) { receive timeout: 1; :done }
terminate b, :remote_err
receive timeout: 1
end,
on_pool:
-> do
b = spawn(link: true) { receive(on(ANY, :done), on(TIMEOUT, :timeout), timeout: 1) }
terminate b, :remote_err
receive(on(ANY) { |v| [b, v] },
on(TIMEOUT, :timeout),
timeout: 1)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
a.terminated.wait
expect(a.terminated.reason).to eq :remote_err
end
specify 7 do
body = { on_thread:
-> do
b = spawn(link: true) { receive timeout: 1; :done }
terminate b, :remote_err
trap
[b, receive(timeout: 1)]
end,
on_pool:
-> do
b = spawn(link: true) { receive(on(ANY, :done), on(TIMEOUT, :timeout), timeout: 1) }
terminate b, :remote_err
trap
receive(on(ANY) { |v| [b, v] },
on(TIMEOUT, :timeout),
timeout: 1)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
b, captured = a.terminated.value!
expect(b.terminated.reason).to eq :remote_err
expect(captured.reason).to eq :remote_err
end
specify 8 do
body = { on_thread:
-> do
b = spawn(link: true) { receive timeout: 1; :done }
terminate b, :kill
receive timeout: 1
end,
on_pool:
-> do
b = spawn(link: true) { receive(on(ANY, :done), on(TIMEOUT, :done), timeout: 1) }
terminate b, :kill
receive(on(ANY, &identity), on(TIMEOUT, :timeout), timeout: 1)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
expect(a.terminated.reason).to eq :killed
end
specify 9 do
body = { on_thread:
-> do
b = spawn(link: true) { receive timeout: 0.01; :done }
terminate b, :kill
trap
[b, receive(timeout: 0.01)]
end,
on_pool:
-> do
b = spawn(link: true) { receive(on(ANY, :done), on(TIMEOUT, :done), timeout: 1) }
terminate b, :kill
trap
receive(on(ANY) { |v| [b, v] }, on(TIMEOUT, :timeout), timeout: 1)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
b, captured = a.terminated.value!
expect(b.terminated.reason).to eq :killed
expect(captured.reason).to eq :killed
end
specify 10 do
body = { on_thread:
-> do
terminate pid, :kill
receive timeout: 0.01
end,
on_pool:
-> do
terminate pid, :kill
receive(on(ANY, :continued), on(TIMEOUT, :timeout), timeout: 1)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
expect(a.terminated.reason).to eq :killed
end
specify 11 do
body = { on_thread:
-> do
terminate pid, :kill
trap
receive timeout: 0.01
end,
on_pool:
-> do
terminate pid, :kill
trap
receive(on(ANY, &identity), on(TIMEOUT, :timeout), timeout: 1)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
expect(a.terminated.reason).to eq :killed
end
# explained in
# http://erlang.org/pipermail/erlang-questions/2009-October/047241.html
specify 12 do
body = { on_thread:
-> do
spawn(link: true) { terminate :kill }
receive timeout: 1
end,
on_pool:
-> do
spawn(link: true) { terminate :kill }
receive(on(ANY, :continued),
on(TIMEOUT, :timeout),
timeout: 1)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
expect(a.terminated.reason).to eq :kill
end
specify 13 do
body = { on_thread:
-> do
b = spawn(link: true) { terminate :kill }
trap
[b, receive(timeout: 1)]
end,
on_pool:
-> do
b = spawn(link: true) { terminate :kill }
trap
receive(on(ANY) { |v| [b, v] },
on(TIMEOUT, :timeout),
timeout: 1)
end }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
b, captured = a.terminated.value!
expect(b.terminated.reason).to eq :kill
expect(captured).to eq Concurrent::ErlangActor::Exit.new(b, :kill)
end
end
end
specify 'spawn(link: true)' do
a = Concurrent::ErlangActor.spawn(type) do
b = spawn(link: true) { :v }
linked? b
end
expect(a.terminated.value!).to be_truthy
a = Concurrent::ErlangActor.spawn(type) do
b = spawn { :v }
linked? b
end
expect(a.terminated.value!).to be_falsey
end
specify 'termination' do
a = Concurrent::ErlangActor.spawn(type) { :v }
expect(a.terminated.value!).to eq :v
a = Concurrent::ErlangActor.spawn(type) { raise 'err' }
expect { a.terminated.value! }.to raise_error(RuntimeError, 'err')
a = Concurrent::ErlangActor.spawn(type) { terminate :normal, value: :val }
expect(a.terminated.value!).to eq :val
a = Concurrent::ErlangActor.spawn(type) { terminate :er }
expect(a.terminated.reason).to eq :er
end
describe 'asking' do
specify "replies" do
body = { on_thread: -> { reply receive },
on_pool: -> { receive { |v| reply v } } }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
expect(a.ask(:v)).to eq :v
body = { on_thread: -> { v = receive; reply v; reply v; },
on_pool: -> { receive { |v| reply v; reply v } } }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
expect(a.ask(:v)).to eq :v
expect(a.terminated.reason).to be_a_kind_of Concurrent::MultipleAssignmentError
body = { on_thread: -> { v = receive; reply v; reply_resolution true, v, nil, false },
on_pool: -> { receive { |v| reply v; reply_resolution true, v, nil, false } } }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
expect(a.ask(:v)).to eq :v
expect(a.terminated.value!).to be_falsey
body = { on_thread: -> { reply_resolution false, nil, receive },
on_pool: -> { receive { |v| reply_resolution false, nil, v } } }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
expect { a.ask(:err) }.to raise_error StandardError, 'err'
body = { on_thread: -> { reply_resolution false, nil, receive },
on_pool: -> { receive { |v| reply_resolution false, nil, v } } }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
expect(a.ask_op(:err).reason).to eq :err
end
specify "rejects on no reply" do
body = { on_thread: -> { receive; receive },
on_pool: -> { receive { receive {} } } }
a = Concurrent::ErlangActor.spawn(type, &body.fetch(type))
expect(a.ask_op(:v).reason).to eq Concurrent::ErlangActor::NoReply
expect { raise a.ask_op(:v).wait }.to raise_error Concurrent::ErlangActor::NoReply
end
end
end
describe 'on thread' do
let(:type) { :on_thread }
it_behaves_like 'erlang actor'
end
describe 'event based' do
let(:type) { :on_pool }
it_behaves_like 'erlang actor'
specify "receives message repeatedly with keep" do
actor = Concurrent::ErlangActor.spawn(:on_pool) do
receive on(ANY) { |v| v == :done ? terminate(:normal, value: 42) : reply(v) },
keep: true
end
expect(actor.ask(1)).to eq 1
expect(actor.ask(2)).to eq 2
actor.tell :done
expect(actor.terminated.value!).to eq 42
end
specify "class defined" do
definition_module = Module.new do
def start
@sum = 0
receive on(Numeric, &method(:count)),
on(:done, &method(:stop)),
on(TIMEOUT, &method(:fail)),
keep: true,
timeout: 0.1
end
def count(message)
reply @sum += message
end
def stop(_message)
terminate :normal, value: @sum
end
def fail(_message)
terminate :timeout
end
end
definition_class = Class.new Concurrent::ErlangActor::Environment do
include definition_module
end
actor = Concurrent::ErlangActor.spawn(:on_pool, environment: definition_class) { start }
actor.tell 1
expect(actor.ask(2)).to eq 3
actor.tell :done
expect(actor.terminated.value!).to eq 3
actor = Concurrent::ErlangActor.spawn(:on_pool, environment: definition_module)
actor.tell 1
expect(actor.ask(2)).to eq 3
expect(actor.terminated.reason).to eq :timeout
end
end
end
end

View File

@ -692,6 +692,17 @@ RSpec.describe 'Concurrent::Promises' do
value!).to eq 6
end
it 'with erlang actor' do
actor = Concurrent::ErlangActor.spawn :on_thread do
reply receive * 2
end
expect(future { 2 }.
then_ask(actor).
then { |v| v + 2 }.
value!).to eq 6
end
it 'with channel' do
ch1 = Concurrent::Promises::Channel.new
ch2 = Concurrent::Promises::Channel.new

View File

@ -4,7 +4,8 @@ module YARD
module Templates::Helpers
# make sure the signatures are complete not simplified with ...
# make sure the signatures are complete not simplified with
# '...' and '?' instead of nil
module HtmlHelper
def signature_types(meth, link = true)
meth = convert_method_to_overload(meth)