Skip to content

Commit 010e9f2

Browse files
committed
ErlangActor improvements
* Allow actors to accept initial arguments * Separate Down into message and signal * Rename Exit signal to Terminate * Make sure monitor and link send after message which terminates the actor will never return anything other then NoActor * cleanup messages and signals
1 parent bb99e88 commit 010e9f2

File tree

4 files changed

+426
-312
lines changed

4 files changed

+426
-312
lines changed

docs-source/erlang_actor.in.md

Lines changed: 101 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,25 @@ The simplest example is to use the actor as an asynchronous execution.
44
Although, `Promises.future { 1 + 1 }` is better suited for that purpose.
55

66
```ruby
7-
actor = Concurrent::ErlangActor.spawn(:on_thread, name: 'addition') { 1 + 1 }
7+
actor = Concurrent::ErlangActor.spawn(type: :on_thread, name: 'addition') { 1 + 1 }
88
actor.terminated.value!
99
```
1010

1111
Let's send some messages and maintain some internal state
1212
which is what actors are good for.
1313

1414
```ruby
15-
actor = Concurrent::ErlangActor.spawn(:on_thread, name: 'sum') do
15+
actor = Concurrent::ErlangActor.spawn(type: :on_thread, name: 'sum') do
1616
sum = 0 # internal state
1717
# receive and sum the messages until the actor gets :done
1818
while true
1919
message = receive
2020
break if message == :done
2121
# if the message is asked and not only told,
22-
# reply with a current sum
22+
# reply with the current sum (has no effect if actor was not asked)
2323
reply sum += message
2424
end
25+
# The final value of the actor
2526
sum
2627
end
2728
```
@@ -36,15 +37,74 @@ actor.tell(1).tell(1)
3637
actor.ask 10
3738
# stop the actor
3839
actor.tell :done
40+
# The final value of the actor
3941
actor.terminated.value!
4042
```
4143

42-
### Receiving
44+
### Actor types
45+
46+
There are two types of actors.
47+
The type is specified when calling spawn as a first argument,
48+
`Concurrent::ErlangActor.spawn(type: :on_thread, ...` or
49+
`Concurrent::ErlangActor.spawn(type: :on_pool, ...`.
50+
51+
The main difference is in how receive method returns.
52+
53+
- `:on_thread` it blocks the thread until message is available,
54+
then it returns or calls the provided block first.
55+
56+
- However, `:on_pool` it has to free up the thread on the receive
57+
call back to the pool. Therefore the call to receive ends the
58+
execution of current scope. The receive has to be given block
59+
or blocks that act as a continuations and are called
60+
when there is message available.
61+
62+
Let's have a look at how the bodies of actors differ between the types:
63+
64+
```ruby
65+
ping = Concurrent::ErlangActor.spawn(type: :on_thread) { reply receive }
66+
ping.ask 42
67+
```
68+
69+
It first calls receive, which blocks the thread of the actor.
70+
When it returns the received message is passed an an argument to reply,
71+
which replies the same value back to the ask method.
72+
Then the actor terminates normally, because there is nothing else to do.
73+
74+
However when running on pool a block with code which should be evaluated
75+
after the message is received has to be provided.
76+
77+
```ruby
78+
ping = Concurrent::ErlangActor.spawn(type: :on_pool) { receive { |m| reply m } }
79+
ping.ask 42
80+
```
81+
82+
It starts by calling receive which will remember the given block for later
83+
execution when a message is available and stops executing the current scope.
84+
Later when a message becomes available the previously provided block is given
85+
the message and called. The result of the block is the final value of the
86+
normally terminated actor.
87+
88+
The direct blocking style of `:on_thread` is simpler to write and more straight
89+
forward however it has limitations. Each `:on_thread` actor creates a Thread
90+
taking time and resources.
91+
There is also a limited number of threads the Ruby process can create
92+
so you may hit the limit and fail to create more threads and therefore actors.
93+
94+
Since the `:on_pool` actor runs on a poll of threads, its creations
95+
is faster and cheaper and it does not create new threads.
96+
Therefore there is no limit (only RAM) on how many actors can be created.
97+
98+
To simplify, if you need only few actors `:on_thread` is fine.
99+
However if you will be creating hundreds of actors or
100+
they will be short-lived `:on_pool` should be used.
101+
102+
### Receiving messages
43103

44104
Simplest message receive.
45105

46106
```ruby
47-
actor = Concurrent::ErlangActor.spawn(:on_thread) { receive }
107+
actor = Concurrent::ErlangActor.spawn(type: :on_thread) { receive }
48108
actor.tell :m
49109
actor.terminated.value!
50110
```
@@ -53,9 +113,9 @@ which also works for actor on pool,
53113
because if no block is given it will use a default block `{ |v| v }`
54114

55115
```ruby
56-
actor = Concurrent::ErlangActor.spawn(:on_pool) { receive { |v| v } }
116+
actor = Concurrent::ErlangActor.spawn(type: :on_pool) { receive { |v| v } }
57117
# can simply be following
58-
actor = Concurrent::ErlangActor.spawn(:on_pool) { receive }
118+
actor = Concurrent::ErlangActor.spawn(type: :on_pool) { receive }
59119
actor.tell :m
60120
actor.terminated.value!
61121
```
@@ -64,7 +124,7 @@ The received message type can be limited.
64124

65125
```ruby
66126
Concurrent::ErlangActor.
67-
spawn(:on_thread) { receive(Numeric).succ }.
127+
spawn(type: :on_thread) { receive(Numeric).succ }.
68128
tell('junk'). # ignored message
69129
tell(42).
70130
terminated.value!
@@ -74,7 +134,7 @@ On pool it requires a block.
74134

75135
```ruby
76136
Concurrent::ErlangActor.
77-
spawn(:on_pool) { receive(Numeric) { |v| v.succ } }.
137+
spawn(type: :on_pool) { receive(Numeric) { |v| v.succ } }.
78138
tell('junk'). # ignored message
79139
tell(42).
80140
terminated.value!
@@ -85,7 +145,7 @@ as well.
85145

86146
```ruby
87147
Concurrent::ErlangActor.
88-
spawn(:on_thread) { receive(Numeric) { |v| v.succ } }.
148+
spawn(type: :on_thread) { receive(Numeric) { |v| v.succ } }.
89149
tell('junk'). # ignored message
90150
tell(42).
91151
terminated.value!
@@ -94,7 +154,7 @@ Concurrent::ErlangActor.
94154
The `receive` method can be also used to dispatch based on the received message.
95155

96156
```ruby
97-
actor = Concurrent::ErlangActor.spawn(:on_thread) do
157+
actor = Concurrent::ErlangActor.spawn(type: :on_thread) do
98158
while true
99159
receive(on(Symbol) { |s| reply s.to_s },
100160
on(And[Numeric, -> v { v >= 0 }]) { |v| reply v.succ },
@@ -137,7 +197,7 @@ module Behaviour
137197
end
138198
end
139199

140-
actor = Concurrent::ErlangActor.spawn(:on_pool, environment: Behaviour) { body }
200+
actor = Concurrent::ErlangActor.spawn(type: :on_pool, environment: Behaviour) { body }
141201
actor.ask 1
142202
actor.ask 2
143203
actor.ask :value
@@ -153,7 +213,7 @@ that will keep the receive rules until another receive is called
153213
replacing the kept rules.
154214

155215
```ruby
156-
actor = Concurrent::ErlangActor.spawn(:on_pool) do
216+
actor = Concurrent::ErlangActor.spawn(type: :on_pool) do
157217
receive(on(Symbol) { |s| reply s.to_s },
158218
on(And[Numeric, -> v { v >= 0 }]) { |v| reply v.succ },
159219
# put last works as else
@@ -173,71 +233,13 @@ actor.ask "junk" rescue $!
173233
actor.terminated.result
174234
```
175235

176-
### Actor types
177-
178-
There are two types of actors.
179-
The type is specified when calling spawn as a first argument,
180-
`Concurrent::ErlangActor.spawn(:on_thread, ...` or
181-
`Concurrent::ErlangActor.spawn(:on_pool, ...`.
182-
183-
The main difference is in how receive method returns.
184-
185-
- `:on_thread` it blocks the thread until message is available,
186-
then it returns or calls the provided block first.
187-
188-
- However, `:on_pool` it has to free up the thread on the receive
189-
call back to the pool. Therefore the call to receive ends the
190-
execution of current scope. The receive has to be given block
191-
or blocks that act as a continuations and are called
192-
when there is message available.
193-
194-
Let's have a look at how the bodies of actors differ between the types:
195-
196-
```ruby
197-
ping = Concurrent::ErlangActor.spawn(:on_thread) { reply receive }
198-
ping.ask 42
199-
```
200-
201-
It first calls receive, which blocks the thread of the actor.
202-
When it returns the received message is passed an an argument to reply,
203-
which replies the same value back to the ask method.
204-
Then the actor terminates normally, because there is nothing else to do.
205-
206-
However when running on pool a block with code which should be evaluated
207-
after the message is received has to be provided.
208-
209-
```ruby
210-
ping = Concurrent::ErlangActor.spawn(:on_pool) { receive { |m| reply m } }
211-
ping.ask 42
212-
```
213-
214-
It starts by calling receive which will remember the given block for later
215-
execution when a message is available and stops executing the current scope.
216-
Later when a message becomes available the previously provided block is given
217-
the message and called. The result of the block is the final value of the
218-
normally terminated actor.
219-
220-
The direct blocking style of `:on_thread` is simpler to write and more straight
221-
forward however it has limitations. Each `:on_thread` actor creates a Thread
222-
taking time and resources.
223-
There is also a limited number of threads the Ruby process can create
224-
so you may hit the limit and fail to create more threads and therefore actors.
225-
226-
Since the `:on_pool` actor runs on a poll of threads, its creations
227-
is faster and cheaper and it does not create new threads.
228-
Therefore there is no limit (only RAM) on how many actors can be created.
229-
230-
To simplify, if you need only few actors `:on_thread` is fine.
231-
However if you will be creating hundreds of actors or
232-
they will be short-lived `:on_pool` should be used.
233-
234236
### Erlang behaviour
235237

236238
The actor matches Erlang processes in behaviour.
237239
Therefore it supports the usual Erlang actor linking, monitoring, exit behaviour, etc.
238240

239241
```ruby
240-
actor = Concurrent::ErlangActor.spawn(:on_thread) do
242+
actor = Concurrent::ErlangActor.spawn(type: :on_thread) do
241243
spawn(link: true) do # equivalent of spawn_link in Erlang
242244
terminate :err # equivalent of exit in Erlang
243245
end
@@ -247,13 +249,30 @@ end
247249
actor.terminated.value!
248250
```
249251

250-
### TODO
251-
252-
* receives
253-
* More erlang behaviour examples
254-
* Back pressure with bounded mailbox
255-
* _op methods
256-
* types of actors
257-
* always use timeout
258-
* drop and log unrecognized messages, or just terminate
259-
* Functions module
252+
The methods have same or very similar name to be easily found.
253+
The one exception from the original Erlang naming is exit.
254+
To avoid clashing with `Kernel#exit` it's called `terminate`.
255+
256+
Until there is more information available here, the chapters listed below from
257+
a book [lern you some Erlang](https://learnyousomeerlang.com)
258+
are excellent source of information.
259+
The Ruby ErlangActor implementation has same behaviour.
260+
261+
- [Links](https://learnyousomeerlang.com/errors-and-processes#links)
262+
- [It's a trap](https://learnyousomeerlang.com/errors-and-processes#its-a-trap)
263+
- [Monitors](https://learnyousomeerlang.com/errors-and-processes#monitors)
264+
265+
If anything behaves differently than in Erlang, please file an issue.
266+
267+
### Chapters or points to be added
268+
269+
* More erlang behaviour examples.
270+
* The mailbox can be bounded in size,
271+
then the tell and ask will block until there is space available in the mailbox.
272+
Useful for building systems with backpressure.
273+
* `#tell_op` and `ask_op` method examples, integration with promises.
274+
* Best practice: always use timeout,
275+
and do something if the message does not arrive, don't leave the actor stuck.
276+
* Best practice: drop and log unrecognized messages,
277+
or be even more defensive and terminate.
278+
* Environment definition for actors.

0 commit comments

Comments
 (0)