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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb04be405f37c5acf9a5fa5ed21ba327bb92168cc6f830dc52314356ccbfd4dd
4
- data.tar.gz: cabf17aba3c7c80939b95d5af80e48cb6fcc8c4260f2278386e3952b2128a59f
3
+ metadata.gz: 18c6a530ca05cc94c234d366dc6b93cec5c646e3b7cb0413858ea066ced8c5a8
4
+ data.tar.gz: 3ef23ca30903e60b4418b5117a73c05d0effc00979b765532e5565239468fa31
5
5
  SHA512:
6
- metadata.gz: 971b987e34b8eb5142d05f774368cec64e363c4c7ecd23555dcd594b8631d4237c85ab969a5392c184ed7a727c78c13586876e1ce6b629ae86bf865ac1ccec1d
7
- data.tar.gz: 575157aae6f247f68672327689b7396804d63e1b78162f68a73be51ac56a818039bb40c8af765c5481dbf2b557fb4cee884d93aaf5949ea744ba9599aeb99479
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.7'
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
- include Concurrent::Promises::FactoryMethods
6
- include ConcurrentRails::CombinatorAdapter
7
- include ConcurrentRails::DelayAdapter
8
- include ConcurrentRails::FutureAdapter
9
- include ConcurrentRails::ScheduleAdapter
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
- def initialize(executor)
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
- method = "#{chainable}_on"
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
- @instance = rails_wrapped { instance.touch }
61
+ instance.touch
33
62
 
34
63
  self
35
64
  end
36
65
 
37
66
  def wait(timeout = nil)
38
- result = rails_wrapped { instance.__send__(:wait_until_resolved, timeout) }
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
- wrapped_callback = proc { |*a| rails_wrapped { callback_task.call(*a) } }
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
- wrapped_callback = proc { |*a| rails_wrapped { callback_task.call(*a) } }
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
- 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.8.0"
4
+ VERSION = "0.9.0"
5
5
  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.8.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.2'
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.8
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,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ConcurrentRails
4
- class Railtie < ::Rails::Railtie
5
- initializer "concurrent_rails.executor_hooks" do |app|
6
- # TODO: Add something here at some point
7
- end
8
- end
9
- 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