concurrent_rails 0.1.8 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 257bcdcbd99076d7b73c193297ce5bc410c1fd5962bb823b965b5abbdf21197c
4
- data.tar.gz: 5ae2230ba4416193fac70bcd9500673e7d96081491786717eb8d6ce95c938f92
3
+ metadata.gz: 9d92e4343775ab47bf053ce94e272124a6b703e4bfb087483c0ff6aa45257744
4
+ data.tar.gz: 43c84651bafee307b178694a5a3113d9e21d1a8c6995daccc8ffee5963e62170
5
5
  SHA512:
6
- metadata.gz: df4405c6f8e8efe155dea24db8a3799955b59c4d3a93118153b557711941c66428027caea64d245df4437be49d3d0482ffb2461bad863b38877e152f57eb6970
7
- data.tar.gz: 8c11ffcd0afbeedf533a8da19ca19f79a1e2566e326946f55e699337f45c179a2f245f5b4fd46f62b694a9803ac317cf0bfb6dec95c38285cf3487c5631a4457
6
+ metadata.gz: 23726c05ee5c47a4cdfa911ae6ff537b87bb72c70cf71cb0ede046682951405c14a0d960dccd4dae0d79921fad335013f0a2afd5ca12b1f949030bdfa1cf983f
7
+ data.tar.gz: 7b40732c370731530f7295d4f9cb4f40ef7f50c07d9ff3a11e17e9101d78622c5005750d1894b78244536c34fcc87266ab81c31283230ef670948d1ca09b7d3e
data/README.md CHANGED
@@ -2,8 +2,7 @@
2
2
 
