standard-procedure-async 0.1.0 → 0.1.2

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: b92c0e8440b6f2ad4b0c95cc83ac562ece4124490f15982f1d6531efec3ca2b1
4
- data.tar.gz: ba0100a22ccbf8c572489111b841bdee403347e2f3a65172b8584594ebd026e5
3
+ metadata.gz: 7c712b9a4a30138eb2c834422df444d22da7fbb16da26de1c1ec81db7cf15985
4
+ data.tar.gz: 71dbabb9e071adb19146e17a275daaad05b0c9f242843b3d957908087f604a91
5
5
  SHA512:
6
- metadata.gz: ec6d3846f51149189ae18dffbf38af10cbb9c317ac9b546b1e336b726219d040b7b0d5fab65dd4a33c1be079ad093703947236c46ce1345af0f33cc5474bb50d
7
- data.tar.gz: fb99264092314bad0f9c4c1e7d1061e2530456bf59b409eadbbe6f5f4a216ad69d71a5abd2fd3a74c4d0c1de0d9fa4f50b7f2053259de9435bfd3e9f7eb34fc8
6
+ metadata.gz: d84213a0def468819fbd0f63bc42d10a483b13790e1e3ee632b72d63d2d2d9438df2c7a3a586b6acbedba07ceaf8794b999c33cd53df4fb166cf0f5e5f998289
7
+ data.tar.gz: 3972be625613fa325b92273d594f0e9c213fa4675e03181c39d60e334251f3fea7e6d369a9335dc639994aa182de8ead6785be04d011d21186c56b08badfc9fb
data/CHANGELOG.md CHANGED
@@ -1,4 +1,10 @@
1
- ## [Unreleased]
1
+ ## [0.1.2] - 2023-05-31
2
+
3
+ - Added a timeout to `StandardProcedure::Async::Actor::Message` to prevent indefinite deadlocks.
4
+
5
+ ## [0.1.1] - 2023-05-31
6
+
7
+ - Changed the namespace from Standard::Procedure to StandardProcedure
2
8
 
3
9
  ## [0.1.0] - 2023-05-30
4
10
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- standard-procedure-async (0.1.0)
4
+ standard-procedure-async (0.1.2)
5
5
  concurrent-ruby (>= 1.0)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Standard::Procedure::Async
1
+ # StandardProcedure::Async
2
2
 
3
3
  ## The Actor model
4
4
 
@@ -8,7 +8,11 @@ The actual implementation does not use a thread-per-object as that could get ver
8
8
 
9
9
  ## Why does this gem exist?
10
10
 
11
- Two reasons.
11
+ The short answer:
12
+
13
+ Because you can't use concurrent-ruby's Async mixin inside a Rails app. On top of that, I wanted to change its syntax a little bit to match how it's done in Javascript.
14
+
15
+ The long answer:
12
16
 
13
17
  Concurrent-ruby has a simple implementation of this model, using the [Async](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Async.html) mixin. However, Concurrent::Async uses [IVar](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/IVar.html)s which are now deprecated.
14
18
 
@@ -16,24 +20,24 @@ In addition, while concurrent-ruby is an excellent library, it does not work wel
16
20
 
17
21
  Finally, using the actor model is infectious. If you mark a public method as asynchronous, in order to be safe, you have to mark them all as asynchronous. Concurrent::Async's syntax relies on the caller using `@my_object.async.my_method` which means that it is easy to forget and miss an asynchronous call. Which in turn will result in inconsistent behaviour and hard to trace bugs.
18
22
 
