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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a51cb6cf16fcc70cd0f6607a461e817c7021ed9f0de445e1f628ff1a8de9e489
4
- data.tar.gz: 992e53f3a79cb4f3597b9758e10953156ec9dda073eff97bc740e6050746fddc
3
+ metadata.gz: ace6c01658b88c08c34d48406f983fdad449bc4f122b8dfed026853841b48d35
4
+ data.tar.gz: 96b4458d0c667e10bb47fb8881de2e21df0ff40243cf07ff30e2e22f29892625
5
5
  SHA512:
6
- metadata.gz: f4ff32c0bbd46d2433bcb051572914fa96378f5ffe6badd0fa77462565c058357b6803ff98afcf4534a739a571bb9084e39fe045ec4415dd084379d12e99e684
7
- data.tar.gz: 6a1d52c882d1edb1d5705383596e4b4c556f4f561770466a4bd18529e8e21630d3c522d323feb6fed8e0b87c22bc35b56075b47ae53a62a9176473061b0840a0
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 `: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.
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 rails mode.
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 `#await` - or alternatively, wrap your call in `await { ... }`. `await` is added in to ruby's `Kernel` so it is available everywhere. 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.
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 `await` has a timeout defaulting to 30s.
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 actprs
203
+ ### Threaded actors
188
204
 
189
- Using threaded (or 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.
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 `clone`d. 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.
193
- 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 `await` or you risk a deadlock. If you do pass a block or proc parameter, you should limit your actions to sending a message to other actors without awaiting the results.
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
- (Note: we break that rule in the specs for the Pipe object - 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).
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
- Even when using actors, there is one condition where concurrency may cause issues. If object A makes a query to object B which in turn makes a query back to object A, you will hit a deadlock. This is because A is waiting on the response from B but B is now querying, and waiting for, A. This does not apply to commands because they do not wait for a response. However, when writing queries, be careful who you interact with - the configuration allows you to set a timeout (defaulting to 30s) in case this happens.
239
+ ### Referencing actors
200
240
 
201
- Also be aware that if you use actors in one place, you need to use them everywhere - especially if you're using threads or ractors (coming soon). 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.
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
- query :name, :job_title, :greet_slowly
215
- command :promote
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
- # `greet_slowly` is a query so will block until a response is received
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
- # This will run in the background
289
+ # this will run in the background
248
290
  @person.promote
249
- # this will block, as we wait for the result from #job_title and #job_title will not run until after #promote has completed
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
- # store this event into our buffer
306
- @events << event
307
- # if this is the third event we've received then clear the buffer and broadcast the latest event
308
- if @events.count >= 3
309
- @events.clear
310
- self << event
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.
@@ -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 await
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
- Result = Data.define(:value) do
17
- def await = value.is_a?(Exception) ? raise(value) : value
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
@@ -2,7 +2,8 @@ module Plumbing
2
2
  module Actor
3
3
  ::Kernel.class_eval do
4
4
  def await &block
5
- block.call.await
5
+ result = block.call
6
+ result.respond_to?(:value) ? result.send(:value) : result
6
7
  end
7
8
  end
8
9
  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
- @queue << message
21
- send_messages if @queue.size == 1
21
+ @mutex.synchronize do
22
+ @queue << message
23
+ send_messages if @queue.any?
24
+ end
22
25
  end
23
26
  end
24
27
 
25
- protected
28
+ def safely(&)
29
+ send_message(:perform_safely, &)
30
+ nil
31
+ end
32
+
33
+ def within_actor? = @mutex.owned?
26
34
 
27
- def future(&)
28
- Concurrent::Promises.future(&)
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
- while (message = @queue.shift)
36
- message.call
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 await
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
@@ -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
- build_proxy_for(new(*, **, &))
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
- rails: "Plumbing::Actor::Rails"
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
@@ -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
- @sources = sources.collect { |source| add(source) }
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
- dispatch event
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
@@ -49,6 +49,7 @@ module Plumbing
49
49
  # Subclasses should override this to perform their own shutdown routines and call `super` to ensure everything is tidied up
50
50
  def shutdown
51
51
  observers.clear
52
+ stop
52
53
  end
53
54
 
54
55
  protected
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -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 < 50) && (matched == false)
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
@@ -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 = await { @person.job_title }
27
- expect(@job_title).to eq "Sales manager"
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
@@ -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
- @events << event
46
- if @events.count >= 3
47
- @events.clear
48
- self << event
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.new(source: @source)
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 fibers" do
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
@@ -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
- expect(await { @pipe.is_observer?(@observer) }).to eq false
106
+ @pipe.notify "ignore_me"
107
+ sleep 0.2
108
+ expect(@results).to be_empty
107
109
  end
108
110
  end
@@ -15,6 +15,7 @@ RSpec.describe Plumbing::Actor::Transporter do
15
15
  end
16
16
  end
17
17
  # standard:enable Lint/ConstantDefinitionInBlock
18
+
18
19
  before do
19
20
  GlobalID.app = "rspec"
20
21
  GlobalID::Locator.use :rspec do |gid, options|
@@ -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 commands and queries from the superclass" do
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.await).to eq 100
74
- expect(@step_counter.step_value.await).to eq 10
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.await).to eq 110
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.await).to eq "inline counter"
89
- expect(@counter.count.await).to eq 100
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.await).to eq 100
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.await).to eq 101
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.await).to eq "async counter"
120
- expect(@counter.count.await).to eq 100
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.await).to eq 100
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.await }
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.await }.to raise_error "I'm an 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).await
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).await
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.new(source: @pipe)
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.0
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-15 00:00:00.000000000 Z
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