3
3
  ![status](https://github.com/luizkowalski/concurrent_rails/actions/workflows/ruby.yml/badge.svg?branch=master)
4
4
 
5
- Multithread is hard. [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) did an amazing job
6
- implementing the concepts of multithread in the Ruby world. The problem is that Rails doesn't play nice with it. Rails have a complex way of managing threads called Executor and concurrent-ruby (most specifically, [Future](https://github.com/ruby-concurrency/concurrent-ruby/blob/master/docs-source/future.md)) does not work seamlessly with it.
5
+ Multithread is hard. [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) did an amazing job implementing the concepts of multithread in the Ruby world. The problem is that Rails doesn't play nice with it. Rails have a complex way of managing threads called Executor and concurrent-ruby (most specifically, [Future](https://github.com/ruby-concurrency/concurrent-ruby/blob/master/docs-source/future.md)) does not work seamlessly with it.
7
6
 
8
7
  The goal of this gem is to provide a simple library that allows the developer to work with Futures without having to care about Rails's Executor and the whole pack of problems that come with it: autoload, thread pools, active record connections, etc.
9
8
 
@@ -13,8 +12,7 @@ This library provides three classes that will help you run tasks in parallel: `C
13
12
 
14
13
  ### Promises
15
14
 
16
- `Promises` is the recommended way from `concurrent-ruby` to create `Future`s as `Concurrent::Future` will be deprecated at some point.
17
- Similar to other classes, all you have to do is call `.future` helper and pass a block:
15
+ `Promises` is the recommended way from `concurrent-ruby` to create `Future`s as `Concurrent::Future` will be deprecated at some point. All you have to do is call `#future` and pass a block to be executed asynchronously:
18
16
 
19
17
  ```ruby
20
18
  irb(main):001:0> future = ConcurrentRails::Promises.future(5) { |v| sleep(v); 42 }
@@ -40,9 +38,52 @@ irb(main):002:0> future.value
40
38
  => 84
41
39
  ```
42
40
 
43
- ### Future
41
+ ### Delayed futures
44
42
 
45
- `ConcurrentRails::Future` will execute your code in a separated thread and you can check the progress of it whenever you need it. When the task is ready, you can access the result with `#result` function:
43
+ Delayed future is a future that is enqueued but not run until `#touch` or any other method that requires a resolution is called.
44
+
45
+ ```ruby
46
+ irb(main):002:0> delay = ConcurrentRails::Promises.delay { 42 }
47
+ => #<ConcurrentRails::Promises:0x00007f8b55333d48 @executor=:io, @instan...
48
+
49
+ irb(main):003:0> delay.state
50
+ => :pending
51
+
52
+ irb(main):004:0> delay.touch
53
+ => #<Concurrent::Promises::Future:0x00007f8b553325b0 pending>
54
+
55
+ irb(main):005:0> delay.state
56
+ => :fulfilled
57
+
58
+ irb(main):006:0> delay.value
59
+ => 42
60
+ ```
61
+
62
+ Three methods will trigger a resolution: `#touch`, `#value` and `#wait`: `#touch` will simply trigger the execution but won't block the main thread, while `#wait` and `#value` will block the main thread until a resolution is given.
63
+
64
+ ### Callbacks
65
+
66
+ Delayed and regular futures can set a callback to be executed after the resolution of the future. There are three different callbacks:
67
+
68
+ * `on_resolution`: runs after the future is resolved and yields three parameters to the callback in the following order: `true/false` for future's fulfillment, `value` as the result of the future execution, and `reason`, that will be `nil` if the future fulfilled or the error that the future triggered.
69
+
70
+ * `on_fulfillment`: runs after the future is fulfilled and yields `value` to the callback
71
+
72
+ * `on_rejection`: runs after the future is rejected and yields the `error` to the callback
73
+
74
+ ```ruby
75
+ delay = ConcurrentRails::Promises.delay { complex_find_user_query }.
76
+ on_fulfillment { |user| user.update!(name: 'John Doe') }.
77
+ on_rejection { |reason| log_error(reason) }
78
+
79
+ delay.touch
80
+ ```
81
+
82
+ All of these callbacks have a bang version (e.g. `on_fulfillment!`). The bang version will execute the callback on the same thread pool that was initially set up and the version without bang will run asynchronously on a different executor.
83
+
84
+ ### (Deprecated) Future
85
+
86
+ `ConcurrentRails::Future` will execute your code in a separate thread and you can check the progress of it whenever you need it. When the task is ready, you can access the result with `#result` function:
46
87
 
47
88
  ```ruby
48
89
  irb(main):001:0> future = ConcurrentRails::Future.new do
@@ -81,7 +122,7 @@ irb(main):005:0> future.reason
81
122
  => #<ZeroDivisionError: divided by 0>
82
123
  ```
83
124
 
84
- ### Multi
125
+ ### (Deprecated) Multi
85
126
 
86
127
  `ConcurrentRails::Multi` will let you execute multiple tasks in parallel and aggregate the results of each task when they are done. `Multi` accepts an undefined number of `Proc`s.
87
128
 
@@ -132,6 +173,39 @@ irb(main):007:0> multi.errors
132
173
 
133
174
  It is worth mention that a failed proc will return `nil`.
134
175
 
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 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
+
180
+ `immediate` strategy:
181
+ ```ruby
182
+ irb(main):001:1* result = ConcurrentRails::Testing.immediate do
183
+ irb(main):002:1* ConcurrentRails::Promises.future { 42 }
184
+ irb(main):003:0> end
185
+ =>
186
+ #<ConcurrentRails::Promises:0x000000013e5fc870
187
+ ...
188
+ irb(main):004:0> result.class
189
+ => ConcurrentRails::Promises # <-- Still a `ConcurrentRails::Promises` class
190
+ irb(main):005:0> result.executor
191
+ => :immediate # <-- default executor (:io) gets replaced
192
+ ```
193
+
194
+ `fake` strategy:
195
+
196
+ ```ruby
197
+ irb(main):001:1* result = ConcurrentRails::Testing.fake do
198
+ irb(main):002:1* ConcurrentRails::Promises.future { 42 }
199
+ irb(main):003:0> end
200
+ => 42 # <-- yields the task but does not return a Promise
201
+ irb(main):004:0> result.class
202
+ => Integer
203
+ ```
204
+
205
+ You can also set the stragegy globally using `ConcurrentRails::Testing.fake!` or `ConcurrentRails::Testing.immediate!`
206
+
207
+ ## Further reading
208
+
135
209
  For more information on how Futures work and how Rails handle multithread check these links:
136
210
 
137
211
  [Future documentation](https://github.com/ruby-concurrency/concurrent-ruby/blob/master/docs-source/future.md)
@@ -143,19 +217,19 @@ For more information on how Futures work and how Rails handle multithread check
143
217
  Add this line to your application's Gemfile:
144
218
 
145
219
  ```ruby
146
- gem 'concurrent_rails', '~> 0.1.8'
220
+ gem 'concurrent_rails', '~> 0.2.1'
147
221
  ```
148
222
 
149
223
  And then execute:
150
224
 
151
225
  ```bash
152
- $ bundle
226
+ bundle
153
227
  ```
154
228
 
155
229
  Or install it yourself as:
156
230
 
157
231
  ```bash
158
- $ gem install concurrent_rails
232
+ gem install concurrent_rails
159
233
  ```
160
234
 
161
235
  ## Contributing
@@ -0,0 +1,23 @@
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, &task)
18
+ @instance = rails_wrapped { delay_on(executor, *args, &task) }
19
+
20
+ self
21
+ end
22
+ end
23
+ end
@@ -2,11 +2,10 @@
2
2
 
3
3
  module ConcurrentRails
4
4
  class Future
5
- extend Forwardable
6
-
7
- def initialize(executor: :fast, &block)
5
+ def initialize(executor: :io, &block)
8
6
  @executor = executor
9
7
  @future = run_on_rails(block)
8
+ ActiveSupport::Deprecation.warn('ConcurrentRails::Future is deprecated. See README for details')
10
9
  end
11
10
 
12
11
  def execute
@@ -17,15 +16,13 @@ module ConcurrentRails
17
16
 
18
17
  %i[value value!].each do |method_name|
19
18
  define_method method_name do
20
- rails_wrapped do
21
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
22
- future.__send__(method_name)
23
- end
19
+ permit_concurrent_loads do
20
+ future.__send__(method_name)
24
21
  end
25
22
  end
26
23
  end
27
24
 
28
- def_delegators :@future, :state, :reason, :rejected?, :complete?, :add_observer
25
+ delegate :state, :reason, :rejected?, :complete?, :add_observer, to: :future
29
26
 
30
27
  private
31
28
 
@@ -41,6 +38,12 @@ module ConcurrentRails
41
38
  Rails.application.executor.wrap(&block)
42
39
  end
43
40
 
41
+ def permit_concurrent_loads(&block)
42
+ rails_wrapped do
43
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads(&block)
44
+ end
45
+ end
46
+
44
47
  attr_reader :executor, :future
45
48
  end
46
49
  end
@@ -0,0 +1,23 @@
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, &task)
18
+ @instance = rails_wrapped { future_on(executor, *args, &task) }
19
+
20
+ self
21
+ end
22
+ end
23
+ end
@@ -3,9 +3,7 @@
3
3
  module ConcurrentRails
