concurrent_rails 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +43 -7
- data/lib/concurrent_rails/promises.rb +44 -17
- data/lib/concurrent_rails/testing.rb +27 -10
- data/lib/concurrent_rails/version.rb +1 -1
- metadata +17 -8
- data/lib/concurrent_rails/combinator_adapter.rb +0 -31
- data/lib/concurrent_rails/delay_adapter.rb +0 -24
- data/lib/concurrent_rails/future_adapter.rb +0 -24
- data/lib/concurrent_rails/railtie.rb +0 -9
- data/lib/concurrent_rails/schedule_adapter.rb +0 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 18c6a530ca05cc94c234d366dc6b93cec5c646e3b7cb0413858ea066ced8c5a8
|
|
4
|
+
data.tar.gz: 3ef23ca30903e60b4418b5117a73c05d0effc00979b765532e5565239468fa31
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e22fe7e8dfffd950cca1e3e70078cd5a5f705faff659269430070b3e871347fd1161765b3c301a553cf36e64c8d38d72670d4dfd501034025987c17e705752f
|
|
7
|
+
data.tar.gz: 82dda70e0a8d9f2979c42310b9a2766cd906febc41349b05ce55ee15aee308af7f0c20a46454d182ccb24546eeefeaa9fe3934020ebbeff4437429cc9d445270
|
data/README.md
CHANGED
|
@@ -8,11 +8,11 @@ The goal of this gem is to provide a simple library that allows the developer to
|
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
11
|
-
This library provides
|
|
11
|
+
This library provides `ConcurrentRails::Promises`, a Rails-aware wrapper around [`Concurrent::Promises`](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Promises.html), plus `ConcurrentRails::Testing` helpers for your test suite.
|
|
12
12
|
|
|
13
13
|
### Promises
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
All you have to do is call `#future` and pass a block to be executed asynchronously:
|
|
16
16
|
|
|
17
17
|
```ruby
|
|
18
18
|
irb(main):001:0> future = ConcurrentRails::Promises.future(5) { |v| sleep(v); 42 }
|
|
@@ -33,11 +33,40 @@ The benefit of `Promises` over a pure `Future` class is that you can chain futur
|
|
|
33
33
|
|
|
34
34
|
```ruby
|
|
35
35
|
irb(main):001:0> future = ConcurrentRails::Promises.future { 42 }.then { |v| v * 2 }
|
|
36
|
-
=> #<ConcurrentRails::Promises:0x00007fe92eba3460 @
|
|
36
|
+
=> #<ConcurrentRails::Promises:0x00007fe92eba3460 @instance=#...
|
|
37
37
|
irb(main):002:0> future.value
|
|
38
38
|
=> 84
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
`then` and `chain` return a **new** `ConcurrentRails::Promises` instance and leave the receiver untouched, matching `Concurrent::Promises` semantics. This means you can branch multiple chains off the same future:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
root = ConcurrentRails::Promises.future { expensive_call }
|
|
45
|
+
doubled = root.then { |v| v * 2 }
|
|
46
|
+
tripled = root.then { |v| v * 3 } # branches off root, not doubled
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Combinators and factories
|
|
50
|
+
|
|
51
|
+
`zip` and `any_resolved_future` combine multiple promises; `fulfilled_future` and `rejected_future` create already-resolved ones:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
a = ConcurrentRails::Promises.future { 1 }
|
|
55
|
+
b = ConcurrentRails::Promises.future { 2 }
|
|
56
|
+
|
|
57
|
+
ConcurrentRails::Promises.zip(a, b).value # => [1, 2]
|
|
58
|
+
ConcurrentRails::Promises.any_resolved_future(a, b).value # => first to settle
|
|
59
|
+
ConcurrentRails::Promises.fulfilled_future(42).value # => 42
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Scheduled futures
|
|
63
|
+
|
|
64
|
+
`schedule` runs a task after the given delay (in seconds):
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
ConcurrentRails::Promises.schedule(10) { cleanup! }
|
|
68
|
+
```
|
|
69
|
+
|
|
41
70
|
### Delayed futures
|
|
42
71
|
|
|
43
72
|
A delayed future is a Future that is enqueued but not run until `#touch` or any other method that requires a resolution is called.
|
|
@@ -50,7 +79,7 @@ irb(main):003:0> delay.state
|
|
|
50
79
|
=> :pending
|
|
51
80
|
|
|
52
81
|
irb(main):004:0> delay.touch
|
|
53
|
-
=> #<
|
|
82
|
+
=> #<ConcurrentRails::Promises:0x00007f8b55333d48 ...
|
|
54
83
|
|
|
55
84
|
irb(main):005:0> delay.state
|
|
56
85
|
=> :fulfilled
|
|
@@ -79,7 +108,12 @@ delay = ConcurrentRails::Promises.delay { complex_find_user_query }.
|
|
|
79
108
|
delay.touch
|
|
80
109
|
```
|
|
81
110
|
|
|
82
|
-
All of these callbacks have a bang version (e.g. `on_fulfillment!`). The bang version
|
|
111
|
+
All of these callbacks have a bang version (e.g. `on_fulfillment!`). The bang version runs the callback synchronously on the thread that resolved the future, while the version without bang runs it asynchronously on the promise's executor.
|
|
112
|
+
|
|
113
|
+
## Caveats
|
|
114
|
+
|
|
115
|
+
* `Current` attributes (`ActiveSupport::CurrentAttributes`) are **not** propagated to the future's thread. Each task runs inside `Rails.application.executor.wrap`, which resets per-execution state, so `Current.user` and friends will be `nil` inside the block. Pass what you need as arguments instead.
|
|
116
|
+
* `#value` and `#wait` block the calling thread. In development, a thread blocked inside a future wait counts as a running execution, so code reloading has to wait for it. Prefer `#touch` plus callbacks when you don't need the result immediately.
|
|
83
117
|
|
|
84
118
|
## Testing
|
|
85
119
|
|
|
@@ -112,7 +146,9 @@ irb(main):004:0> result.class
|
|
|
112
146
|
=> Integer
|
|
113
147
|
```
|
|
114
148
|
|
|
115
|
-
|
|
149
|
+
Both strategies also apply to `delay` and `schedule` (`fake` runs a scheduled task right away, ignoring the delay).
|
|
150
|
+
|
|
151
|
+
The block form only changes the mode for the current thread and restores the previous mode when the block exits, even if it raises. You can also set the strategy globally using `ConcurrentRails::Testing.fake!` or `ConcurrentRails::Testing.immediate!`, and reset it with `ConcurrentRails::Testing.real!`
|
|
116
152
|
|
|
117
153
|
## Further reading
|
|
118
154
|
|
|
@@ -127,7 +163,7 @@ For more information on how Futures works and how Rails handles multithread chec
|
|
|
127
163
|
Add this line to your application's Gemfile:
|
|
128
164
|
|
|
129
165
|
```ruby
|
|
130
|
-
gem 'concurrent_rails', '~> 0.
|
|
166
|
+
gem 'concurrent_rails', '~> 0.9'
|
|
131
167
|
```
|
|
132
168
|
|
|
133
169
|
And then execute:
|
|
@@ -2,14 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
module ConcurrentRails
|
|
4
4
|
class Promises
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
class << self
|
|
6
|
+
%i[future delay schedule].each do |factory|
|
|
7
|
+
define_method(factory) do |*args, &task|
|
|
8
|
+
public_send(:"#{factory}_on", :io, *args, &task)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
define_method(:"#{factory}_on") do |executor, *args, &task|
|
|
12
|
+
new(executor, Concurrent::Promises.public_send(:"#{factory}_on", executor, *args, &wrap_task(task)))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def zip(*promises)
|
|
17
|
+
new(:io, Concurrent::Promises.zip(*unwrap(promises)))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def any_resolved_future(*promises)
|
|
21
|
+
new(:io, Concurrent::Promises.any_resolved_future(*unwrap(promises)))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fulfilled_future(value, executor = :io)
|
|
25
|
+
new(executor, Concurrent::Promises.fulfilled_future(value))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def rejected_future(reason, executor = :io)
|
|
29
|
+
new(executor, Concurrent::Promises.rejected_future(reason))
|
|
30
|
+
end
|
|
10
31
|
|
|
11
|
-
|
|
32
|
+
def wrap_task(task)
|
|
33
|
+
proc { |*args| Rails.application.executor.wrap { task.call(*args) } }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def unwrap(promises)
|
|
39
|
+
promises.map { |p| p.is_a?(Promises) ? p.__send__(:instance) : p }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(executor, instance)
|
|
12
44
|
@executor = executor
|
|
45
|
+
@instance = instance
|
|
13
46
|
end
|
|
14
47
|
|
|
15
48
|
%i[value value!].each do |method_name|
|
|
@@ -20,37 +53,31 @@ module ConcurrentRails
|
|
|
20
53
|
|
|
21
54
|
%i[then chain].each do |chainable|
|
|
22
55
|
define_method(chainable) do |*args, &task|
|
|
23
|
-
|
|
24
|
-
wrapped_task = proc { |*a| rails_wrapped { task.call(*a) } }
|
|
25
|
-
@instance = instance.public_send(method, executor, *args, &wrapped_task)
|
|
26
|
-
|
|
27
|
-
self
|
|
56
|
+
self.class.new(executor, instance.public_send(:"#{chainable}_on", executor, *args, &self.class.wrap_task(task)))
|
|
28
57
|
end
|
|
29
58
|
end
|
|
30
59
|
|
|
31
60
|
def touch
|
|
32
|
-
|
|
61
|
+
instance.touch
|
|
33
62
|
|
|
34
63
|
self
|
|
35
64
|
end
|
|
36
65
|
|
|
37
66
|
def wait(timeout = nil)
|
|
38
|
-
result = rails_wrapped { instance.
|
|
67
|
+
result = rails_wrapped { instance.wait(timeout) }
|
|
39
68
|
|
|
40
69
|
timeout ? result : self
|
|
41
70
|
end
|
|
42
71
|
|
|
43
72
|
%i[on_fulfillment on_rejection on_resolution].each do |method|
|
|
44
73
|
define_method(method) do |*args, &callback_task|
|
|
45
|
-
|
|
46
|
-
@instance = instance.__send__(:"#{method}_using", executor, *args, &wrapped_callback)
|
|
74
|
+
instance.public_send(:"#{method}_using", executor, *args, &self.class.wrap_task(callback_task))
|
|
47
75
|
|
|
48
76
|
self
|
|
49
77
|
end
|
|
50
78
|
|
|
51
79
|
define_method(:"#{method}!") do |*args, &callback_task|
|
|
52
|
-
|
|
53
|
-
@instance = instance.__send__(:add_callback, "callback_#{method}", args, wrapped_callback)
|
|
80
|
+
instance.public_send(:"#{method}!", *args, &self.class.wrap_task(callback_task))
|
|
54
81
|
|
|
55
82
|
self
|
|
56
83
|
end
|
|
@@ -2,20 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
module ConcurrentRails
|
|
4
4
|
class Testing
|
|
5
|
+
ISOLATION_KEY = :concurrent_rails_testing_mode
|
|
6
|
+
|
|
5
7
|
class << self
|
|
6
|
-
|
|
8
|
+
def execution_mode
|
|
9
|
+
ActiveSupport::IsolatedExecutionState[ISOLATION_KEY] || @default_mode || "real" # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
10
|
+
end
|
|
7
11
|
|
|
8
12
|
%w[immediate fake real].each do |test_mode|
|
|
9
13
|
define_method(test_mode) do |&task|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@execution_mode = "real"
|
|
14
|
+
previous = ActiveSupport::IsolatedExecutionState[ISOLATION_KEY]
|
|
15
|
+
ActiveSupport::IsolatedExecutionState[ISOLATION_KEY] = test_mode
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
task.call
|
|
18
|
+
ensure
|
|
19
|
+
ActiveSupport::IsolatedExecutionState[ISOLATION_KEY] = previous
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
define_method(:"#{test_mode}!") do
|
|
18
|
-
@
|
|
23
|
+
@default_mode = test_mode
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
define_method(:"#{test_mode}?") do
|
|
@@ -25,17 +30,29 @@ module ConcurrentRails
|
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
module TestingFuture
|
|
28
|
-
|
|
33
|
+
%i[future delay].each do |factory|
|
|
34
|
+
define_method(factory) do |*args, &task|
|
|
35
|
+
if ConcurrentRails::Testing.immediate?
|
|
36
|
+
public_send(:"#{factory}_on", :immediate, *args, &task)
|
|
37
|
+
elsif ConcurrentRails::Testing.fake?
|
|
38
|
+
task.call(*args)
|
|
39
|
+
else
|
|
40
|
+
super(*args, &task)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def schedule(delay, *args, &)
|
|
29
46
|
if ConcurrentRails::Testing.immediate?
|
|
30
|
-
|
|
47
|
+
schedule_on(:immediate, delay, *args, &)
|
|
31
48
|
elsif ConcurrentRails::Testing.fake?
|
|
32
|
-
yield
|
|
49
|
+
yield(*args)
|
|
33
50
|
else
|
|
34
51
|
super
|
|
35
52
|
end
|
|
36
53
|
end
|
|
37
54
|
end
|
|
38
55
|
|
|
39
|
-
ConcurrentRails::Promises.
|
|
56
|
+
ConcurrentRails::Promises.singleton_class.prepend(TestingFuture)
|
|
40
57
|
end
|
|
41
58
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: concurrent_rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Luiz Eduardo Kowalski
|
|
@@ -9,6 +9,20 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: concurrent-ruby
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.3'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: railties
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -48,12 +62,7 @@ files:
|
|
|
48
62
|
- README.md
|
|
49
63
|
- Rakefile
|
|
50
64
|
- lib/concurrent_rails.rb
|
|
51
|
-
- lib/concurrent_rails/combinator_adapter.rb
|
|
52
|
-
- lib/concurrent_rails/delay_adapter.rb
|
|
53
|
-
- lib/concurrent_rails/future_adapter.rb
|
|
54
65
|
- lib/concurrent_rails/promises.rb
|
|
55
|
-
- lib/concurrent_rails/railtie.rb
|
|
56
|
-
- lib/concurrent_rails/schedule_adapter.rb
|
|
57
66
|
- lib/concurrent_rails/testing.rb
|
|
58
67
|
- lib/concurrent_rails/version.rb
|
|
59
68
|
homepage: https://github.com/luizkowalski/concurrent_rails
|
|
@@ -71,14 +80,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
71
80
|
requirements:
|
|
72
81
|
- - ">="
|
|
73
82
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '3.
|
|
83
|
+
version: '3.3'
|
|
75
84
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
85
|
requirements:
|
|
77
86
|
- - ">="
|
|
78
87
|
- !ruby/object:Gem::Version
|
|
79
88
|
version: '0'
|
|
80
89
|
requirements: []
|
|
81
|
-
rubygems_version: 4.0.
|
|
90
|
+
rubygems_version: 4.0.16
|
|
82
91
|
specification_version: 4
|
|
83
92
|
summary: Multithread is hard
|
|
84
93
|
test_files: []
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ConcurrentRails
|
|
4
|
-
module CombinatorAdapter
|
|
5
|
-
extend ActiveSupport::Concern
|
|
6
|
-
|
|
7
|
-
class_methods do
|
|
8
|
-
def zip(*promises)
|
|
9
|
-
new(:io).tap { |p| p.instance_variable_set(:@instance, Concurrent::Promises.zip(*unwrap(promises))) }
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def any_resolved_future(*promises)
|
|
13
|
-
new(:io).tap { |p| p.instance_variable_set(:@instance, Concurrent::Promises.any_resolved_future(*unwrap(promises))) }
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def fulfilled_future(value, executor = :io)
|
|
17
|
-
new(executor).tap { |p| p.instance_variable_set(:@instance, Concurrent::Promises.fulfilled_future(value)) }
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def rejected_future(reason, executor = :io)
|
|
21
|
-
new(executor).tap { |p| p.instance_variable_set(:@instance, Concurrent::Promises.rejected_future(reason)) }
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def unwrap(promises)
|
|
27
|
-
promises.map { |p| p.is_a?(ConcurrentRails::Promises) ? p.__send__(:instance) : p }
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ConcurrentRails
|
|
4
|
-
module DelayAdapter
|
|
5
|
-
extend ActiveSupport::Concern
|
|
6
|
-
|
|
7
|
-
class_methods do
|
|
8
|
-
def delay(*args, &task)
|
|
9
|
-
delay_on(:io, *args, &task)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def delay_on(executor, *args, &task)
|
|
13
|
-
new(executor).delay_on_rails(*args, &task)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def delay_on_rails(*args)
|
|
18
|
-
wrapped_task = proc { |*a| rails_wrapped { yield(*a) } }
|
|
19
|
-
@instance = delay_on(executor, *args, &wrapped_task)
|
|
20
|
-
|
|
21
|
-
self
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ConcurrentRails
|
|
4
|
-
module FutureAdapter
|
|
5
|
-
extend ActiveSupport::Concern
|
|
6
|
-
|
|
7
|
-
class_methods do
|
|
8
|
-
def future(*args, &task)
|
|
9
|
-
future_on(:io, *args, &task)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def future_on(executor, *args, &task)
|
|
13
|
-
new(executor).future_on_rails(*args, &task)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def future_on_rails(*args)
|
|
18
|
-
wrapped_task = proc { |*a| rails_wrapped { yield(*a) } }
|
|
19
|
-
@instance = future_on(executor, *args, &wrapped_task)
|
|
20
|
-
|
|
21
|
-
self
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ConcurrentRails
|
|
4
|
-
module ScheduleAdapter
|
|
5
|
-
extend ActiveSupport::Concern
|
|
6
|
-
|
|
7
|
-
class_methods do
|
|
8
|
-
def schedule(delay, *args, &task)
|
|
9
|
-
schedule_on(:io, delay, *args, &task)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def schedule_on(executor, delay, *args, &task)
|
|
13
|
-
new(executor).schedule_on_rails(delay, *args, &task)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def schedule_on_rails(delay, *args)
|
|
18
|
-
wrapped_task = proc { |*a| rails_wrapped { yield(*a) } }
|
|
19
|
-
@instance = schedule_on(executor, delay, *args, &wrapped_task)
|
|
20
|
-
|
|
21
|
-
self
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|