19
- So [Standard::Procedure::Async::Actor](https://github.com/standard-procedure/async/blob/main/lib/standard/procedure/async/actor.rb) replaces the IVars with a mix of Futures and MVars. The MVar is used to transfer the return values of any methods back to the calling thread and the Future is used to do the work in a separate thread.
23
+ So [StandardProcedure::Async::Actor](https://github.com/standard-procedure/async/blob/main/lib/standard_procedure/async/actor.rb) replaces the IVars with a mix of Futures and [MVar](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/MVar.html)s. The MVar is used to transfer the return values of any methods back to the calling thread and the Future is used to do the work in a separate thread.
20
24
 
21
- In addition, if you are in a Ruby on Rails project, Standard::Procedure::Async uses [Luiz Kowalski](https://github.com/luizkowalski)'s [concurrent_rails](https://github.com/luizkowalski/concurrent_rails) gem. This is a layer above concurrent-ruby's Futures that ensure any future code is run within the Rails Executor.
25
+ In addition, if you are in a Ruby on Rails project, StandardProcedure::Async uses [Luiz Kowalski](https://github.com/luizkowalski)'s [concurrent_rails](https://github.com/luizkowalski/concurrent_rails) gem. This is a layer above concurrent-ruby's Futures that ensure any future code is run within the Rails Executor.
22
26
 
23
27
  Finally, instead of relying on the caller to call `async` on asynchronous methods, we define the asynchronous methods on the class itself with the `async` definition. The caller simply uses `@my_object.my_method` and it will always be run safely in an alternate thread. There is also an implementation of `await`, making it easy to resolve the results of your method calls.
24
28
 
25
29
  ## Adding `async` and `await` capabilities to ruby objects
26
30
 
27
- Instead of defining methods on your class with ruby's `def` keyword, include the [Standard::Procedure::Async::Actor](https://github.com/standard-procedure/async/blob/main/lib/standard/procedure/async/actor.rb) module and use the `async` class method.
31
+ Instead of defining methods on your class with ruby's `def` keyword, include the [StandardProcedure::Async::Actor](https://github.com/standard-procedure/async/blob/main/lib/standard_procedure/async/actor.rb) module and use the `async` class method.
28
32
 
29
- When you call an asynchronous method, it immediately returns an internal message object. If you don't care about the return value from the method, you can discard this object immediately. But if you do need the return value, you can call `value` on this message object - and your thread will then wait until the return value is ready. Alternatively, you can use `await`, effectively turning your asynchronous method into a synchronous one.
33
+ When you call an asynchronous method, it immediately returns an internal message object. If you don't care about the return value from the method, you can discard this object immediately. But if you do need the return value, you can call `value` on this message object - and your thread will then block until the return value is ready. Alternatively, you can use `await`, effectively turning your asynchronous method into a synchronous one. See the note below about using `await` or `value` if you are calling a method from inside an actor - in short, be careful.
30
34
 
31
- An added bonus (or negative, depending upon your point of view) is that this syntax is very similar to Javascript's async/await pairing, which similarly marks out asychronous function calls and waits until any Promises are ready to return their values.
35
+ An added bonus (or negative, depending upon your point of view) is that this syntax is very similar to Javascript's async/await pairing, which marks out asychronous function calls and waits until any [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) are ready to return their values.
32
36
 
33
37
  Example usage:
34
38
  ```ruby
35
39
  class MyObject
36
- include Standard::Procedure::Async::Actor
40
+ include StandardProcedure::Async::Actor
37
41
 
38
42
  def initialize name
39
43
  @name = name
@@ -69,15 +73,20 @@ end
69
73
 
70
74
  @my_object = MyObject.new "George"
71
75
 
76
+ # Getting started
77
+ puts @my_object.report_status # => internal message object
78
+ puts @my_object.report_status.value # => :idle
79
+ puts await { @my_object.report_status } # => :idle
72
80
  puts await { @my_object.greet } # => "Hello George"
81
+ # Do some actual work
73
82
  await { @my_object.rename "Ringo" }
74
83
  puts await { @my_object.greet } # => "Hello Ringo"
75
84
  @my_object.rename "Paul" # Note: we didn't use await here - we'll talk about that later
76
85
  puts await { @my_object.greet } # => "Hello Paul"
77
-
86
+ # Do something a bit more complex
78
87
  @initial_status = @my_object.do_some_long_running_task
79
- puts @initial_status # => a internal message object
80
- puts @initial_status.value # => :in_progress
88
+ puts await { @initial_status } # => :in_progress
89
+ # *drums fingers* *sips from cup of tea*
81
90
  sleep 11
82
91
  @final_status = await { @my_object.report_status }
83
92
  puts @final_status # => :done
@@ -90,13 +99,21 @@ When defining `MyObject`, we use `async` instead of `def` for each method. For
90
99
 
91
100
  ### Awaiting the results from those methods
92
101
 
93
- The asynchronous wrapper always returns an object containing a [Concurrent::MVar](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/MVar.html) which is empty until the actor has completed its work.
102
+ The asynchronous wrapper always returns a internal message object that contains a [Concurrent::MVar](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/MVar.html). The MVar is empty until the actor has completed its work but if you need the return value from the method, there are two ways to access it.
103
+
104
+ You can call `value` on the returned message.
105
+
106
+ Or you can use the `await` method (which is just a fancy wrapper around `value`).
107
+
108
+ In both cases the calling thread will block until the value is returned.
94
109
 
95
- If you need the return value from the method, there are two ways to access it.
110
+ In the example above, you can see how this works.
96
111
 
97
- You can call `value` on the returned object, or you can use the `await` method (which is just a fancy wrapper around `value`). In both cases the calling thread will block until the value is returned. In the example above, you can see what happens if you use neither of these methods - `puts @initial_status` returns the internal message object itself, not any meaningful information. The next line then calls `@initial_status.value` to wait until the return value is generated.
112
+ - `puts @my_object.report_status` returns the internal message object itself, not any meaningful information.
113
+ - `puts @my_object.report_status.value` blocks until the `report_status` method has completed and gives you the return value.
114
+ - `puts await { @my_object.report_status }` is just a fancier (and potentially more readable and explicit) way of doing the same thing
98
115
 
99
- ### The sequence of asynchronous calls
116
+ ### The strict sequencing of asynchronous calls
100
117
 
101
118
  In the example above there is the following code:
102
119
 
@@ -104,11 +121,13 @@ In the example above there is the following code:
104
121
  @my_object.rename "Paul"
105
122
  puts await { @my_object.greet } # => "Hello Paul"
106
123
  ```
107
- The call to `rename` is asynchronous, so you may expect that sometimes the following call to `greet` would return "Hello Paul" and other times it would return "Hello Ringo" (the previous value) - depending on the timing of the two calls.
124
+ The call to `rename` is asynchronous and we call `greet` immediately afterwards. You may expect that the result from `greet` would not be deterministic. If `rename` takes a long time to complete it may or may not have finished before we get to the next line of code. Does that mean that `greet` could return "Hello Ringo" or "Hello Paul" depending on timing?
108
125
 
109
- However, it will _always_ return "Hello Paul".
126
+ No - the call to `greet` will _always_ return "Hello Paul".
110
127
 
111
- This is because internally, the actor queues all method calls. So even if the call to `rename` takes a long time to complete, the subsequent call to `greet` will not start until `rename` has finished.
128
+ This is because internally, the actor queues all asynchronous method calls.
129
+
130
+ The call to `rename` is added to the queue, then a future starts to process it. The subsequent call to `greet` is added to the queue and will not begin until the call to `rename` has completed.
112
131
 
113
132
  Another example of the same behaviour is in `do_some_long_running_task`:
114
133
 
@@ -122,13 +141,30 @@ Another example of the same behaviour is in `do_some_long_running_task`:
122
141
 
123
142
  When `@my_object.do_some_long_running_task` is called, the message `do_some_long_running_task` is added to the queue.
124
143
 
125
- When the queue starts processing that message, `_do_some_long_running_task` (the implementation of the method) changes the status to :in_progress, then adds `do_part_two_of_the_long_running_task` to the message queue. This second message will _not_ start processing immediately as it is behind the unfinished `do_some_long_running_task`. `_do_some_long_running_task` returns the value of status (which is still :in_progress) and completes, which allows the next message on the queue to start. And that next message is probably `do_part_two_of_the_long_running_task` - but it might not be, as other threads may have got there first.
144
+ When the queue starts processing that message, `_do_some_long_running_task` (the implementation of the method) changes the status to :in_progress, then adds `do_part_two_of_the_long_running_task` to the message queue. This second message will _not_ start processing immediately as it is behind the unfinished `do_some_long_running_task`. Eventually `_do_some_long_running_task` returns the value of status (which is still :in_progress) and completes, which allows the next message on the queue to start. And that next message is probably `do_part_two_of_the_long_running_task` - but, this being concurrent programming, it might not be (as another thread may have put a different message onto the queue first).
145
+
146
+ This queueing has an important implication.
147
+
148
+ If you are within an actor class, you can happily call async methods on other objects and `await` the results - because each actor has its own independent queue. But if you are calling your own methods, you must **never** call `await` (or call `value` on the returned message object).
149
+
150
+ Picture this:
151
+ - Your actor has two `async` methods - `start_processing` and `do_complicated_calculation`
152
+ - The main thread calls `start_processing` - so a message gets added to the head of the queue.
153
+ - The actor's internal thread starts working on the message at the head of the queue, and `_start_processing` (the actual implementation) is called.
154
+ - Within `_start_processing` your actor calls `do_complicated_calculation`. This adds a message to the second position in the queue, ready to start working after `_start_processing` has completed. The message object itself is returned to `_start_processing`.
155
+ - Within `_start_processing` you call `value` to extract the actual return value from `do_complicated_calculation`. This then blocks the actor's thread, while it waits for the value to be calculated.
156
+ - But the message for `do_complicated_calculation` is second in the queue and won't be called until `_start_processing` has completed. And `_start_processing` won't complete, as it is now blocked until `_do_complicated_calculation` has finished.
157
+ - Your actor is now deadlocked and will not be able to do any more work.
158
+
159
+ To avoid this, if you ever need the return value from an internal `async` method, always call the implementation method: `_do_complicated_calculation` instead of `do_complicated_calculation`. This does not put anything on the queue and proceeds as a normal method call.
160
+
161
+ As a fail-safe, any calls to `value` will also time-out after 30 seconds, returning Concurrent::MVar::TIMEOUT. If you need to override the timeout value you can use `message.value(timeout: value_in_seconds)`.
126
162
 
127
163
  ### The rules of using actors
128
164
 
129
- - If you make a public method asynchronous, you need to make _all_ public methods asynchronous. You cannot mix and match asynchronous and synchronous usage. The simplest way to comply is to make all your public methods as `async` and your protected and private methods as synchronous. If you have to call an async method on `self` use the internal implementation (which starts with `_`).
130
- - Never make internal instance variables directly accessible without an asynchronous method call. Do not use `attr_reader` or `attr_accessor` - these will bypass the internal queue and you may get inconsistent results from the actor. For the same reason, never update internal variables outside of an asynchronous call.
131
- - When an object is calling its own internal methods, never use `await` or `value` as this will cause your actor to block indefinitely. If you don't care about the return value or if it does not matter when the method starts and finishes, call the asynchronous method. If you need the return value or need to be sure that the method runs immediately then use the internal implementation - `_rename` instead of `rename`.
165
+ - If you make a public method asynchronous, you need to make _all_ public methods asynchronous. You cannot mix and match asynchronous and synchronous usage. The simplest way to comply is to make all your public methods as `async` and your protected and private methods as synchronous.
166
+ - Never make internal instance variables directly accessible without an asynchronous method call. Do not use `attr_reader` or `attr_accessor` - these will bypass the internal queue and you may get inconsistent results from the actor. For the same reason, never update internal variables outside of an asynchronous call. In Object-Oriented terms, the doctrine is "Tell, don't Ask": tell your objects what you want them to do, instead of asking them for information about themselves.
167
+ - When an object is calling its own internal methods, never use `await` or `value` as this will cause your actor to block indefinitely (actually, they will time out after 30s). If you don't care about the return value or if it does not matter when the method starts and finishes, call the asynchronous method. If you need the return value or need to be sure that the method runs immediately then use the internal implementation - `_my_method` instead of `my_method`.
132
168
  - Avoid class variables. These are effectively global variables that are accessible without any locking around them, so you could get inconsistent results. If you must have class variables, intialize them on startup and if they are mutable, use concurrent-ruby's thread-safe objects.
133
169
 
134
170
  ## Making Rails work with Concurrent-Ruby
@@ -4,7 +4,7 @@ require "concurrent/array"
4
4
  require "concurrent/mvar"
5
5
  require "concurrent/immutable_struct"
6
6
 
7
- module Standard::Procedure::Async
7
+ module StandardProcedure::Async
8
8
  module Actor
9
9
  def self.included base
10
10
  base.class_eval do
@@ -44,7 +44,7 @@ module Standard::Procedure::Async
44
44
  end
45
45
 
46
46
  def _perform_messages
47
- Standard::Procedure::Async.promises.future do
47
+ StandardProcedure::Async.promises.future do
48
48
  while (message = _messages.shift)
49
49
  message.call
50
50
  end
@@ -52,8 +52,8 @@ module Standard::Procedure::Async
52
52
  end
53
53
 
54
54
  class Message < Concurrent::ImmutableStruct.new(:target, :name, :args, :block, :result)
55
- def value
56
- result.take
55
+ def value(timeout: 30)
56
+ result.take(timeout)
57
57
  end
58
58
 
59
59
  def call
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Standard::Procedure::Async
3
+ module StandardProcedure::Async
4
4
  class Error < StandardError
5
5
  end
6
6
  end
@@ -3,7 +3,7 @@
3
3
  require_relative "rails_not_loaded_error"
4
4
  require "concurrent/promises"
5
5
 
6
- module Standard::Procedure::Async
6
+ module StandardProcedure::Async
7
7
  class Promises
8
8
  def initialize
9
9
  @promises = rails_is_loaded? ? ConcurrentRails::Promises : Concurrent::Promises
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "error"
4
4
 
5
- module Standard::Procedure::Async
5
+ module StandardProcedure::Async
6
6
  class RailsNotLoadedError < Error
7
7
  end
8
8
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardProcedure
4
+ module Async
5
+ VERSION = "0.1.2"
6
+ end
7
+ end
@@ -5,12 +5,10 @@ require_relative "async/error"
5
5
  require_relative "async/promises"
6
6
  require_relative "async/actor"
7
7
  require_relative "async/await"
8
- module Standard
9
- module Procedure
10
- module Async
11
- def self.promises
12
- @promises ||= Promises.new
13
- end
8
+ module StandardProcedure
9
+ module Async
10
+ def self.promises
11
+ @promises ||= Promises.new
14
12
  end
15
13
  end
16
14
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/standard/procedure/async/version"
3
+ require_relative "lib/standard_procedure/async/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "standard-procedure-async"
7
- spec.version = Standard::Procedure::Async::VERSION
7
+ spec.version = StandardProcedure::Async::VERSION
8
8
  spec.authors = ["Rahoul Baruah"]
9
9
  spec.email = ["rahoulb@standardprocedure.app"]
10
10
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard-procedure-async
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
@@ -82,14 +82,14 @@ files:
82
82
  - LICENSE.txt
83
83
  - README.md
84
84
  - Rakefile
85
- - lib/standard/procedure/async.rb
86
- - lib/standard/procedure/async/actor.rb
87
- - lib/standard/procedure/async/await.rb
88
- - lib/standard/procedure/async/error.rb
89
- - lib/standard/procedure/async/promises.rb
90
- - lib/standard/procedure/async/rails_not_loaded_error.rb
91
- - lib/standard/procedure/async/version.rb
92
- - sig/standard/procedure/async.rbs
85
+ - lib/standard_procedure/async.rb
86
+ - lib/standard_procedure/async/actor.rb
87
+ - lib/standard_procedure/async/await.rb
88
+ - lib/standard_procedure/async/error.rb
89
+ - lib/standard_procedure/async/promises.rb
90
+ - lib/standard_procedure/async/rails_not_loaded_error.rb
91
+ - lib/standard_procedure/async/version.rb
92
+ - sig/standard_procedure/async.rbs
93
93
  - standard-procedure-async.gemspec
94
94
  homepage: https://github.com/standard-procedure/async
95
95
  licenses:
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Standard
4
- module Procedure
5
- module Async
6
- VERSION = "0.1.0"
7
- end
8
- end
9
- end