4
4
  class Multi
5
5
  def self.enqueue(*actions, executor: :io)
6
- unless actions.all? { |action| action.is_a?(Proc) }
7
- raise ArgumentError, '#enqueue accepts `Proc`s only'
8
- end
6
+ raise ArgumentError, '#enqueue accepts `Proc`s only' unless actions.all?(Proc)
9
7
 
10
8
  new(actions, executor).enqueue
11
9
  end
@@ -1,61 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'concurrent_rails/future_adapter'
4
+ require 'concurrent_rails/delay_adapter'
5
+
3
6
  module ConcurrentRails
4
7
  class Promises
5
- extend Forwardable
6
8
  include Concurrent::Promises::FactoryMethods
7
-
8
- class << self
9
- def future(*args, &task)
10
- future_on(:fast, *args, &task)
11
- end
12
-
13
- def future_on(executor, *args, &task)
14
- new(executor).run_on_rails(*args, &task)
15
- end
16
- end
9
+ include ConcurrentRails::DelayAdapter
10
+ include ConcurrentRails::FutureAdapter
17
11
 
18
12
  def initialize(executor)
19
13
  @executor = executor
20
14
  end
21
15
 
22
- def run_on_rails(*args, &task)
23
- @future_instance = rails_wrapped { future_on(executor, *args, &task) }
24
-
25
- self
16
+ %i[value value!].each do |method_name|
17
+ define_method(method_name) do |timeout = nil, timeout_value = nil|
18
+ permit_concurrent_loads do
19
+ instance.public_send(method_name, timeout, timeout_value)
20
+ end
21
+ end
26
22
  end
27
23
 
28
24
  %i[then chain].each do |chainable|
29
25
  define_method(chainable) do |*args, &task|
30
26
  method = "#{chainable}_on"
31
- @future_instance = rails_wrapped do
32
- future_instance.__send__(method, executor, *args, &task)
27
+ @instance = rails_wrapped do
28
+ instance.public_send(method, executor, *args, &task)
33
29
  end
34
30
 
35
31
  self
36
32
  end
37
33
  end
38
34
 
