concurrent_rails 0.2.1 → 0.3.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 +8 -6
- data/lib/concurrent_rails/delay_adapter.rb +21 -0
- data/lib/concurrent_rails/future.rb +30 -32
- data/lib/concurrent_rails/future_adapter.rb +21 -0
- data/lib/concurrent_rails/multi.rb +34 -36
- data/lib/concurrent_rails/promises.rb +52 -54
- data/lib/concurrent_rails/railtie.rb +1 -3
- data/lib/concurrent_rails/testing.rb +27 -25
- data/lib/concurrent_rails/version.rb +1 -1
- metadata +19 -19
- data/lib/concurrent_rails/adapters/delay.rb +0 -23
- data/lib/concurrent_rails/adapters/future.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd75cf15aa7d7d0354bea478936bde3967d20a8e38daabff9e3fddf5c11bd5b6
|
4
|
+
data.tar.gz: 8c5faee065ed6de8bb7fedf2b29c3bc371792f579c7a00e6195d4b4e18ee0a29
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 649cf84ed43e2b6c7e70f89840a3091fefea3ade9df62af7d50ee7e7ef309af24729912bd9dbc5adc4112da953d3964e8b0ac4d41b36d3eb256edd73813c979f
|
7
|
+
data.tar.gz: 818ffd9287980e87473f9445b0424a5a006271b3ce4a070a4bd4b6fb08c4405576eff466e4def1c5ddce032614600f3d9a830951d20cfbb29823b7667545780a
|
data/README.md
CHANGED
@@ -174,12 +174,12 @@ irb(main):007:0> multi.errors
|
|
174
174
|
It is worth mention that a failed proc will return `nil`.
|
175
175
|
|
176
176
|
## Testing
|
177
|
-
If you are using RSpec, you will notice that it might not play well with threads. ActiveRecord opens a database connection for every thread and since RSpec tests are wrapped in a transaction, by the time
|
178
|
-
Since none of these solutions were satisfactory to me, I created `ConcurrentRails::Testing` with two strategies: `immediate
|
177
|
+
If you are using RSpec, you will notice that it might not play well with threads. ActiveRecord opens a database connection for every thread and since RSpec tests are wrapped in a transaction, by the time your promise tries to access something on the database, for example, a user, gems like Database Cleaner probably already triggered and deleted the user, resulting in `ActiveRecord::RecordNotFound` errors. You have a couple of solutions like disable transactional fixtures if you are using it or update the Database Cleaner strategy (that will result in much slower tests).
|
178
|
+
Since none of these solutions were satisfactory to me, I created `ConcurrentRails::Testing` with two strategies: `immediate` and `fake`. When you wrap a Promise's `future` with `immediate`, the executor gets replaced from `:io` to `:immediate`. It still returns a promise anyway. This is not the case with `fake` strategy: it executes the task outside the `ConcurrentRails` engine and returns whatever `.value` would return:
|
179
179
|
|
180
|
-
`immediate
|
180
|
+
`immediate` strategy:
|
181
181
|
```ruby
|
182
|
-
irb(main):001:1* result = ConcurrentRails::Testing.immediate
|
182
|
+
irb(main):001:1* result = ConcurrentRails::Testing.immediate do
|
183
183
|
irb(main):002:1* ConcurrentRails::Promises.future { 42 }
|
184
184
|
irb(main):003:0> end
|
185
185
|
=>
|
@@ -191,10 +191,10 @@ irb(main):005:0> result.executor
|
|
191
191
|
=> :immediate # <-- default executor (:io) gets replaced
|
192
192
|
```
|
193
193
|
|
194
|
-
`fake
|
194
|
+
`fake` strategy:
|
195
195
|
|
196
196
|
```ruby
|
197
|
-
irb(main):001:1* result = ConcurrentRails::Testing.fake
|
197
|
+
irb(main):001:1* result = ConcurrentRails::Testing.fake do
|
198
198
|
irb(main):002:1* ConcurrentRails::Promises.future { 42 }
|
199
199
|
irb(main):003:0> end
|
200
200
|
=> 42 # <-- yields the task but does not return a Promise
|
@@ -202,6 +202,8 @@ irb(main):004:0> result.class
|
|
202
202
|
=> Integer
|
203
203
|
```
|
204
204
|
|
205
|
+
You can also set the stragegy globally using `ConcurrentRails::Testing.fake!` or `ConcurrentRails::Testing.immediate!`
|
206
|
+
|
205
207
|
## Further reading
|
206
208
|
|
207
209
|
For more information on how Futures work and how Rails handle multithread check these links:
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ConcurrentRails::DelayAdapter
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
def delay(*args, &task)
|
8
|
+
delay_on(:io, *args, &task)
|
9
|
+
end
|
10
|
+
|
11
|
+
def delay_on(executor, *args, &task)
|
12
|
+
new(executor).delay_on_rails(*args, &task)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def delay_on_rails(*args, &task)
|
17
|
+
@instance = rails_wrapped { delay_on(executor, *args, &task) }
|
18
|
+
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|
@@ -1,49 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
3
|
+
class ConcurrentRails::Future
|
4
|
+
def initialize(executor: :io, &block)
|
5
|
+
@executor = executor
|
6
|
+
@future = run_on_rails(block)
|
7
|
+
ActiveSupport::Deprecation.warn('ConcurrentRails::Future is deprecated. See README for details')
|
8
|
+
end
|
10
9
|
|
11
|
-
|
12
|
-
|
10
|
+
def execute
|
11
|
+
future.execute
|
13
12
|
|
14
|
-
|
15
|
-
|
13
|
+
self
|
14
|
+
end
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
end
|
16
|
+
%i[value value!].each do |method_name|
|
17
|
+
define_method method_name do
|
18
|
+
permit_concurrent_loads do
|
19
|
+
future.__send__(method_name)
|
22
20
|
end
|
23
21
|
end
|
22
|
+
end
|
24
23
|
|
25
|
-
|
24
|
+
delegate :state, :reason, :rejected?, :complete?, :add_observer, to: :future
|
26
25
|
|
27
|
-
|
26
|
+
private
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
28
|
+
def run_on_rails(block)
|
29
|
+
@future = rails_wrapped do
|
30
|
+
Concurrent::Future.new(executor: executor) do
|
31
|
+
rails_wrapped(&block)
|
34
32
|
end
|
35
33
|
end
|
34
|
+
end
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
def rails_wrapped(&block)
|
37
|
+
Rails.application.executor.wrap(&block)
|
38
|
+
end
|
40
39
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
40
|
+
def permit_concurrent_loads(&block)
|
41
|
+
rails_wrapped do
|
42
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads(&block)
|
45
43
|
end
|
46
|
-
|
47
|
-
attr_reader :executor, :future
|
48
44
|
end
|
45
|
+
|
46
|
+
attr_reader :executor, :future
|
49
47
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ConcurrentRails::FutureAdapter
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
def future(*args, &task)
|
8
|
+
future_on(:io, *args, &task)
|
9
|
+
end
|
10
|
+
|
11
|
+
def future_on(executor, *args, &task)
|
12
|
+
new(executor).future_on_rails(*args, &task)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def future_on_rails(*args, &task)
|
17
|
+
@instance = rails_wrapped { future_on(executor, *args, &task) }
|
18
|
+
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|
@@ -1,51 +1,49 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
raise ArgumentError, '#enqueue accepts `Proc`s only' unless actions.all?(Proc)
|
3
|
+
class ConcurrentRails::Multi
|
4
|
+
def self.enqueue(*actions, executor: :io)
|
5
|
+
raise ArgumentError, '#enqueue accepts `Proc`s only' unless actions.all?(Proc)
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def initialize(actions, executor)
|
12
|
-
@actions = actions
|
13
|
-
@executor = executor
|
14
|
-
@exceptions = Concurrent::Array.new
|
15
|
-
end
|
7
|
+
new(actions, executor).enqueue
|
8
|
+
end
|
16
9
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
10
|
+
def initialize(actions, executor)
|
11
|
+
@actions = actions
|
12
|
+
@executor = executor
|
13
|
+
@exceptions = Concurrent::Array.new
|
14
|
+
end
|
23
15
|
|
24
|
-
|
16
|
+
def enqueue
|
17
|
+
@futures = actions.map do |action|
|
18
|
+
f = ConcurrentRails::Future.new(executor: executor, &action)
|
19
|
+
f.add_observer(self)
|
20
|
+
f.execute
|
25
21
|
end
|
26
22
|
|
27
|
-
|
28
|
-
|
29
|
-
end
|
23
|
+
self
|
24
|
+
end
|
30
25
|
|
31
|
-
|
32
|
-
|
33
|
-
|
26
|
+
def compute
|
27
|
+
futures.map(&:value)
|
28
|
+
end
|
34
29
|
|
35
|
-
|
36
|
-
|
37
|
-
|
30
|
+
def compute!
|
31
|
+
futures.map(&:value!)
|
32
|
+
end
|
38
33
|
|
39
|
-
|
40
|
-
|
41
|
-
|
34
|
+
def complete?
|
35
|
+
futures.all?(&:complete?)
|
36
|
+
end
|
42
37
|
|
43
|
-
|
38
|
+
def errors
|
39
|
+
@exceptions
|
40
|
+
end
|
44
41
|
|
45
|
-
|
46
|
-
@exceptions << reason if reason
|
47
|
-
end
|
42
|
+
private
|
48
43
|
|
49
|
-
|
44
|
+
def update(_time, _value, reason)
|
45
|
+
@exceptions << reason if reason
|
50
46
|
end
|
47
|
+
|
48
|
+
attr_reader :actions, :futures, :exceptions, :executor
|
51
49
|
end
|
@@ -1,83 +1,81 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'concurrent_rails/
|
4
|
-
require 'concurrent_rails/
|
3
|
+
require 'concurrent_rails/future_adapter'
|
4
|
+
require 'concurrent_rails/delay_adapter'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
include ConcurrentRails::Adapters::Future
|
6
|
+
class ConcurrentRails::Promises
|
7
|
+
include Concurrent::Promises::FactoryMethods
|
8
|
+
include ConcurrentRails::DelayAdapter
|
9
|
+
include ConcurrentRails::FutureAdapter
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
def initialize(executor)
|
12
|
+
@executor = executor
|
13
|
+
end
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
15
|
+
%i[value value!].each do |method_name|
|
16
|
+
define_method(method_name) do |timeout = nil, timeout_value = nil|
|
17
|
+
permit_concurrent_loads do
|
18
|
+
instance.public_send(method_name, timeout, timeout_value)
|
21
19
|
end
|
22
20
|
end
|
21
|
+
end
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
|
31
|
-
self
|
23
|
+
%i[then chain].each do |chainable|
|
24
|
+
define_method(chainable) do |*args, &task|
|
25
|
+
method = "#{chainable}_on"
|
26
|
+
@instance = rails_wrapped do
|
27
|
+
instance.public_send(method, executor, *args, &task)
|
32
28
|
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def touch
|
36
|
-
@instance = rails_wrapped { instance.touch }
|
37
29
|
|
38
30
|
self
|
39
31
|
end
|
32
|
+
end
|
40
33
|
|
41
|
-
|
42
|
-
|
34
|
+
def touch
|
35
|
+
@instance = rails_wrapped { instance.touch }
|
43
36
|
|
44
|
-
|
45
|
-
|
37
|
+
self
|
38
|
+
end
|
46
39
|
|
47
|
-
|
48
|
-
|
49
|
-
rails_wrapped do
|
50
|
-
@instance = instance.__send__("#{method}_using", executor, *args, &callback_task)
|
51
|
-
end
|
40
|
+
def wait(timeout = nil)
|
41
|
+
result = permit_concurrent_loads { instance.__send__(:wait_until_resolved, timeout) }
|
52
42
|
|
53
|
-
|
43
|
+
timeout ? result : self
|
44
|
+
end
|
45
|
+
|
46
|
+
%i[on_fulfillment on_rejection on_resolution].each do |method|
|
47
|
+
define_method(method) do |*args, &callback_task|
|
48
|
+
rails_wrapped do
|
49
|
+
@instance = instance.__send__("#{method}_using", executor, *args, &callback_task)
|
54
50
|
end
|
55
51
|
|
56
|
-
|
57
|
-
|
58
|
-
@instance = instance.__send__(:add_callback, "callback_#{method}", args, callback_task)
|
59
|
-
end
|
52
|
+
self
|
53
|
+
end
|
60
54
|
|
61
|
-
|
55
|
+
define_method("#{method}!") do |*args, &callback_task|
|
56
|
+
rails_wrapped do
|
57
|
+
@instance = instance.__send__(:add_callback, "callback_#{method}", args, callback_task)
|
62
58
|
end
|
59
|
+
|
60
|
+
self
|
63
61
|
end
|
62
|
+
end
|
64
63
|
|
65
|
-
|
64
|
+
delegate :state, :reason, :rejected?, :resolved?, :fulfilled?, to: :instance
|
66
65
|
|
67
|
-
|
66
|
+
attr_reader :executor
|
68
67
|
|
69
|
-
|
68
|
+
private
|
70
69
|
|
71
|
-
|
72
|
-
|
73
|
-
|
70
|
+
def rails_wrapped(&block)
|
71
|
+
Rails.application.executor.wrap(&block)
|
72
|
+
end
|
74
73
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
end
|
74
|
+
def permit_concurrent_loads(&block)
|
75
|
+
rails_wrapped do
|
76
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads(&block)
|
79
77
|
end
|
80
|
-
|
81
|
-
attr_reader :instance
|
82
78
|
end
|
79
|
+
|
80
|
+
attr_reader :instance
|
83
81
|
end
|
@@ -1,37 +1,39 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
class
|
5
|
-
|
6
|
-
attr_reader :execution_mode
|
3
|
+
class ConcurrentRails::Testing
|
4
|
+
class << self
|
5
|
+
attr_reader :execution_mode
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
7
|
+
%w[immediate fake real].each do |test_mode|
|
8
|
+
define_method(test_mode) do |&task|
|
9
|
+
@execution_mode = test_mode
|
10
|
+
result = task.call
|
11
|
+
@execution_mode = :real
|
13
12
|
|
14
|
-
|
15
|
-
|
13
|
+
result
|
14
|
+
end
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
end
|
16
|
+
define_method("#{test_mode}!") do
|
17
|
+
@execution_mode = test_mode
|
20
18
|
end
|
21
19
|
|
22
|
-
|
23
|
-
|
24
|
-
if ConcurrentRails::Testing.immediate?
|
25
|
-
future_on(:immediate, *args, &task)
|
26
|
-
elsif ConcurrentRails::Testing.fake?
|
27
|
-
yield
|
28
|
-
else
|
29
|
-
super
|
30
|
-
end
|
31
|
-
end
|
20
|
+
define_method("#{test_mode}?") do
|
21
|
+
execution_mode == test_mode
|
32
22
|
end
|
23
|
+
end
|
24
|
+
end
|
33
25
|
|
34
|
-
|
26
|
+
module TestingFuture
|
27
|
+
def future(*args, &task)
|
28
|
+
if ConcurrentRails::Testing.immediate?
|
29
|
+
future_on(:immediate, *args, &task)
|
30
|
+
elsif ConcurrentRails::Testing.fake?
|
31
|
+
yield
|
32
|
+
else
|
33
|
+
super
|
34
|
+
end
|
35
35
|
end
|
36
36
|
end
|
37
|
+
|
38
|
+
ConcurrentRails::Promises.extend(TestingFuture)
|
37
39
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: concurrent_rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Luiz Eduardo Kowalski
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-11-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '5.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest-reporters
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.4'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.4'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: rubocop
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,20 +80,6 @@ dependencies:
|
|
66
80
|
- - ">="
|
67
81
|
- !ruby/object:Gem::Version
|
68
82
|
version: '1.10'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: sqlite3
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - "~>"
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '1.4'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - "~>"
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '1.4'
|
83
83
|
description: Small library to make concurrent-ruby and Rails play nice together
|
84
84
|
email:
|
85
85
|
- luizeduardokowalski@gmail.com
|
@@ -91,9 +91,9 @@ files:
|
|
91
91
|
- README.md
|
92
92
|
- Rakefile
|
93
93
|
- lib/concurrent_rails.rb
|
94
|
-
- lib/concurrent_rails/
|
95
|
-
- lib/concurrent_rails/adapters/future.rb
|
94
|
+
- lib/concurrent_rails/delay_adapter.rb
|
96
95
|
- lib/concurrent_rails/future.rb
|
96
|
+
- lib/concurrent_rails/future_adapter.rb
|
97
97
|
- lib/concurrent_rails/multi.rb
|
98
98
|
- lib/concurrent_rails/promises.rb
|
99
99
|
- lib/concurrent_rails/railtie.rb
|
@@ -121,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
121
|
- !ruby/object:Gem::Version
|
122
122
|
version: '0'
|
123
123
|
requirements: []
|
124
|
-
rubygems_version: 3.2.
|
124
|
+
rubygems_version: 3.2.31
|
125
125
|
signing_key:
|
126
126
|
specification_version: 4
|
127
127
|
summary: Multithread is hard
|
@@ -1,23 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ConcurrentRails::Adapters
|
4
|
-
module Delay
|
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, &task)
|
18
|
-
@instance = rails_wrapped { delay_on(executor, *args, &task) }
|
19
|
-
|
20
|
-
self
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ConcurrentRails::Adapters
|
4
|
-
module Future
|
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, &task)
|
18
|
-
@instance = rails_wrapped { future_on(executor, *args, &task) }
|
19
|
-
|
20
|
-
self
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|