concurrent_rails 0.1.5 → 0.2.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: 87ad625b78cf38b613761735ca3ca8ad7dbf12f16f807290082bf1a7081ad383
4
- data.tar.gz: 723e320ec707747381e1d8b7f6b81f801c02643d2d18f4e714330041eeff49a5
3
+ metadata.gz: a0352c1516c930c65708383fa734cd7444b02b0a213c0ef1f794e5e31f8e1fdf
4
+ data.tar.gz: 33ec089c95b33ff31a4652a073feac356e8b23a8d7e30bd214ea0a0dbc4aedcb
5
5
  SHA512:
6
- metadata.gz: 0fa50d7cfad90990b29f75bdcf3f6e15dc8252774259b4d65af62f37cd36a4ed46b0ff5b091a39a8f0e0f54625e90d5444489dd6d4ea13b44d9c474d783c56d2
7
- data.tar.gz: 3a75c1ccd1f4821827359c672c991b0ef071a2dc41a274447c1117c51bc52bc9cc41fec35cde38ef6d79b31d4047b419240ae99775c415bff66240433b05f340
6
+ metadata.gz: a486a3ab777574962127795db050fd2615b69594d0bc88adb5114368c7d90492f2b51b237817f97118a31969396d53171bf66371240577051977a052d935148a
7
+ data.tar.gz: db095fe9e33e527a3ade62e8a40aec1866718bfce6b55ef519775cd1761d78b1eb1868d8c8b0ab799dcd05f78980ba16e31f5a7912b87ab3e2dd49d917dab004
data/README.md CHANGED
@@ -2,19 +2,17 @@
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
 
10
9
  ## Usage
11
10
 
