concurrent_rails 0.7.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc63638fb76dc139c10b9ed53d61e020f7288bbd13f3bed9c4e5129b93c016c9
4
- data.tar.gz: e61aae19d4fd6f1023be007ed77a2edf333a922fe0ab522ea533175e975ce941
3
+ metadata.gz: 18c6a530ca05cc94c234d366dc6b93cec5c646e3b7cb0413858ea066ced8c5a8
4
+ data.tar.gz: 3ef23ca30903e60b4418b5117a73c05d0effc00979b765532e5565239468fa31
5
5
  SHA512:
6
- metadata.gz: f236962918393d4f1a8c81edec667564fc862825940a0ef30e33053d1d1061017172b14cb407eb87177ea1db41471de9c7abafb0648ebd63183b107df1ad3604
7
- data.tar.gz: e453ea4cdcd0161d0159dfeaec1931e43dbd992120ecb57953935517d28072a79338a731feb1be660108dd24f3bb376f66f013de3c1653d08a5e4f756ecc0b7e
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 three classes that will help you run tasks in parallel: `ConcurrentRails::Promises`, `ConcurrentRails::Future` ([in the process of being deprecated by concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby#deprecated)) and `ConcurrentRails::Multi`
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
- `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:
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 @future_instance=#...
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
- => #<Concurrent::Promises::Future:0x00007f8b553325b0 pending>
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 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.
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
- You can also set the strategy globally using `ConcurrentRails::Testing.fake!` or `ConcurrentRails::Testing.immediate!`
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.5.1'
166
+ gem 'concurrent_rails', '~> 0.9'
131
167
  ```
132
168
 
133
169
  And then execute:
@@ -2,58 +2,82 @@
2
2
 
3
3
  module ConcurrentRails
4
4
  class Promises
5
- include Concurrent::Promises::FactoryMethods
6
- include ConcurrentRails::DelayAdapter
7
- include ConcurrentRails::FutureAdapter
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
31
+
32
+ def wrap_task(task)
33
+ proc { |*args| Rails.application.executor.wrap { task.call(*args) } }
34
+ end
8
35
 
9
- def initialize(executor)
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)
10
44
  @executor = executor
45
+ @instance = instance
11
46
  end
12
47
 
13
48
  %i[value value!].each do |method_name|
14
49
  define_method(method_name) do |timeout = nil, timeout_value = nil|
15
- permit_concurrent_loads do
16
- instance.public_send(method_name, timeout, timeout_value)
17
- end
50
+ rails_wrapped { instance.public_send(method_name, timeout, timeout_value) }
18
51
  end
19
52
  end
20
53
 
21
54
  %i[then chain].each do |chainable|
22
55
  define_method(chainable) do |*args, &task|
23
- method = "#{chainable}_on"
24
- @instance = rails_wrapped do
25
- instance.public_send(method, executor, *args, &task)
26
- end
27
-
28
- self
56
+ self.class.new(executor, instance.public_send(:"#{chainable}_on", executor, *args, &self.class.wrap_task(task)))
29
57
  end
30
58
  end
31
59
 
32
60
  def touch
33
- @instance = rails_wrapped { instance.touch }
61
+ instance.touch
34
62
 
35
63
  self
36
64
  end
37
65
 
38
66
  def wait(timeout = nil)
39
- result = permit_concurrent_loads { instance.__send__(:wait_until_resolved, timeout) }
67
+ result = rails_wrapped { instance.wait(timeout) }
40
68
 
41
69
  timeout ? result : self
42
70
  end
43
71
 
44
72
  %i[on_fulfillment on_rejection on_resolution].each do |method|
45
73
  define_method(method) do |*args, &callback_task|
46
- rails_wrapped do
47
- @instance = instance.__send__(:"#{method}_using", executor, *args, &callback_task)
48
- end
74
+ instance.public_send(:"#{method}_using", executor, *args, &self.class.wrap_task(callback_task))
49
75
 
50
76
  self
51
77
  end
52
78
 
53
79
  define_method(:"#{method}!") do |*args, &callback_task|
54
- rails_wrapped do
55
- @instance = instance.__send__(:add_callback, "callback_#{method}", args, callback_task)
56
- end
80
+ instance.public_send(:"#{method}!", *args, &self.class.wrap_task(callback_task))
57
81
 
58
82
  self
59
83
  end
@@ -69,12 +93,6 @@ module ConcurrentRails
69
93
  Rails.application.executor.wrap(&)
70
94
  end
71
95
 
72
- def permit_concurrent_loads(&block)
73
- rails_wrapped do
74
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads(&block)
75
- end
76
- end
77
-
78
96
  attr_reader :instance
79
97
  end
80
98
  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
- attr_reader :execution_mode
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
- @execution_mode = test_mode
11
- result = task.call
12
- @execution_mode = :real
14
+ previous = ActiveSupport::IsolatedExecutionState[ISOLATION_KEY]
15
+ ActiveSupport::IsolatedExecutionState[ISOLATION_KEY] = test_mode
13
16
 
14
- result
17
+ task.call
18
+ ensure
19
+ ActiveSupport::IsolatedExecutionState[ISOLATION_KEY] = previous
15
20
  end
16
21
 
17
22
  define_method(:"#{test_mode}!") do
18
- @execution_mode = test_mode
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
- def future(*args, &)
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
- future_on(:immediate, *args, &)
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.extend(TestingFuture)
56
+ ConcurrentRails::Promises.singleton_class.prepend(TestingFuture)
40
57
  end
41
58
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConcurrentRails
4
- VERSION = "0.7.1"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -5,5 +5,4 @@ require "zeitwerk"
5
5
  loader = Zeitwerk::Loader.for_gem
6
6
  loader.setup
7
7
 
8
- module ConcurrentRails
9
- end
8
+ module ConcurrentRails; 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.7.1
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,10 +62,7 @@ files:
48
62
  - README.md
49
63
  - Rakefile
50
64
  - lib/concurrent_rails.rb
51
- - lib/concurrent_rails/delay_adapter.rb
52
- - lib/concurrent_rails/future_adapter.rb
53
65
  - lib/concurrent_rails/promises.rb
54
- - lib/concurrent_rails/railtie.rb
55
66
  - lib/concurrent_rails/testing.rb
56
67
  - lib/concurrent_rails/version.rb
57
68
  homepage: https://github.com/luizkowalski/concurrent_rails
@@ -69,14 +80,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
69
80
  requirements:
70
81
  - - ">="
71
82
  - !ruby/object:Gem::Version
72
- version: '3.2'
83
+ version: '3.3'
73
84
  required_rubygems_version: !ruby/object:Gem::Requirement
74
85
  requirements:
75
86
  - - ">="
76
87
  - !ruby/object:Gem::Version
77
88
  version: '0'
78
89
  requirements: []
79
- rubygems_version: 4.0.2
90
+ rubygems_version: 4.0.16
80
91
  specification_version: 4
81
92
  summary: Multithread is hard
82
93
  test_files: []
@@ -1,23 +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, &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
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
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ConcurrentRails
4
- class Railtie < ::Rails::Railtie
5
- end
6
- end