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 +4 -4
- data/CHANGELOG.md +7 -1
- data/Gemfile.lock +1 -1
- data/README.md +58 -22
- data/lib/{standard/procedure → standard_procedure}/async/actor.rb +4 -4
- data/lib/{standard/procedure → standard_procedure}/async/error.rb +1 -1
- data/lib/{standard/procedure → standard_procedure}/async/promises.rb +1 -1
- data/lib/{standard/procedure → standard_procedure}/async/rails_not_loaded_error.rb +1 -1
- data/lib/standard_procedure/async/version.rb +7 -0
- data/lib/{standard/procedure → standard_procedure}/async.rb +4 -6
- data/standard-procedure-async.gemspec +2 -2
- metadata +9 -9
- data/lib/standard/procedure/async/version.rb +0 -9
- /data/lib/{standard/procedure → standard_procedure}/async/await.rb +0 -0
- /data/sig/{standard/procedure → standard_procedure}/async.rbs +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7c712b9a4a30138eb2c834422df444d22da7fbb16da26de1c1ec81db7cf15985
|
4
|
+
data.tar.gz: 71dbabb9e071adb19146e17a275daaad05b0c9f242843b3d957908087f604a91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d84213a0def468819fbd0f63bc42d10a483b13790e1e3ee632b72d63d2d2d9438df2c7a3a586b6acbedba07ceaf8794b999c33cd53df4fb166cf0f5e5f998289
|
7
|
+
data.tar.gz: 3972be625613fa325b92273d594f0e9c213fa4675e03181c39d60e334251f3fea7e6d369a9335dc639994aa182de8ead6785be04d011d21186c56b08badfc9fb
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
-
## [
|
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
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
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
|
-
|
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 [
|
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,
|
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 [
|
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
|
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
|
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
|
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 # =>
|
80
|
-
|
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
|
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
|
-
|
110
|
+
In the example above, you can see how this works.
|
96
111
|
|
97
|
-
|
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
|
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
|
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
|
-
|
126
|
+
No - the call to `greet` will _always_ return "Hello Paul".
|
110
127
|
|
111
|
-
This is because internally, the actor queues all method calls.
|
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
|
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.
|
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 - `
|
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
|
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
|
-
|
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
|
@@ -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
|
9
|
-
module
|
10
|
-
|
11
|
-
|
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/
|
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 =
|
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.
|
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/
|
86
|
-
- lib/
|
87
|
-
- lib/
|
88
|
-
- lib/
|
89
|
-
- lib/
|
90
|
-
- lib/
|
91
|
-
- lib/
|
92
|
-
- sig/
|
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:
|
File without changes
|
File without changes
|