39
- %i[value value!].each do |method_name|
40
- define_method(method_name) do |timeout = nil, timeout_value = nil|
35
+ def touch
36
+ @instance = rails_wrapped { instance.touch }
37
+
38
+ self
39
+ end
40
+
41
+ def wait(timeout = nil)
42
+ result = permit_concurrent_loads { instance.__send__(:wait_until_resolved, timeout) }
43
+
44
+ timeout ? result : self
45
+ end
46
+
47
+ %i[on_fulfillment on_rejection on_resolution].each do |method|
48
+ define_method(method) do |*args, &callback_task|
41
49
  rails_wrapped do
42
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
43
- future_instance.__send__(method_name, timeout, timeout_value)
44
- end
50
+ @instance = instance.__send__("#{method}_using", executor, *args, &callback_task)
45
51
  end
52
+
53
+ self
46
54
  end
47
- end
48
55
 
49
- %i[state reason rejected? resolved? fulfilled?].each do |delegatable|
50
- def_delegator :@future_instance, delegatable
56
+ define_method("#{method}!") do |*args, &callback_task|
57
+ rails_wrapped do
58
+ @instance = instance.__send__(:add_callback, "callback_#{method}", args, callback_task)
59
+ end
60
+
61
+ self
62
+ end
51
63
  end
52
64
 
65
+ delegate :state, :reason, :rejected?, :resolved?, :fulfilled?, to: :instance
66
+
67
+ attr_reader :executor
68
+
53
69
  private
54
70
 
55
71
  def rails_wrapped(&block)
56
72
  Rails.application.executor.wrap(&block)
57
73
  end
58
74
 
59
- attr_reader :future_instance, :executor
75
+ def permit_concurrent_loads(&block)
76
+ rails_wrapped do
77
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads(&block)
78
+ end
79
+ end
80
+
81
+ attr_reader :instance
60
82
  end
61
83
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConcurrentRails
4
+ class Testing
5
+ class << self
6
+ attr_reader :execution_mode
7
+
8
+ %w[immediate fake real].each do |test_mode|
9
+ define_method(test_mode) do |&task|
10
+ @execution_mode = test_mode
11
+ result = task.call
12
+ @execution_mode = :real
13
+
14
+ result
15
+ end
16
+
17
+ define_method("#{test_mode}!") do
18
+ @execution_mode = test_mode
19
+ end
20
+
21
+ define_method("#{test_mode}?") do
22
+ execution_mode == test_mode
23
+ end
24
+ end
25
+ end
26
+
27
+ module TestingFuture
28
+ def future(*args, &task)
29
+ if ConcurrentRails::Testing.immediate?
30
+ future_on(:immediate, *args, &task)
31
+ elsif ConcurrentRails::Testing.fake?
32
+ yield
33
+ else
34
+ super
35
+ end
36
+ end
37
+ end
38
+
39
+ ConcurrentRails::Promises.extend(TestingFuture)
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConcurrentRails
4
- VERSION = '0.1.8'
4
+ VERSION = '0.3.1'
5
5
  end
@@ -4,7 +4,5 @@ require 'concurrent_rails/future'
4
4
  require 'concurrent_rails/multi'
5
5
  require 'concurrent_rails/promises'
6
6
  require 'concurrent_rails/railtie'
7
+ require 'concurrent_rails/testing'
7
8
  require 'concurrent_rails/version'
8
-
9
- module ConcurrentRails
10
- 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.1.8
4
+ version: 0.3.1
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-05-05 00:00:00.000000000 Z
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
@@ -38,6 +52,20 @@ dependencies:
38
52
  - - ">="
39
53
  - !ruby/object:Gem::Version
40
54
  version: '1.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0.12'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0.12'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: rubocop-performance
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -63,10 +91,13 @@ files:
63
91
  - README.md
64
92
  - Rakefile
65
93
  - lib/concurrent_rails.rb
94
+ - lib/concurrent_rails/delay_adapter.rb
66
95
  - lib/concurrent_rails/future.rb
96
+ - lib/concurrent_rails/future_adapter.rb
67
97
  - lib/concurrent_rails/multi.rb
68
98
  - lib/concurrent_rails/promises.rb
69
99
  - lib/concurrent_rails/railtie.rb
100
+ - lib/concurrent_rails/testing.rb
70
101
  - lib/concurrent_rails/version.rb
71
102
  homepage: https://github.com/luizkowalski/concurrent_rails
72
103
  licenses:
@@ -90,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
121
  - !ruby/object:Gem::Version
91
122
  version: '0'
92
123
  requirements: []
93
- rubygems_version: 3.1.6
124
+ rubygems_version: 3.2.31
94
125
  signing_key:
95
126
  specification_version: 4
96
127
  summary: Multithread is hard