standard-procedure-async 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +134 -0
- data/LICENSE.txt +21 -0
- data/README.md +173 -0
- data/Rakefile +10 -0
- data/lib/standard/procedure/async/actor.rb +64 -0
- data/lib/standard/procedure/async/await.rb +5 -0
- data/lib/standard/procedure/async/error.rb +6 -0
- data/lib/standard/procedure/async/promises.rb +26 -0
- data/lib/standard/procedure/async/rails_not_loaded_error.rb +8 -0
- data/lib/standard/procedure/async/version.rb +9 -0
- data/lib/standard/procedure/async.rb +16 -0
- data/sig/standard/procedure/async.rbs +8 -0
- data/standard-procedure-async.gemspec +36 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b92c0e8440b6f2ad4b0c95cc83ac562ece4124490f15982f1d6531efec3ca2b1
|
4
|
+
data.tar.gz: ba0100a22ccbf8c572489111b841bdee403347e2f3a65172b8584594ebd026e5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ec6d3846f51149189ae18dffbf38af10cbb9c317ac9b546b1e336b726219d040b7b0d5fab65dd4a33c1be079ad093703947236c46ce1345af0f33cc5474bb50d
|
7
|
+
data.tar.gz: fb99264092314bad0f9c4c1e7d1061e2530456bf59b409eadbbe6f5f4a216ad69d71a5abd2fd3a74c4d0c1de0d9fa4f50b7f2053259de9435bfd3e9f7eb34fc8
|
data/.rspec
ADDED
data/.standard.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
standard-procedure-async (0.1.0)
|
5
|
+
concurrent-ruby (>= 1.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
actionpack (7.0.5)
|
11
|
+
actionview (= 7.0.5)
|
12
|
+
activesupport (= 7.0.5)
|
13
|
+
rack (~> 2.0, >= 2.2.4)
|
14
|
+
rack-test (>= 0.6.3)
|
15
|
+
rails-dom-testing (~> 2.0)
|
16
|
+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
17
|
+
actionview (7.0.5)
|
18
|
+
activesupport (= 7.0.5)
|
19
|
+
builder (~> 3.1)
|
20
|
+
erubi (~> 1.4)
|
21
|
+
rails-dom-testing (~> 2.0)
|
22
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
23
|
+
activesupport (7.0.5)
|
24
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
25
|
+
i18n (>= 1.6, < 2)
|
26
|
+
minitest (>= 5.1)
|
27
|
+
tzinfo (~> 2.0)
|
28
|
+
ast (2.4.2)
|
29
|
+
builder (3.2.4)
|
30
|
+
concurrent-ruby (1.2.2)
|
31
|
+
concurrent_rails (0.4.1)
|
32
|
+
railties (>= 5.2)
|
33
|
+
crass (1.0.6)
|
34
|
+
diff-lcs (1.5.0)
|
35
|
+
erubi (1.12.0)
|
36
|
+
i18n (1.13.0)
|
37
|
+
concurrent-ruby (~> 1.0)
|
38
|
+
json (2.6.3)
|
39
|
+
language_server-protocol (3.17.0.3)
|
40
|
+
lint_roller (1.0.0)
|
41
|
+
loofah (2.21.3)
|
42
|
+
crass (~> 1.0.2)
|
43
|
+
nokogiri (>= 1.12.0)
|
44
|
+
method_source (1.0.0)
|
45
|
+
minitest (5.18.0)
|
46
|
+
nokogiri (1.15.2-arm64-darwin)
|
47
|
+
racc (~> 1.4)
|
48
|
+
nokogiri (1.15.2-x86_64-linux)
|
49
|
+
racc (~> 1.4)
|
50
|
+
parallel (1.23.0)
|
51
|
+
parser (3.2.2.1)
|
52
|
+
ast (~> 2.4.1)
|
53
|
+
racc (1.6.2)
|
54
|
+
rack (2.2.7)
|
55
|
+
rack-test (2.1.0)
|
56
|
+
rack (>= 1.3)
|
57
|
+
rails-dom-testing (2.0.3)
|
58
|
+
activesupport (>= 4.2.0)
|
59
|
+
nokogiri (>= 1.6)
|
60
|
+
rails-html-sanitizer (1.6.0)
|
61
|
+
loofah (~> 2.21)
|
62
|
+
nokogiri (~> 1.14)
|
63
|
+
railties (7.0.5)
|
64
|
+
actionpack (= 7.0.5)
|
65
|
+
activesupport (= 7.0.5)
|
66
|
+
method_source
|
67
|
+
rake (>= 12.2)
|
68
|
+
thor (~> 1.0)
|
69
|
+
zeitwerk (~> 2.5)
|
70
|
+
rainbow (3.1.1)
|
71
|
+
rake (13.0.6)
|
72
|
+
regexp_parser (2.8.0)
|
73
|
+
rexml (3.2.5)
|
74
|
+
rspec (3.12.0)
|
75
|
+
rspec-core (~> 3.12.0)
|
76
|
+
rspec-expectations (~> 3.12.0)
|
77
|
+
rspec-mocks (~> 3.12.0)
|
78
|
+
rspec-core (3.12.2)
|
79
|
+
rspec-support (~> 3.12.0)
|
80
|
+
rspec-expectations (3.12.3)
|
81
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
82
|
+
rspec-support (~> 3.12.0)
|
83
|
+
rspec-mocks (3.12.5)
|
84
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
85
|
+
rspec-support (~> 3.12.0)
|
86
|
+
rspec-support (3.12.0)
|
87
|
+
rubocop (1.50.2)
|
88
|
+
json (~> 2.3)
|
89
|
+
parallel (~> 1.10)
|
90
|
+
parser (>= 3.2.0.0)
|
91
|
+
rainbow (>= 2.2.2, < 4.0)
|
92
|
+
regexp_parser (>= 1.8, < 3.0)
|
93
|
+
rexml (>= 3.2.5, < 4.0)
|
94
|
+
rubocop-ast (>= 1.28.0, < 2.0)
|
95
|
+
ruby-progressbar (~> 1.7)
|
96
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
97
|
+
rubocop-ast (1.28.1)
|
98
|
+
parser (>= 3.2.1.0)
|
99
|
+
rubocop-performance (1.16.0)
|
100
|
+
rubocop (>= 1.7.0, < 2.0)
|
101
|
+
rubocop-ast (>= 0.4.0)
|
102
|
+
ruby-progressbar (1.13.0)
|
103
|
+
standard (1.28.2)
|
104
|
+
language_server-protocol (~> 3.17.0.2)
|
105
|
+
lint_roller (~> 1.0)
|
106
|
+
rubocop (~> 1.50.2)
|
107
|
+
standard-custom (~> 1.0.0)
|
108
|
+
standard-performance (~> 1.0.1)
|
109
|
+
standard-custom (1.0.0)
|
110
|
+
lint_roller (~> 1.0)
|
111
|
+
standard-performance (1.0.1)
|
112
|
+
lint_roller (~> 1.0)
|
113
|
+
rubocop-performance (~> 1.16.0)
|
114
|
+
thor (1.2.2)
|
115
|
+
tzinfo (2.0.6)
|
116
|
+
concurrent-ruby (~> 1.0)
|
117
|
+
unicode-display_width (2.4.2)
|
118
|
+
zeitwerk (2.6.8)
|
119
|
+
|
120
|
+
PLATFORMS
|
121
|
+
arm64-darwin-22
|
122
|
+
x86_64-linux
|
123
|
+
|
124
|
+
DEPENDENCIES
|
125
|
+
activesupport
|
126
|
+
concurrent_rails
|
127
|
+
railties
|
128
|
+
rake (~> 13.0)
|
129
|
+
rspec (~> 3.0)
|
130
|
+
standard (~> 1.3)
|
131
|
+
standard-procedure-async!
|
132
|
+
|
133
|
+
BUNDLED WITH
|
134
|
+
2.3.7
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Rahoul Baruah
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
# Standard::Procedure::Async
|
2
|
+
|
3
|
+
## The Actor model
|
4
|
+
|
5
|
+
The [Actor Model](https://en.wikipedia.org/wiki/Actor_model) is widely regarded as one of the safest ways to write thread-safe code. Each "actor" maintains a single internal thread and as messages are received by the actor (via method calls), the thread responds to those messages sequentially. This means that no matter which thread sends a message to the actor, the actor's internal behaviour is always thread-safe.
|
6
|
+
|
7
|
+
The actual implementation does not use a thread-per-object as that could get very costly. Instead, each actor maintains a queue of incoming messages and then uses a [Future](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Promises.html) to actually perform those messages. The future uses a thread which is allocated from Concurrent Ruby's internal thread pool, freeing up resources when the system is quiet and increasing the number of workers when the system is busy.
|
8
|
+
|
9
|
+
## Why does this gem exist?
|
10
|
+
|
11
|
+
Two reasons.
|
12
|
+
|
13
|
+
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
|
+
|
15
|
+
In addition, while concurrent-ruby is an excellent library, it does not work well with Ruby on Rails. The [Rails Executor](https://guides.rubyonrails.org/threading_and_code_execution.html) ensures that any threads interact safely with the framework. But concurrent-ruby knows nothing about the executor.
|
16
|
+
|
17
|
+
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
|
+
|
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.
|
20
|
+
|
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.
|
22
|
+
|
23
|
+
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
|
+
|
25
|
+
## Adding `async` and `await` capabilities to ruby objects
|
26
|
+
|
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.
|
28
|
+
|
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.
|
30
|
+
|
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.
|
32
|
+
|
33
|
+
Example usage:
|
34
|
+
```ruby
|
35
|
+
class MyObject
|
36
|
+
include Standard::Procedure::Async::Actor
|
37
|
+
|
38
|
+
def initialize name
|
39
|
+
@name = name
|
40
|
+
@status = :idle
|
41
|
+
end
|
42
|
+
|
43
|
+
async :report_status do
|
44
|
+
@status
|
45
|
+
end
|
46
|
+
|
47
|
+
async :greet do
|
48
|
+
"Hello #{@name}".freeze
|
49
|
+
end
|
50
|
+
|
51
|
+
async :rename do |new_name|
|
52
|
+
@name = new_name.freeze
|
53
|
+
end
|
54
|
+
|
55
|
+
async :do_some_long_running_task do
|
56
|
+
@status = :in_progress
|
57
|
+
do_part_two_of_the_long_running_task
|
58
|
+
@status
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
async :do_part_two_of_the_long_running_task do
|
64
|
+
sleep 10
|
65
|
+
_rename "John"
|
66
|
+
@status = :done
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
@my_object = MyObject.new "George"
|
71
|
+
|
72
|
+
puts await { @my_object.greet } # => "Hello George"
|
73
|
+
await { @my_object.rename "Ringo" }
|
74
|
+
puts await { @my_object.greet } # => "Hello Ringo"
|
75
|
+
@my_object.rename "Paul" # Note: we didn't use await here - we'll talk about that later
|
76
|
+
puts await { @my_object.greet } # => "Hello Paul"
|
77
|
+
|
78
|
+
@initial_status = @my_object.do_some_long_running_task
|
79
|
+
puts @initial_status # => a internal message object
|
80
|
+
puts @initial_status.value # => :in_progress
|
81
|
+
sleep 11
|
82
|
+
@final_status = await { @my_object.report_status }
|
83
|
+
puts @final_status # => :done
|
84
|
+
puts await { @my_object.greet } # => Hello John
|
85
|
+
```
|
86
|
+
|
87
|
+
### Defining asynchronous methods
|
88
|
+
|
89
|
+
When defining `MyObject`, we use `async` instead of `def` for each method. For example, we use `async :greet` instead of `def greet`. This creates two methods - `greet` and `_greet`. `_greet` is the actual implementation of the method and `greet` is the asynchronous wrapper around it.
|
90
|
+
|
91
|
+
### Awaiting the results from those methods
|
92
|
+
|
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.
|
94
|
+
|
95
|
+
If you need the return value from the method, there are two ways to access it.
|
96
|
+
|
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.
|
98
|
+
|
99
|
+
### The sequence of asynchronous calls
|
100
|
+
|
101
|
+
In the example above there is the following code:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
@my_object.rename "Paul"
|
105
|
+
puts await { @my_object.greet } # => "Hello Paul"
|
106
|
+
```
|
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.
|
108
|
+
|
109
|
+
However, it will _always_ return "Hello Paul".
|
110
|
+
|
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.
|
112
|
+
|
113
|
+
Another example of the same behaviour is in `do_some_long_running_task`:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
async :do_some_long_running_task do
|
117
|
+
@status = :in_progress
|
118
|
+
do_part_two_of_the_long_running_task
|
119
|
+
@status
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
When `@my_object.do_some_long_running_task` is called, the message `do_some_long_running_task` is added to the queue.
|
124
|
+
|
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.
|
126
|
+
|
127
|
+
### The rules of using actors
|
128
|
+
|
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`.
|
132
|
+
- 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
|
+
|
134
|
+
## Making Rails work with Concurrent-Ruby
|
135
|
+
|
136
|
+
[Concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) is amazing, providing an extensive suite of tools to make multi-threaded programming as safe as it can possibly be in ruby (which isn't perfect, as ruby's dynamic semantics make it impossible to be completely safe).
|
137
|
+
|
138
|
+
However, concurrent-ruby doesn't play well with [Ruby on Rails](https://rubyonrails.org). Rails is a big complex framework that auto-loads code and has lots of data stored in class variables (which are effectively globals that can be written to and read from any thread at any time). Therefore it includes the [Rails Executor](https://guides.rubyonrails.org/threading_and_code_execution.html) which ensures that the framework is aware when other threads may be touching Rails code.
|
139
|
+
|
140
|
+
This gem checks to see if `Rails` is defined, and if so, it attempts to load [Luiz Kowalski](https://github.com/luizkowalski)'s [concurrent_rails](https://github.com/luizkowalski/concurrent_rails) gem. This provides a wrapper around concurrent-ruby's [Promises](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Promises.html) factory that ensures the Rails Executor is loaded before the threaded code is run. This gem then uses the concurrent-rails Future instead of concurrent-ruby's Future, so it can be sure that your code is both thread-safe and rails-safe.
|
141
|
+
|
142
|
+
## Installation
|
143
|
+
|
144
|
+
Add this line to your application's Gemfile:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
gem 'standard-procedure-async'
|
148
|
+
```
|
149
|
+
|
150
|
+
If you are using Ruby on Rails, you must also add in the `concurrent_rails` gem, or this gem will raise an error when you try to use it.
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
gem 'concurrent_rails'
|
154
|
+
```
|
155
|
+
|
156
|
+
And then execute:
|
157
|
+
|
158
|
+
$ bundle install
|
159
|
+
|
160
|
+
Or install it yourself as:
|
161
|
+
|
162
|
+
$ gem install standard-procedure-async
|
163
|
+
|
164
|
+
## Development
|
165
|
+
|
166
|
+
Coming soon
|
167
|
+
## Contributing
|
168
|
+
|
169
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/standard-procedure-async.
|
170
|
+
|
171
|
+
## License
|
172
|
+
|
173
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent/array"
|
4
|
+
require "concurrent/mvar"
|
5
|
+
require "concurrent/immutable_struct"
|
6
|
+
|
7
|
+
module Standard::Procedure::Async
|
8
|
+
module Actor
|
9
|
+
def self.included base
|
10
|
+
base.class_eval do
|
11
|
+
extend ClassMethods
|
12
|
+
|
13
|
+
def initialize *args
|
14
|
+
super
|
15
|
+
@_messages = Concurrent::Array.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
def async name, &implementation
|
22
|
+
name = name.to_sym
|
23
|
+
implementation_name = :"_#{name}"
|
24
|
+
|
25
|
+
define_method name.to_sym do |*args, &block|
|
26
|
+
_add_message_to_queue(implementation_name, *args, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
define_method implementation_name do |*args, &block|
|
30
|
+
implementation.call(*args, &block)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_reader :_messages
|
38
|
+
|
39
|
+
def _add_message_to_queue name, *args, &block
|
40
|
+
message = Message.new(self, name, args, block, Concurrent::MVar.new)
|
41
|
+
_messages << message
|
42
|
+
_perform_messages if _messages.count == 1
|
43
|
+
message
|
44
|
+
end
|
45
|
+
|
46
|
+
def _perform_messages
|
47
|
+
Standard::Procedure::Async.promises.future do
|
48
|
+
while (message = _messages.shift)
|
49
|
+
message.call
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Message < Concurrent::ImmutableStruct.new(:target, :name, :args, :block, :result)
|
55
|
+
def value
|
56
|
+
result.take
|
57
|
+
end
|
58
|
+
|
59
|
+
def call
|
60
|
+
result.put target.send(name, *args, &block)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rails_not_loaded_error"
|
4
|
+
require "concurrent/promises"
|
5
|
+
|
6
|
+
module Standard::Procedure::Async
|
7
|
+
class Promises
|
8
|
+
def initialize
|
9
|
+
@promises = rails_is_loaded? ? ConcurrentRails::Promises : Concurrent::Promises
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :promises
|
13
|
+
|
14
|
+
def future &block
|
15
|
+
@promises.future(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def rails_is_loaded?
|
21
|
+
return false if !defined?(Rails::Railtie)
|
22
|
+
raise RailsNotLoadedError if !defined?(ConcurrentRails) || !defined?(Rails::Railtie)
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "async/version"
|
4
|
+
require_relative "async/error"
|
5
|
+
require_relative "async/promises"
|
6
|
+
require_relative "async/actor"
|
7
|
+
require_relative "async/await"
|
8
|
+
module Standard
|
9
|
+
module Procedure
|
10
|
+
module Async
|
11
|
+
def self.promises
|
12
|
+
@promises ||= Promises.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/standard/procedure/async/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "standard-procedure-async"
|
7
|
+
spec.version = Standard::Procedure::Async::VERSION
|
8
|
+
spec.authors = ["Rahoul Baruah"]
|
9
|
+
spec.email = ["rahoulb@standardprocedure.app"]
|
10
|
+
|
11
|
+
spec.summary = "A simple wrapper around Concurrent::Future to make concurrent-ruby Rails-friendly."
|
12
|
+
spec.description = "Provides a wrapper around concurrent-ruby's Concurrent::Future to automatically wrap it in a Rails-friendly executor."
|
13
|
+
spec.homepage = "https://github.com/standard-procedure/async"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.7.0"
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/standard-procedure/async"
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/standard-procedure/async/blob/main/CHANGELOG.md"
|
22
|
+
|
23
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
spec.add_dependency "concurrent-ruby", ">= 1.0"
|
33
|
+
spec.add_development_dependency "activesupport"
|
34
|
+
spec.add_development_dependency "railties"
|
35
|
+
spec.add_development_dependency "concurrent_rails"
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: standard-procedure-async
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rahoul Baruah
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-05-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: concurrent-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: railties
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: concurrent_rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Provides a wrapper around concurrent-ruby's Concurrent::Future to automatically
|
70
|
+
wrap it in a Rails-friendly executor.
|
71
|
+
email:
|
72
|
+
- rahoulb@standardprocedure.app
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".rspec"
|
78
|
+
- ".standard.yml"
|
79
|
+
- CHANGELOG.md
|
80
|
+
- Gemfile
|
81
|
+
- Gemfile.lock
|
82
|
+
- LICENSE.txt
|
83
|
+
- README.md
|
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
|
93
|
+
- standard-procedure-async.gemspec
|
94
|
+
homepage: https://github.com/standard-procedure/async
|
95
|
+
licenses:
|
96
|
+
- MIT
|
97
|
+
metadata:
|
98
|
+
allowed_push_host: https://rubygems.org
|
99
|
+
homepage_uri: https://github.com/standard-procedure/async
|
100
|
+
source_code_uri: https://github.com/standard-procedure/async
|
101
|
+
changelog_uri: https://github.com/standard-procedure/async/blob/main/CHANGELOG.md
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.7.0
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubygems_version: 3.4.12
|
118
|
+
signing_key:
|
119
|
+
specification_version: 4
|
120
|
+
summary: A simple wrapper around Concurrent::Future to make concurrent-ruby Rails-friendly.
|
121
|
+
test_files: []
|