standard-procedure-plumbing 0.4.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +74 -52
- data/lib/plumbing/actor/async.rb +12 -1
- data/lib/plumbing/actor/inline.rb +13 -2
- data/lib/plumbing/actor/kernel.rb +2 -1
- data/lib/plumbing/actor/threaded.rb +22 -8
- data/lib/plumbing/actor.rb +33 -3
- data/lib/plumbing/junction.rb +4 -3
- data/lib/plumbing/pipe.rb +1 -0
- data/lib/plumbing/version.rb +1 -1
- data/spec/become_equal_to_matcher.rb +3 -2
- data/spec/examples/actor_spec.rb +31 -26
- data/spec/examples/await_spec.rb +43 -0
- data/spec/examples/pipe_spec.rb +42 -6
- data/spec/plumbing/a_pipe.rb +4 -2
- data/spec/plumbing/actor/transporter_spec.rb +1 -0
- data/spec/plumbing/actor_spec.rb +125 -23
- data/spec/plumbing/custom_filter_spec.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ace6c01658b88c08c34d48406f983fdad449bc4f122b8dfed026853841b48d35
|
4
|
+
data.tar.gz: 96b4458d0c667e10bb47fb8881de2e21df0ff40243cf07ff30e2e22f29892625
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a5ee7af05314dbb45d9f46a302f6102f93237334224f82f1c34cdd5c252829039bf6b4bc18a562fcdb5d76a8ca229a484b0f3d3e123b7e5efd46d302f50c72b4
|
7
|
+
data.tar.gz: 005f6cee1eb899207659955ea5a4179133c65e07db913e8f3f9c730cfca8947684927c9b99b675e9f54dc4ea4820159d709d61b146113a993783cec2a0c5842c
|
data/README.md
CHANGED
@@ -12,7 +12,7 @@ By default it is `:inline`, so every command or query is handled synchronously.
|
|
12
12
|
|
13
13
|
`:threaded` mode handles tasks using a thread pool via [Concurrent Ruby](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Promises.html)). Your code should include the "concurrent-ruby" gem in its bundle, as Plumbing does not load it by default.
|
14
14
|
|
15
|
-
However, `:threaded` mode is not safe for Ruby on Rails applications. In this case, use `:
|
15
|
+
However, `:threaded` mode is not safe for Ruby on Rails applications. In this case, use `:threaded_rails` mode, which is identical to `:threaded`, except it wraps the tasks in the Rails executor. This ensures your actors do not interfere with the Rails framework. Note that the Concurrent Ruby's default `:io` scheduler will create extra threads at times of high demand, which may put pressure on the ActiveRecord database connection pool. A future version of plumbing will allow the thread pool to be adjusted with a maximum number of threads, preventing contention with the connection pool.
|
16
16
|
|
17
17
|
The `timeout` setting is used when performing queries - it defaults to 30s.
|
18
18
|
|
@@ -165,40 +165,82 @@ Actors are different. Conceptually, each actor has it's own thread of execution
|
|
165
165
|
|
166
166
|
This means each actor is only ever accessed by a single thread and the vast majority of concurrency issues are eliminated.
|
167
167
|
|
168
|
-
[Plumbing::Actor](/lib/plumbing/actor.rb) allows you to define the `async` public interface to your objects. Calling `.start` builds a proxy to the actual instance of your object and ensures that any messages sent are handled in a manner appropriate to the current mode - immediately for inline mode, using fibers for async mode and using threads for threaded and
|
168
|
+
[Plumbing::Actor](/lib/plumbing/actor.rb) allows you to define the `async` public interface to your objects. Calling `.start` builds a proxy to the actual instance of your object and ensures that any messages sent are handled in a manner appropriate to the current mode - immediately for inline mode, using fibers for async mode and using threads for threaded and threaded_rails mode.
|
169
169
|
|
170
170
|
When sending messages to an actor, this just works.
|
171
171
|
|
172
|
-
However, as the caller, you do not have direct access to the return values of the messages that you send. Instead, you must call `#
|
172
|
+
However, as the caller, you do not have direct access to the return values of the messages that you send. Instead, you must call `#value` - or alternatively, wrap your call in `await { ... }`. The block form of `await` is added in to ruby's `Kernel` so it is available everywhere. It is also safe to use with non-actors (in which case it just returns the original value from the block).
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
@actor = MyActor.start name: "Alice"
|
176
|
+
|
177
|
+
@actor.name.value
|
178
|
+
# => "Alice"
|
179
|
+
|
180
|
+
await { @actor.name }
|
181
|
+
# => "Alice"
|
182
|
+
|
183
|
+
await { "Bob" }
|
184
|
+
# => "Bob"
|
185
|
+
```
|
186
|
+
|
187
|
+
This then makes the caller's thread block until the receiver's thread has finished its work and returned a value. Or if the receiver raises an exception, that exception is then re-raised in the calling thread.
|
188
|
+
|
189
|
+
The actor model does not eliminate every possible concurrency issue. If you use `value` or `await`, it is possible to deadlock yourself.
|
173
190
|
|
174
|
-
The actor model does not eliminate every possible concurrency issue. If you use `await`, it is possible to deadlock yourself.
|
175
191
|
Actor A, running in Thread 1, sends a message to Actor B and then awaits the result, meaning Thread 1 is blocked. Actor B, running in Thread 2, starts to work, but needs to ask Actor A a question. So it sends a message to Actor A and awaits the result. Thread 2 is now blocked, waiting for Actor A to respond. But Actor A, running in Thread 1, is blocked, waiting for Actor B to respond.
|
176
192
|
|
177
|
-
This potential deadlock only occurs if you use `await` and have actors that call back in to each other. If your objects are strictly layered, or you never use `await` (perhaps, instead using a Pipe to observe events), then this particular deadlock should not occur. However, just in case, every call to `
|
193
|
+
This potential deadlock only occurs if you use `value` or `await` and have actors that call back in to each other. If your objects are strictly layered, or you never use `value` or `await` (perhaps, instead using a Pipe to observe events), then this particular deadlock should not occur. However, just in case, every call to `value` has a timeout defaulting to 30s.
|
178
194
|
|
179
195
|
### Inline actors
|
180
196
|
|
181
|
-
Even though inline mode is not asynchronous, you must still use `await` to access the results from another actor. However, as deadlocks are impossible in a single thread, there is no timeout.
|
197
|
+
Even though inline mode is not asynchronous, you must still use `value` or `await` to access the results from another actor. However, as deadlocks are impossible in a single thread, there is no timeout.
|
182
198
|
|
183
199
|
### Async actors
|
184
200
|
|
185
201
|
Using async mode is probably the easiest way to add concurrency to your application. It uses fibers to allow for "concurrency but not parallelism" - that is execution will happen in the background but your objects or data will never be accessed by two things at the exact same time.
|
186
202
|
|
187
|
-
### Threaded
|
203
|
+
### Threaded actors
|
188
204
|
|
189
|
-
Using threaded (or
|
205
|
+
Using threaded (or threaded_rails) mode gives you concurrency and parallelism. If all your public objects are actors and you are careful about callbacks then the actor model will keep your code safe. But there are a couple of extra things to consider.
|
190
206
|
|
191
207
|
Firstly, when you pass parameters or return results between threads, those objects are "transported" across the boundaries.
|
192
|
-
Most objects are
|
193
|
-
|
208
|
+
Most objects are cloned. Hashes, keyword arguments and arrays have their contents recursively transported. And any object that uses `GlobalID::Identification` (for example, ActiveRecord models) are marshalled into a GlobalID, then unmarshalled back in to their original object. This is to prevent the same object from being amended in both the caller and receiver's threads.
|
209
|
+
|
210
|
+
Secondly, when you pass a block (or Proc parameter) to another actor, the block/proc will be executed in the receiver's thread. This means you must not access any variables that would normally be in scope for your block (whether local variables or instance variables of other objects - see note below) This is because you will be accessing them from a different thread to where they were defined, leading to potential race conditions. And, if you access any actors, you must not use `value` or `await` or you risk a deadlock. If you are within an actor and need to pass a block or proc parameter, you should use the `safely` method to ensure that your block is run within the context of the calling actor, not the receiving actor.
|
211
|
+
|
212
|
+
For example, when defining a custom filter, the filter adds itself as an observer to its source. The source triggers the `received` method on the filter, which will run in the context of the source. So the custom filter uses `safely` to move back into its own context and access its instance variables.
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
class EveryThirdEvent < Plumbing::CustomFilter
|
216
|
+
def initialize source:
|
217
|
+
super
|
218
|
+
@events = []
|
219
|
+
end
|
220
|
+
|
221
|
+
def received event
|
222
|
+
safely do
|
223
|
+
@events << event
|
224
|
+
if @events.count >= 3
|
225
|
+
@events.clear
|
226
|
+
self << event
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
```
|
232
|
+
|
233
|
+
(Note: we break that rule in the specs for Pipe objects - we use a block observer that sets the value on a local variable. That's because it is a controlled situation where we know there are only two threads involved and we are explicitly waiting for the second thread to complete. For almost every app that uses actors, there will be multiple threads and it will be impossible to predict the access patterns).
|
194
234
|
|
195
|
-
|
235
|
+
### Constructing actors
|
196
236
|
|
197
237
|
Instead of constructing your object with `.new`, use `.start`. This builds a proxy object that wraps the target instance and dispatches messages through a safe mechanism. Only messages that have been defined as part of the actor are available in this proxy - so you don't have to worry about callers bypassing the actor's internal context.
|
198
238
|
|
199
|
-
|
239
|
+
### Referencing actors
|
200
240
|
|
201
|
-
|
241
|
+
If you're within a method inside your actor and you want to pass a reference to yourself, instead of using `self`, you should use `proxy` (which is also aliased as `as_actor` or `async`).
|
242
|
+
|
243
|
+
Also be aware that if you use actors in one place, you need to use them everywhere - especially if you're using threads. This is because as the actor sends messages to its collaborators, those calls will be made from within the actor's internal context. If the collaborators are also actors, the subsequent messages will be handled correctly, if not, data consistency bugs could occur. This does not mean that every class needs to be an actor, just your "public API" classes which may be accessed from multiple actors or other threads.
|
202
244
|
|
203
245
|
### Usage
|
204
246
|
|
@@ -208,17 +250,17 @@ Also be aware that if you use actors in one place, you need to use them everywhe
|
|
208
250
|
require "plumbing"
|
209
251
|
|
210
252
|
class Employee
|
211
|
-
attr_reader :name, :job_title
|
212
|
-
|
213
253
|
include Plumbing::Actor
|
214
|
-
|
215
|
-
|
254
|
+
async :name, :job_title, :greet_slowly, :promote
|
255
|
+
attr_reader :name, :job_title
|
216
256
|
|
217
257
|
def initialize(name)
|
218
258
|
@name = name
|
219
259
|
@job_title = "Sales assistant"
|
220
260
|
end
|
221
261
|
|
262
|
+
private
|
263
|
+
|
222
264
|
def promote
|
223
265
|
sleep 0.5
|
224
266
|
@job_title = "Sales manager"
|
@@ -237,16 +279,16 @@ Also be aware that if you use actors in one place, you need to use them everywhe
|
|
237
279
|
await { @person.job_title }
|
238
280
|
# => "Sales assistant"
|
239
281
|
|
240
|
-
#
|
282
|
+
# by using `await`, we will block until `greet_slowly` has returned a value
|
241
283
|
await { @person.greet_slowly }
|
242
284
|
# => "H E L L O"
|
243
285
|
|
244
|
-
# we're not awaiting the result, so this will run in the background (unless we're using inline mode)
|
286
|
+
# this time, we're not awaiting the result, so this will run in the background (unless we're using inline mode)
|
245
287
|
@person.greet_slowly
|
246
288
|
|
247
|
-
#
|
289
|
+
# this will run in the background
|
248
290
|
@person.promote
|
249
|
-
# this will block
|
291
|
+
# this will block - it will not return until the previous calls, #greet_slowly, #promote, and this call to #job_title have completed
|
250
292
|
await { @person.job_title }
|
251
293
|
# => "Sales manager"
|
252
294
|
```
|
@@ -302,12 +344,17 @@ Pipes are implemented as actors, meaning that event notifications can be dispatc
|
|
302
344
|
end
|
303
345
|
|
304
346
|
def received event
|
305
|
-
#
|
306
|
-
|
307
|
-
#
|
308
|
-
|
309
|
-
|
310
|
-
|
347
|
+
# #received is called in the context of the `source` actor
|
348
|
+
# in order to safely access the `EveryThirdEvent` instance variables
|
349
|
+
# we need to move into the context of our own actor
|
350
|
+
safely do
|
351
|
+
# store this event into our buffer
|
352
|
+
@events << event
|
353
|
+
# if this is the third event we've received then clear the buffer and broadcast the latest event
|
354
|
+
if @events.count >= 3
|
355
|
+
@events.clear
|
356
|
+
self << event
|
357
|
+
end
|
311
358
|
end
|
312
359
|
end
|
313
360
|
end
|
@@ -344,31 +391,6 @@ Pipes are implemented as actors, meaning that event notifications can be dispatc
|
|
344
391
|
@second_source.notify "two"
|
345
392
|
# => "two"
|
346
393
|
```
|
347
|
-
|
348
|
-
[Dispatching events asynchronously (using Fibers)](/spec/examples/pipe_spec.rb):
|
349
|
-
```ruby
|
350
|
-
require "plumbing"
|
351
|
-
require "async"
|
352
|
-
|
353
|
-
Plumbing.configure mode: :async
|
354
|
-
|
355
|
-
Sync do
|
356
|
-
@first_source = Plumbing::Pipe.start
|
357
|
-
@second_source = Plumbing::Pipe.start
|
358
|
-
|
359
|
-
@junction = Plumbing::Junction.start @first_source, @second_source
|
360
|
-
|
361
|
-
@filter = Plumbing::Filter.start source: @junction do |event|
|
362
|
-
%w[one-one two-two].include? event.type
|
363
|
-
end
|
364
|
-
|
365
|
-
@first_source.notify "one-one"
|
366
|
-
@first_source.notify "one-two"
|
367
|
-
@second_source.notify "two-one"
|
368
|
-
@second_source.notify "two-two"
|
369
|
-
end
|
370
|
-
```
|
371
|
-
|
372
394
|
## Plumbing::RubberDuck - duck types and type-casts
|
373
395
|
|
374
396
|
Define an [interface or protocol](https://en.wikipedia.org/wiki/Interface_(object-oriented_programming)) specifying which messages you expect to be able to send.
|
data/lib/plumbing/actor/async.rb
CHANGED
@@ -21,8 +21,19 @@ module Plumbing
|
|
21
21
|
Result.new(task)
|
22
22
|
end
|
23
23
|
|
24
|
+
def safely(&)
|
25
|
+
send_message(:perform_safely, &)
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def within_actor? = true
|
30
|
+
|
31
|
+
def stop
|
32
|
+
# do nothing
|
33
|
+
end
|
34
|
+
|
24
35
|
Result = Data.define(:task) do
|
25
|
-
def
|
36
|
+
def value
|
26
37
|
Timeout.timeout(Plumbing::Actor.timeout) do
|
27
38
|
task.wait
|
28
39
|
end
|
@@ -13,8 +13,19 @@ module Plumbing
|
|
13
13
|
Result.new(ex)
|
14
14
|
end
|
15
15
|
|
16
|
-
|
17
|
-
|
16
|
+
def safely(&)
|
17
|
+
send_message(:perform_safely, &)
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def within_actor? = true
|
22
|
+
|
23
|
+
def stop
|
24
|
+
# do nothing
|
25
|
+
end
|
26
|
+
|
27
|
+
Result = Data.define(:result) do
|
28
|
+
def value = result.is_a?(Exception) ? raise(result) : result
|
18
29
|
end
|
19
30
|
private_constant :Result
|
20
31
|
end
|
@@ -12,28 +12,41 @@ module Plumbing
|
|
12
12
|
def initialize target
|
13
13
|
@target = target
|
14
14
|
@queue = Concurrent::Array.new
|
15
|
+
@mutex = Thread::Mutex.new
|
15
16
|
end
|
16
17
|
|
17
18
|
# Send the message to the target and wrap the result
|
18
19
|
def send_message message_name, *args, &block
|
19
20
|
Message.new(@target, message_name, Plumbing::Actor.transporter.marshal(*args), block, Concurrent::MVar.new).tap do |message|
|
20
|
-
@
|
21
|
-
|
21
|
+
@mutex.synchronize do
|
22
|
+
@queue << message
|
23
|
+
send_messages if @queue.any?
|
24
|
+
end
|
22
25
|
end
|
23
26
|
end
|
24
27
|
|
25
|
-
|
28
|
+
def safely(&)
|
29
|
+
send_message(:perform_safely, &)
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def within_actor? = @mutex.owned?
|
26
34
|
|
27
|
-
def
|
28
|
-
|
35
|
+
def stop
|
36
|
+
within_actor? ? @queue.clear : @mutex.synchronize { @queue.clear }
|
29
37
|
end
|
30
38
|
|
39
|
+
protected
|
40
|
+
|
41
|
+
def future(&) = Concurrent::Promises.future(&)
|
42
|
+
|
31
43
|
private
|
32
44
|
|
33
45
|
def send_messages
|
34
46
|
future do
|
35
|
-
|
36
|
-
message.
|
47
|
+
@mutex.synchronize do
|
48
|
+
message = @queue.shift
|
49
|
+
message&.call
|
37
50
|
end
|
38
51
|
end
|
39
52
|
end
|
@@ -42,12 +55,13 @@ module Plumbing
|
|
42
55
|
def call
|
43
56
|
args = Plumbing::Actor.transporter.unmarshal(*packed_args)
|
44
57
|
value = target.send message_name, *args, &unsafe_block
|
58
|
+
|
45
59
|
result.put Plumbing::Actor.transporter.marshal(value)
|
46
60
|
rescue => ex
|
47
61
|
result.put ex
|
48
62
|
end
|
49
63
|
|
50
|
-
def
|
64
|
+
def value
|
51
65
|
value = Plumbing::Actor.transporter.unmarshal(*result.take(Plumbing.config.timeout)).first
|
52
66
|
raise value if value.is_a? Exception
|
53
67
|
value
|
data/lib/plumbing/actor.rb
CHANGED
@@ -3,6 +3,15 @@ require_relative "actor/inline"
|
|
3
3
|
|
4
4
|
module Plumbing
|
5
5
|
module Actor
|
6
|
+
def safely(&)
|
7
|
+
proxy.safely(&)
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def within_actor? = proxy.within_actor?
|
12
|
+
|
13
|
+
def stop = proxy.stop
|
14
|
+
|
6
15
|
def self.included base
|
7
16
|
base.extend ClassMethods
|
8
17
|
end
|
@@ -10,8 +19,11 @@ module Plumbing
|
|
10
19
|
module ClassMethods
|
11
20
|
# Create a new actor instance and build a proxy for it using the current mode
|
12
21
|
# @return [Object] the proxy for the actor instance
|
13
|
-
def start(
|
14
|
-
|
22
|
+
def start(...)
|
23
|
+
instance = new(...)
|
24
|
+
build_proxy_for(instance).tap do |proxy|
|
25
|
+
instance.send :"proxy=", proxy
|
26
|
+
end
|
15
27
|
end
|
16
28
|
|
17
29
|
# Define the async messages that this actor can respond to
|
@@ -41,7 +53,7 @@ module Plumbing
|
|
41
53
|
inline: "Plumbing::Actor::Inline",
|
42
54
|
async: "Plumbing::Actor::Async",
|
43
55
|
threaded: "Plumbing::Actor::Threaded",
|
44
|
-
|
56
|
+
threaded_rails: "Plumbing::Actor::Rails"
|
45
57
|
}.freeze
|
46
58
|
private_constant :PROXY_BASE_CLASSES
|
47
59
|
|
@@ -59,5 +71,23 @@ module Plumbing
|
|
59
71
|
end
|
60
72
|
end
|
61
73
|
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def proxy= proxy
|
78
|
+
@proxy = proxy
|
79
|
+
end
|
80
|
+
|
81
|
+
def proxy = @proxy
|
82
|
+
alias_method :as_actor, :proxy
|
83
|
+
alias_method :async, :proxy
|
84
|
+
|
85
|
+
def perform_safely(&)
|
86
|
+
instance_eval(&)
|
87
|
+
nil
|
88
|
+
rescue => ex
|
89
|
+
puts ex
|
90
|
+
nil
|
91
|
+
end
|
62
92
|
end
|
63
93
|
end
|
data/lib/plumbing/junction.rb
CHANGED
@@ -5,16 +5,17 @@ module Plumbing
|
|
5
5
|
# @param sources [Array<Plumbing::Observable>] the sources which will be joined and relayed
|
6
6
|
def initialize *sources
|
7
7
|
super()
|
8
|
-
|
8
|
+
sources.each { |source| add(source) }
|
9
9
|
end
|
10
10
|
|
11
11
|
private
|
12
12
|
|
13
13
|
def add source
|
14
14
|
source.as(Observable).add_observer do |event|
|
15
|
-
|
15
|
+
safely do
|
16
|
+
dispatch event
|
17
|
+
end
|
16
18
|
end
|
17
|
-
source
|
18
19
|
end
|
19
20
|
end
|
20
21
|
end
|
data/lib/plumbing/pipe.rb
CHANGED
data/lib/plumbing/version.rb
CHANGED
@@ -9,12 +9,13 @@ require "rspec/expectations"
|
|
9
9
|
#
|
10
10
|
RSpec::Matchers.define :become_equal_to do
|
11
11
|
match do |expected|
|
12
|
+
max = Plumbing.config.timeout * 10
|
12
13
|
counter = 0
|
13
14
|
matched = false
|
14
|
-
while (counter <
|
15
|
-
matched = true if (@result = block_arg.call) == expected
|
15
|
+
while (counter < max) && (matched == false)
|
16
16
|
sleep 0.1
|
17
17
|
counter += 1
|
18
|
+
matched = true if (@result = block_arg.call) == expected
|
18
19
|
end
|
19
20
|
matched
|
20
21
|
end
|
data/spec/examples/actor_spec.rb
CHANGED
@@ -1,6 +1,32 @@
|
|
1
1
|
require "spec_helper"
|
2
|
+
require "plumbing/actor/async"
|
3
|
+
require "plumbing/actor/threaded"
|
2
4
|
|
3
5
|
RSpec.shared_examples "an example actor" do |runs_in_background|
|
6
|
+
# standard:disable Lint/ConstantDefinitionInBlock
|
7
|
+
class Employee
|
8
|
+
include Plumbing::Actor
|
9
|
+
async :name, :job_title, :greet_slowly, :promote
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
@name = name
|
13
|
+
@job_title = "Sales assistant"
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :name, :job_title
|
17
|
+
|
18
|
+
def promote
|
19
|
+
sleep 0.5
|
20
|
+
@job_title = "Sales manager"
|
21
|
+
end
|
22
|
+
|
23
|
+
def greet_slowly
|
24
|
+
sleep 0.2
|
25
|
+
"H E L L O"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
# standard:enable Lint/ConstantDefinitionInBlock
|
29
|
+
|
4
30
|
it "queries an object" do
|
5
31
|
@person = Employee.start "Alice"
|
6
32
|
|
@@ -18,41 +44,20 @@ RSpec.shared_examples "an example actor" do |runs_in_background|
|
|
18
44
|
|
19
45
|
expect(Time.now - @time).to be < 0.1 if runs_in_background
|
20
46
|
expect(Time.now - @time).to be > 0.1 if !runs_in_background
|
47
|
+
ensure
|
48
|
+
@person.stop
|
21
49
|
end
|
22
50
|
|
23
51
|
it "commands an object" do
|
24
52
|
@person = Employee.start "Alice"
|
25
53
|
@person.promote
|
26
|
-
@job_title
|
27
|
-
|
54
|
+
expect(@person.job_title.value).to eq "Sales manager"
|
55
|
+
ensure
|
56
|
+
@person.stop
|
28
57
|
end
|
29
58
|
end
|
30
59
|
|
31
60
|
RSpec.describe "Actor example: " do
|
32
|
-
# standard:disable Lint/ConstantDefinitionInBlock
|
33
|
-
class Employee
|
34
|
-
include Plumbing::Actor
|
35
|
-
async :name, :job_title, :greet_slowly, :promote
|
36
|
-
|
37
|
-
def initialize(name)
|
38
|
-
@name = name
|
39
|
-
@job_title = "Sales assistant"
|
40
|
-
end
|
41
|
-
|
42
|
-
attr_reader :name, :job_title
|
43
|
-
|
44
|
-
def promote
|
45
|
-
sleep 0.5
|
46
|
-
@job_title = "Sales manager"
|
47
|
-
end
|
48
|
-
|
49
|
-
def greet_slowly
|
50
|
-
sleep 0.2
|
51
|
-
"H E L L O"
|
52
|
-
end
|
53
|
-
end
|
54
|
-
# standard:enable Lint/ConstantDefinitionInBlock
|
55
|
-
|
56
61
|
context "inline mode" do
|
57
62
|
around :example do |example|
|
58
63
|
Plumbing.configure mode: :inline, &example
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "plumbing/actor/async"
|
3
|
+
require "plumbing/actor/threaded"
|
4
|
+
|
5
|
+
RSpec.describe "await" do
|
6
|
+
# standard:disable Lint/ConstantDefinitionInBlock
|
7
|
+
class Person
|
8
|
+
include Plumbing::Actor
|
9
|
+
async :name
|
10
|
+
def initialize name
|
11
|
+
@name = name
|
12
|
+
end
|
13
|
+
attr_reader :name
|
14
|
+
end
|
15
|
+
# standard:enable Lint/ConstantDefinitionInBlock
|
16
|
+
|
17
|
+
[:inline, :async, :threaded].each do |mode|
|
18
|
+
context "#{mode} mode" do
|
19
|
+
around :example do |example|
|
20
|
+
Sync do
|
21
|
+
Plumbing.configure mode: mode, &example
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "awaits a result from the actor directly" do
|
26
|
+
@person = Person.start "Alice"
|
27
|
+
|
28
|
+
expect(@person.name.value).to eq "Alice"
|
29
|
+
end
|
30
|
+
|
31
|
+
it "uses a block to await the result from the actor" do
|
32
|
+
@person = Person.start "Alice"
|
33
|
+
|
34
|
+
expect(await { @person.name }).to eq "Alice"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "uses a block to immediately access non-actor objects" do
|
38
|
+
@person = "Bob"
|
39
|
+
expect(await { @person }).to eq "Bob"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/spec/examples/pipe_spec.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
require "async"
|
3
|
+
require "plumbing/actor/async"
|
4
|
+
require "plumbing/actor/threaded"
|
3
5
|
|
4
6
|
RSpec.describe "Pipe examples" do
|
5
7
|
it "observes events" do
|
@@ -42,17 +44,19 @@ RSpec.describe "Pipe examples" do
|
|
42
44
|
end
|
43
45
|
|
44
46
|
def received event
|
45
|
-
|
46
|
-
|
47
|
-
@events.
|
48
|
-
|
47
|
+
safely do
|
48
|
+
@events << event
|
49
|
+
if @events.count >= 3
|
50
|
+
@events.clear
|
51
|
+
self << event
|
52
|
+
end
|
49
53
|
end
|
50
54
|
end
|
51
55
|
end
|
52
56
|
# standard:enable Lint/ConstantDefinitionInBlock
|
53
57
|
|
54
58
|
@source = Plumbing::Pipe.start
|
55
|
-
@filter = EveryThirdEvent.
|
59
|
+
@filter = EveryThirdEvent.start(source: @source)
|
56
60
|
|
57
61
|
@result = []
|
58
62
|
@filter.add_observer do |event|
|
@@ -83,7 +87,7 @@ RSpec.describe "Pipe examples" do
|
|
83
87
|
expect(@result).to eq ["one", "two"]
|
84
88
|
end
|
85
89
|
|
86
|
-
it "dispatches events asynchronously using
|
90
|
+
it "dispatches events asynchronously using async" do
|
87
91
|
Plumbing.configure mode: :async do
|
88
92
|
Sync do
|
89
93
|
@first_source = Plumbing::Pipe.start
|
@@ -106,4 +110,36 @@ RSpec.describe "Pipe examples" do
|
|
106
110
|
end
|
107
111
|
end
|
108
112
|
end
|
113
|
+
|
114
|
+
it "dispatches events asynchronously using threads" do
|
115
|
+
Plumbing.configure mode: :threaded do
|
116
|
+
@result = []
|
117
|
+
|
118
|
+
@first_source = Plumbing::Pipe.start
|
119
|
+
@second_source = Plumbing::Pipe.start
|
120
|
+
@junction = Plumbing::Junction.start @first_source, @second_source
|
121
|
+
|
122
|
+
@filter = Plumbing::Filter.start source: @junction do |event|
|
123
|
+
%w[one-one two-two].include? event.type
|
124
|
+
end
|
125
|
+
await do
|
126
|
+
@filter.add_observer do |event|
|
127
|
+
puts "observing #{event.type}"
|
128
|
+
@result << event.type
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
@first_source.notify "one-one"
|
133
|
+
@first_source.notify "one-two"
|
134
|
+
@second_source.notify "two-one"
|
135
|
+
@second_source.notify "two-two"
|
136
|
+
|
137
|
+
expect(["one-one", "two-two"]).to become_equal_to { @result.sort }
|
138
|
+
ensure
|
139
|
+
@first_source.shutdown
|
140
|
+
@second_source.shutdown
|
141
|
+
@junction.shutdown
|
142
|
+
@filter.shutdown
|
143
|
+
end
|
144
|
+
end
|
109
145
|
end
|
data/spec/plumbing/a_pipe.rb
CHANGED
@@ -98,11 +98,13 @@ RSpec.shared_examples "a pipe" do
|
|
98
98
|
|
99
99
|
it "shuts down the pipe" do
|
100
100
|
@pipe = described_class.start
|
101
|
+
@results = []
|
101
102
|
@observer = ->(event) { @results << event }
|
102
103
|
@pipe.add_observer @observer
|
103
104
|
|
104
105
|
@pipe.shutdown
|
105
|
-
|
106
|
-
|
106
|
+
@pipe.notify "ignore_me"
|
107
|
+
sleep 0.2
|
108
|
+
expect(@results).to be_empty
|
107
109
|
end
|
108
110
|
end
|
data/spec/plumbing/actor_spec.rb
CHANGED
@@ -51,6 +51,67 @@ RSpec.describe Plumbing::Actor do
|
|
51
51
|
raise "I'm a failure"
|
52
52
|
end
|
53
53
|
end
|
54
|
+
|
55
|
+
class WhoAmI
|
56
|
+
include Plumbing::Actor
|
57
|
+
async :me_as_actor, :me_as_self
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def me_as_actor = as_actor
|
62
|
+
|
63
|
+
def me_as_self = self
|
64
|
+
|
65
|
+
def prepare = @calling_thread = Thread.current
|
66
|
+
|
67
|
+
def check = @calling_thread == Thread.current
|
68
|
+
end
|
69
|
+
|
70
|
+
class Actor
|
71
|
+
include Plumbing::Actor
|
72
|
+
async :get_object_id, :get_object
|
73
|
+
|
74
|
+
private def get_object_id(record) = record.object_id
|
75
|
+
private def get_object(record) = record
|
76
|
+
end
|
77
|
+
|
78
|
+
class SafetyCheck
|
79
|
+
include Plumbing::Actor
|
80
|
+
async :called_from_actor_thread?
|
81
|
+
|
82
|
+
def initialize tester
|
83
|
+
@tester = tester
|
84
|
+
@called_from_actor_thread = false
|
85
|
+
configure_safety_check
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def called_from_actor_thread? = @called_from_actor_thread
|
91
|
+
|
92
|
+
def configure_safety_check
|
93
|
+
@tester.on_safety_check do
|
94
|
+
safely do
|
95
|
+
@called_from_actor_thread = proxy.within_actor?
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class Tester
|
102
|
+
include Plumbing::Actor
|
103
|
+
async :on_safety_check, :do_safety_check
|
104
|
+
|
105
|
+
def initialize
|
106
|
+
@on_safety_check = nil
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def on_safety_check(&block) = @on_safety_check = block
|
112
|
+
|
113
|
+
def do_safety_check = @on_safety_check&.call
|
114
|
+
end
|
54
115
|
# standard:enable Lint/ConstantDefinitionInBlock
|
55
116
|
|
56
117
|
it "knows which async messages are understood" do
|
@@ -65,15 +126,22 @@ RSpec.describe Plumbing::Actor do
|
|
65
126
|
expect(@counter.class).to eq @proxy_class
|
66
127
|
end
|
67
128
|
|
68
|
-
it "includes
|
129
|
+
it "includes async messages from the superclass" do
|
69
130
|
expect(StepCounter.async_messages).to eq [:name, :count, :slow_query, :slowly_increment, :raises_error, :step_value]
|
70
131
|
|
71
132
|
@step_counter = StepCounter.start "step counter", initial_value: 100, step_value: 10
|
72
133
|
|
73
|
-
expect(@step_counter.count.
|
74
|
-
expect(@step_counter.step_value.
|
134
|
+
expect(@step_counter.count.value).to eq 100
|
135
|
+
expect(@step_counter.step_value.value).to eq 10
|
75
136
|
@step_counter.slowly_increment
|
76
|
-
expect(@step_counter.count.
|
137
|
+
expect(@step_counter.count.value).to eq 110
|
138
|
+
end
|
139
|
+
|
140
|
+
it "can access its own proxy" do
|
141
|
+
@actor = WhoAmI.start
|
142
|
+
|
143
|
+
expect(await { @actor.me_as_self }).to_not eq @actor
|
144
|
+
expect(await { @actor.me_as_actor }).to eq @actor
|
77
145
|
end
|
78
146
|
|
79
147
|
context "inline" do
|
@@ -85,11 +153,11 @@ RSpec.describe Plumbing::Actor do
|
|
85
153
|
@counter = Counter.start "inline counter", initial_value: 100
|
86
154
|
@time = Time.now
|
87
155
|
|
88
|
-
expect(@counter.name.
|
89
|
-
expect(@counter.count.
|
156
|
+
expect(@counter.name.value).to eq "inline counter"
|
157
|
+
expect(@counter.count.value).to eq 100
|
90
158
|
expect(Time.now - @time).to be < 0.1
|
91
159
|
|
92
|
-
expect(@counter.slow_query.
|
160
|
+
expect(@counter.slow_query.value).to eq 100
|
93
161
|
expect(Time.now - @time).to be > 0.1
|
94
162
|
end
|
95
163
|
|
@@ -99,9 +167,18 @@ RSpec.describe Plumbing::Actor do
|
|
99
167
|
|
100
168
|
@counter.slowly_increment
|
101
169
|
|
102
|
-
expect(@counter.count.
|
170
|
+
expect(@counter.count.value).to eq 101
|
103
171
|
expect(Time.now - @time).to be > 0.1
|
104
172
|
end
|
173
|
+
|
174
|
+
it "can safely access its own data" do
|
175
|
+
@tester = Tester.start
|
176
|
+
@safety_check = SafetyCheck.start @tester
|
177
|
+
|
178
|
+
@tester.do_safety_check
|
179
|
+
|
180
|
+
expect(true).to become_equal_to { @safety_check.called_from_actor_thread?.value }
|
181
|
+
end
|
105
182
|
end
|
106
183
|
|
107
184
|
[:threaded, :async].each do |mode|
|
@@ -116,12 +193,14 @@ RSpec.describe Plumbing::Actor do
|
|
116
193
|
@counter = Counter.start "async counter", initial_value: 100
|
117
194
|
@time = Time.now
|
118
195
|
|
119
|
-
expect(@counter.name.
|
120
|
-
expect(@counter.count.
|
196
|
+
expect(@counter.name.value).to eq "async counter"
|
197
|
+
expect(@counter.count.value).to eq 100
|
121
198
|
expect(Time.now - @time).to be < 0.1
|
122
199
|
|
123
|
-
expect(@counter.slow_query.
|
200
|
+
expect(@counter.slow_query.value).to eq 100
|
124
201
|
expect(Time.now - @time).to be > 0.1
|
202
|
+
ensure
|
203
|
+
@counter.stop
|
125
204
|
end
|
126
205
|
|
127
206
|
it "performs queries ignoring the response and returning immediately" do
|
@@ -131,6 +210,8 @@ RSpec.describe Plumbing::Actor do
|
|
131
210
|
@counter.slow_query
|
132
211
|
|
133
212
|
expect(Time.now - @time).to be < 0.1
|
213
|
+
ensure
|
214
|
+
@counter.stop
|
134
215
|
end
|
135
216
|
|
136
217
|
it "performs commands in the background and returning immediately" do
|
@@ -141,20 +222,26 @@ RSpec.describe Plumbing::Actor do
|
|
141
222
|
expect(Time.now - @time).to be < 0.1
|
142
223
|
|
143
224
|
# wait for the background task to complete
|
144
|
-
expect(101).to become_equal_to { @counter.count.
|
225
|
+
expect(101).to become_equal_to { @counter.count.value }
|
145
226
|
expect(Time.now - @time).to be > 0.1
|
227
|
+
ensure
|
228
|
+
@counter.stop
|
146
229
|
end
|
147
230
|
|
148
231
|
it "re-raises exceptions when checking the result" do
|
149
232
|
@counter = Counter.start "failure"
|
150
233
|
|
151
|
-
expect { @counter.raises_error.
|
234
|
+
expect { @counter.raises_error.value }.to raise_error "I'm an error"
|
235
|
+
ensure
|
236
|
+
@counter.stop
|
152
237
|
end
|
153
238
|
|
154
239
|
it "does not raise exceptions if ignoring the result" do
|
155
240
|
@counter = Counter.start "failure"
|
156
241
|
|
157
242
|
expect { @counter.raises_error }.not_to raise_error
|
243
|
+
ensure
|
244
|
+
@counter.stop
|
158
245
|
end
|
159
246
|
end
|
160
247
|
end
|
@@ -164,6 +251,13 @@ RSpec.describe Plumbing::Actor do
|
|
164
251
|
Plumbing.configure mode: :threaded, &example
|
165
252
|
end
|
166
253
|
|
254
|
+
before do
|
255
|
+
GlobalID.app = "rspec"
|
256
|
+
GlobalID::Locator.use :rspec do |gid, options|
|
257
|
+
Record.new gid.model_id
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
167
261
|
# standard:disable Lint/ConstantDefinitionInBlock
|
168
262
|
class Record
|
169
263
|
include GlobalID::Identification
|
@@ -176,33 +270,41 @@ RSpec.describe Plumbing::Actor do
|
|
176
270
|
other.id == @id
|
177
271
|
end
|
178
272
|
end
|
179
|
-
|
180
|
-
class Actor
|
181
|
-
include Plumbing::Actor
|
182
|
-
async :get_object_id, :get_object
|
183
|
-
|
184
|
-
private def get_object_id(record) = record.object_id
|
185
|
-
private def get_object(record) = record
|
186
|
-
end
|
187
273
|
# standard:enable Lint/ConstantDefinitionInBlock
|
188
274
|
|
189
275
|
it "packs and unpacks arguments when sending them across threads" do
|
190
276
|
@actor = Actor.start
|
191
277
|
@record = Record.new "999"
|
192
278
|
|
193
|
-
@object_id = @actor.get_object_id(@record).
|
279
|
+
@object_id = @actor.get_object_id(@record).value
|
194
280
|
|
195
281
|
expect(@object_id).to_not eq @record.object_id
|
282
|
+
ensure
|
283
|
+
@actor.stop
|
196
284
|
end
|
197
285
|
|
198
286
|
it "packs and unpacks results when sending them across threads" do
|
199
287
|
@actor = Actor.start
|
200
288
|
@record = Record.new "999"
|
201
289
|
|
202
|
-
@object = @actor.get_object(@record).
|
290
|
+
@object = @actor.get_object(@record).value
|
203
291
|
|
204
292
|
expect(@object.id).to eq @record.id
|
205
293
|
expect(@object.object_id).to_not eq @record.object_id
|
294
|
+
ensure
|
295
|
+
@actor.stop
|
296
|
+
end
|
297
|
+
|
298
|
+
it "can safely access its own data" do
|
299
|
+
@tester = Tester.start
|
300
|
+
@safety_check = SafetyCheck.start @tester
|
301
|
+
|
302
|
+
@tester.do_safety_check
|
303
|
+
|
304
|
+
expect(true).to become_equal_to { @safety_check.called_from_actor_thread?.value }
|
305
|
+
ensure
|
306
|
+
@tester.stop
|
307
|
+
@safety_check.stop
|
206
308
|
end
|
207
309
|
end
|
208
310
|
end
|
@@ -15,7 +15,7 @@ RSpec.describe Plumbing::CustomFilter do
|
|
15
15
|
# standard:enable Lint/ConstantDefinitionInBlock
|
16
16
|
|
17
17
|
@pipe = Plumbing::Pipe.start
|
18
|
-
@filter = ReversingFilter.
|
18
|
+
@filter = ReversingFilter.start(source: @pipe)
|
19
19
|
@result = []
|
20
20
|
@filter.add_observer do |event|
|
21
21
|
@result << event.type
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: standard-procedure-plumbing
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rahoul Baruah
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-09-
|
11
|
+
date: 2024-09-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: globalid
|
@@ -59,6 +59,7 @@ files:
|
|
59
59
|
- lib/plumbing/version.rb
|
60
60
|
- spec/become_equal_to_matcher.rb
|
61
61
|
- spec/examples/actor_spec.rb
|
62
|
+
- spec/examples/await_spec.rb
|
62
63
|
- spec/examples/pipe_spec.rb
|
63
64
|
- spec/examples/pipeline_spec.rb
|
64
65
|
- spec/examples/rubber_duck_spec.rb
|