12
- This library provides three classes that will help you run tasks in parallel: `ConcurrentRails::Promises`, `ConcurrentRails::Future` and `ConcurrentRails::Multi`
11
+ This library provides three classes that will help you run tasks in parallel: `ConcurrentRails::Promises`, `ConcurrentRails::Future` ([in process of being deprecated by concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby#deprecated)) and `ConcurrentRails::Multi`
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,37 @@ 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 you 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
+ ## Further reading
206
+
135
207
  For more information on how Futures work and how Rails handle multithread check these links:
136
208
 
137
209
  [Future documentation](https://github.com/ruby-concurrency/concurrent-ruby/blob/master/docs-source/future.md)
@@ -143,19 +215,19 @@ For more information on how Futures work and how Rails handle multithread check
143
215
  Add this line to your application's Gemfile:
144
216
 
145
217
  ```ruby
146
- gem 'concurrent_rails', '~> 0.1.5'
218
+ gem 'concurrent_rails', '~> 0.2.1'
147
219
  ```
148
220
 
149
221
  And then execute:
150
222
 
151
223
  ```bash
152
- $ bundle
224
+ bundle
153
225
  ```
154
226
 
155
227
  Or install it yourself as:
156
228
 
157
229
  ```bash
158
- $ gem install concurrent_rails
230
+ gem install concurrent_rails
159
231
  ```
160
232
 
161
233
  ## Contributing
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent_rails/version'
4
- require 'concurrent_rails/railtie'
5
3
  require 'concurrent_rails/future'
6
4
  require 'concurrent_rails/multi'
7
5
  require 'concurrent_rails/promises'
8
-
9
- module ConcurrentRails
10
- end
6
+ require 'concurrent_rails/railtie'
7
+ require 'concurrent_rails/testing'
8
+ require 'concurrent_rails/version'
@@ -0,0 +1,23 @@
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
@@ -0,0 +1,23 @@
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
@@ -2,11 +2,10 @@
2
2
 
3
3
  module ConcurrentRails
4
4
  class Future
5
- extend Forwardable
6
-
7
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,30 +16,34 @@ module ConcurrentRails
17
16
 
18
17
  %i[value value!].each do |method_name|
19
18
  define_method method_name do
20
- Rails.application.executor.wrap do
21
- result = nil
22
-
23
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
24
- result = future.__send__(method_name)
25
- end
26
-
27
- result
19
+ permit_concurrent_loads do
20
+ future.__send__(method_name)
28
21
  end
29
22
  end
30
23
  end
31
24
 
32
- def_delegators :@future, :state, :reason, :rejected?, :complete?, :add_observer
25
+ delegate :state, :reason, :rejected?, :complete?, :add_observer, to: :future
33
26
 
34
27
  private
35
28
 
36
29
  def run_on_rails(block)
37
- @future = Rails.application.executor.wrap do
30
+ @future = rails_wrapped do
38
31
  Concurrent::Future.new(executor: executor) do
39
- Rails.application.executor.wrap(&block)
32
+ rails_wrapped(&block)
40
33
  end
41
34
  end
42
35
  end
43
36
 
37
+ def rails_wrapped(&block)
38
+ Rails.application.executor.wrap(&block)
39
+ end
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
@@ -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
@@ -30,6 +28,10 @@ module ConcurrentRails
30
28
  futures.map(&:value)
31
29
  end
32
30
 
31
+ def compute!
32
+ futures.map(&:value!)
33
+ end
34
+
33
35
  def complete?
34
36
  futures.all?(&:complete?)
35
37
  end
@@ -1,48 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'concurrent_rails/adapters/future'
4
+ require 'concurrent_rails/adapters/delay'
5
+
3
6
  module ConcurrentRails
4
7
  class Promises
5
8
  include Concurrent::Promises::FactoryMethods
6
- extend Forwardable
9
+ include ConcurrentRails::Adapters::Delay
10
+ include ConcurrentRails::Adapters::Future
11
+
12
+ def initialize(executor)
13
+ @executor = executor
14
+ end
7
15
 
8
- def self.future(*args, &task)
9
- new.run_on_rails(*args, &task)
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.__send__(method_name, timeout, timeout_value)
20
+ end
21
+ end
10
22
  end
11
23
 
12
- def then(*args, &task)
13
- @future_instance = Rails.application.executor.wrap do
14
- future_instance.then(*args, &task)
24
+ %i[then chain].each do |chainable|
25
+ define_method(chainable) do |*args, &task|
26
+ method = "#{chainable}_on"
27
+ @instance = rails_wrapped do
28
+ instance.__send__(method, executor, *args, &task)
29
+ end
30
+
31
+ self
15
32
  end
33
+ end
34
+
35
+ def touch
36
+ @instance = rails_wrapped { instance.touch }
16
37
 
17
38
  self
18
39
  end
19
40
 
20
- %i[value value!].each do |method_name|
21
- define_method method_name do
22
- Rails.application.executor.wrap do
23
- result = nil
41
+ def wait(timeout = nil)
42
+ result = permit_concurrent_loads { instance.__send__(:wait_until_resolved, timeout) }
24
43
 
25
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
26
- result = future_instance.__send__(method_name)
27
- end
44
+ timeout ? result : self
45
+ end
28
46
 
29
- result
47
+ %i[on_fulfillment on_rejection on_resolution].each do |method|
48
+ define_method(method) do |*args, &callback_task|
49
+ rails_wrapped do
50
+ @instance = instance.__send__("#{method}_using", executor, *args, &callback_task)
30
51
  end
31
- end
32
- end
33
52
 
34
- def run_on_rails(*args, &task)
35
- @future_instance = Rails.application.executor.wrap do
36
- future_on(default_executor, *args, &task)
53
+ self
37
54
  end
38
55
 
39
- self
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
40
63
  end
41
64
 
42
- def_delegators :@future_instance, :state, :reason, :rejected?, :complete?
65
+ delegate :state, :reason, :rejected?, :resolved?, :fulfilled?, to: :instance
66
+
67
+ attr_reader :executor
43
68
 
44
69
  private
45
70
 
46
- attr_reader :future_instance
71
+ def rails_wrapped(&block)
72
+ Rails.application.executor.wrap(&block)
73
+ end
74
+
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
47
82
  end
48
83
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConcurrentRails
4
+ class Testing
5
+ class << self
6
+ attr_reader :execution_mode
7
+
8
+ %i[immediate fake].each do |exec_method|
9
+ define_method("#{exec_method}!") do |&task|
10
+ @execution_mode = exec_method
11
+ result = task.call
12
+ @execution_mode = :real
13
+
14
+ result
15
+ end
16
+
17
+ define_method("#{exec_method}?") do
18
+ execution_mode == exec_method
19
+ end
20
+ end
21
+
22
+ module TestingFuture
23
+ def future(*args, &task)
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
32
+ end
33
+
34
+ ConcurrentRails::Promises.extend(TestingFuture)
35
+ end
36
+ end
37
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConcurrentRails
4
- VERSION = '0.1.5'
4
+ VERSION = '0.2.1'
5
5
  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.5
4
+ version: 0.2.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-04-24 00:00:00.000000000 Z
11
+ date: 2021-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop-minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0.12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0.12'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rubocop-performance
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +66,20 @@ dependencies:
52
66
  - - ">="
53
67
  - !ruby/object:Gem::Version
54
68
  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'
55
83
  description: Small library to make concurrent-ruby and Rails play nice together
56
84
  email:
57
85
  - luizeduardokowalski@gmail.com
@@ -63,12 +91,14 @@ files:
63
91
  - README.md
64
92
  - Rakefile
65
93
  - lib/concurrent_rails.rb
94
+ - lib/concurrent_rails/adapters/delay.rb
95
+ - lib/concurrent_rails/adapters/future.rb
66
96
  - lib/concurrent_rails/future.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
- - lib/tasks/concurrent_rails_tasks.rake
72
102
  homepage: https://github.com/luizkowalski/concurrent_rails
73
103
  licenses:
74
104
  - MIT
@@ -91,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
121
  - !ruby/object:Gem::Version
92
122
  version: '0'
93
123
  requirements: []
94
- rubygems_version: 3.1.6
124
+ rubygems_version: 3.2.20
95
125
  signing_key:
96
126
  specification_version: 4
97
127
  summary: Multithread is hard
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
- # desc "Explaining what the task does"
3
- # task :concurrent_rails do
4
- # # Task goes here
5
- # end