concurrently 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +4 -0
  4. data/.travis.yml +16 -0
  5. data/.yardopts +7 -0
  6. data/Gemfile +17 -0
  7. data/LICENSE +176 -0
  8. data/README.md +129 -0
  9. data/RELEASE_NOTES.md +49 -0
  10. data/Rakefile +28 -0
  11. data/concurrently.gemspec +33 -0
  12. data/ext/Ruby/thread.rb +28 -0
  13. data/ext/all/array.rb +24 -0
  14. data/ext/mruby/array.rb +19 -0
  15. data/ext/mruby/fiber.rb +5 -0
  16. data/ext/mruby/io.rb +54 -0
  17. data/guides/Installation.md +46 -0
  18. data/guides/Overview.md +335 -0
  19. data/guides/Performance.md +140 -0
  20. data/guides/Troubleshooting.md +262 -0
  21. data/lib/Ruby/concurrently.rb +12 -0
  22. data/lib/Ruby/concurrently/error.rb +4 -0
  23. data/lib/Ruby/concurrently/event_loop.rb +24 -0
  24. data/lib/Ruby/concurrently/event_loop/io_selector.rb +38 -0
  25. data/lib/all/concurrently/error.rb +10 -0
  26. data/lib/all/concurrently/evaluation.rb +109 -0
  27. data/lib/all/concurrently/evaluation/error.rb +18 -0
  28. data/lib/all/concurrently/event_loop.rb +101 -0
  29. data/lib/all/concurrently/event_loop/fiber.rb +37 -0
  30. data/lib/all/concurrently/event_loop/io_selector.rb +42 -0
  31. data/lib/all/concurrently/event_loop/proc_fiber_pool.rb +18 -0
  32. data/lib/all/concurrently/event_loop/run_queue.rb +111 -0
  33. data/lib/all/concurrently/proc.rb +233 -0
  34. data/lib/all/concurrently/proc/evaluation.rb +246 -0
  35. data/lib/all/concurrently/proc/fiber.rb +67 -0
  36. data/lib/all/concurrently/version.rb +8 -0
  37. data/lib/all/io.rb +248 -0
  38. data/lib/all/kernel.rb +201 -0
  39. data/lib/mruby/concurrently/proc.rb +21 -0
  40. data/lib/mruby/kernel.rb +15 -0
  41. data/mrbgem.rake +42 -0
  42. data/perf/_shared/stage.rb +33 -0
  43. data/perf/concurrent_proc_call.rb +13 -0
  44. data/perf/concurrent_proc_call_and_forget.rb +15 -0
  45. data/perf/concurrent_proc_call_detached.rb +15 -0
  46. data/perf/concurrent_proc_call_nonblock.rb +13 -0
  47. data/perf/concurrent_proc_calls.rb +49 -0
  48. data/perf/concurrent_proc_calls_awaiting.rb +48 -0
  49. metadata +144 -0
@@ -0,0 +1,233 @@
1
+ module Concurrently
2
+ if Object.const_defined? :MRUBY_VERSION
3
+ # @api mruby_patches
4
+ # @since 1.0.0
5
+ #
6
+ # mruby's Proc does not support instance variables. So, whe have to make
7
+ # it a normal class that does not inherit from Proc :(
8
+ class Proc; end
9
+ else
10
+ class Proc < ::Proc
11
+ # @private
12
+ # Calls the concurrent proc like a normal proc
13
+ alias_method :__proc_call__, :call
14
+ end
15
+ end
16
+
17
+ # @api public
18
+ # @since 1.0.0
19
+ #
20
+ # @note Concurrent procs are **thread safe**.
21
+ #
22
+ # A `Concurrently::Proc` is like a regular Proc except its block of code is
23
+ # evaluated concurrently. Its evaluation can wait for other stuff to happen
24
+ # (e.g. result of another concurrent proc or readiness of an IO) without
25
+ # blocking the execution of its thread.
26
+ #
27
+ # Errors raised inside concurrent procs are re-raised when getting their
28
+ # result with {Evaluation#await_result}. They can also be watched by
29
+ # registering callbacks for the `:error` event as shown in the example below.
30
+ # This is useful as a central hook to all errors inside concurrent procs for
31
+ # monitoring or logging purposes. Also, concurrent procs evaluated with
32
+ # {Kernel#concurrently} resp. {Proc#call_and_forget} are run in the
33
+ # background and will fail silently. The callbacks are the only way to be
34
+ # notified about errors inside them.
35
+ #
36
+ # The callbacks can be registered for all procs or only for one specific
37
+ # proc:
38
+ #
39
+ # @example Watching errors
40
+ # # Callbacks for all procs are registered for the `Concurrently::Proc` class:
41
+ # Concurrently::Proc.on(:error) do |error|
42
+ # puts "error in one of many procs: #{error}"
43
+ # end
44
+ #
45
+ # concurrently do
46
+ # raise "eternal darkness"
47
+ # end
48
+ #
49
+ # sunshine_proc = concurrent_proc do
50
+ # raise "eternal sunshine"
51
+ # end
52
+ #
53
+ # # Callbacks for a single proc are registered for the instance:
54
+ # sunshine_proc.on(:error) do |error|
55
+ # puts "error in the sunshine proc: #{error}"
56
+ # end
57
+ #
58
+ # # defer execution a little. This will make the concurrently block run in the
59
+ # # meantime.
60
+ # wait 0
61
+ # # the concurrently block will fail in the background and causes a printed
62
+ # # "error in one of many procs: eternal darkness"
63
+ #
64
+ # sunshine_proc.call
65
+ # # prints "error in one of many procs: eternal sunshine"
66
+ # # prints "error in the sunshine proc: eternal sunshine"
67
+ # # raises RuntimeError: eternal sunshine
68
+ class Proc
69
+ include CallbacksAttachable
70
+
71
+ # A new instance of {Proc}
72
+ #
73
+ # @param [Class] evaluation_class It can be given a custom class to create
74
+ # evaluation objects. This can be useful if all evaluations for this proc
75
+ # share some custom behavior and it makes sense to create a sub class of
76
+ # {Evaluation} for them.
77
+ def initialize(evaluation_class = Evaluation)
78
+ @evaluation_class = evaluation_class
79
+ end
80
+
81
+ # Evaluates the concurrent proc in a blocking manner.
82
+ #
83
+ # Evaluating the proc this way executes its block of code immediately
84
+ # and blocks the current thread of execution until the result is available.
85
+ #
86
+ # @return [Object] the result of the evaluation.
87
+ # @raise [Exception] if the evaluation raises an error.
88
+ #
89
+ # @example The proc can be evaluated without waiting
90
+ # add = concurrent_proc do |a, b|
91
+ # a + b
92
+ # end
93
+ # add.call 5, 8 # => 13
94
+ #
95
+ # @example The proc needs to wait to conclude evaluation
96
+ # time_in = concurrent_proc do |seconds|
97
+ # wait seconds
98
+ # Time.now
99
+ # end
100
+ #
101
+ # Time.now.strftime('%H:%M:%S.%L') # => "13:47:45.850"
102
+ # time_in.call(1.5).strftime('%H:%M:%S.%L') # => "13:47:47.351"
103
+ def call(*args)
104
+ case immediate_result = call_nonblock(*args)
105
+ when Evaluation
106
+ immediate_result.await_result
107
+ else
108
+ immediate_result
109
+ end
110
+ end
111
+
112
+ alias [] call
113
+
114
+ # Evaluates the concurrent proc in a non-blocking manner.
115
+ #
116
+ # Evaluating the proc this way executes its block of code immediately until
117
+ # the result is available or the evaluation needs to wait.
118
+ #
119
+ # Dealing with this method is similar to dealing with `IO#*_nonblock`.
120
+ #
121
+ # @return [Object] the result of the evaluation if it can be executed
122
+ # without waiting.
123
+ # @return [Evaluation] if the evaluation needs to wait.
124
+ # @raise [Exception] if the evaluation raises an error.
125
+ #
126
+ # @example The proc can be evaluated without waiting
127
+ # add = concurrent_proc do |a, b|
128
+ # a + b
129
+ # end
130
+ #
131
+ # case immediate_result = add.call_nonblock(5, 8)
132
+ # when Concurrently::Evaluation
133
+ # # won't happen here
134
+ # else
135
+ # immediate_result # => 13
136
+ # end
137
+ #
138
+ # @example The proc needs to wait to conclude evaluation
139
+ # time_in = concurrent_proc do |seconds|
140
+ # wait seconds
141
+ # Time.now
142
+ # end
143
+ #
144
+ # Time.now.strftime('%H:%M:%S.%L') # => "15:18:42.439"
145
+ #
146
+ # case immediate_result = time_in.call_nonblock(1.5)
147
+ # when Concurrently::Evaluation
148
+ # immediate_result.await_result.strftime('%H:%M:%S.%L') # => "15:18:44.577"
149
+ # else
150
+ # # won't happen here
151
+ # end
152
+ def call_nonblock(*args)
153
+ event_loop = EventLoop.current
154
+ run_queue = event_loop.run_queue
155
+ evaluation_bucket = []
156
+
157
+ result = begin
158
+ fiber = event_loop.proc_fiber_pool.take_fiber
159
+ # ProcFiberPool#take_fiber might have accessed the current evaluation
160
+ # if it needs to wait for the next iteration to get a fiber. Reset the
161
+ # current evaluation afterwards!
162
+ previous_evaluation = run_queue.current_evaluation
163
+ run_queue.current_evaluation = nil
164
+ run_queue.evaluation_class = @evaluation_class
165
+ fiber.resume [self, args, evaluation_bucket]
166
+ ensure
167
+ run_queue.current_evaluation = previous_evaluation
168
+ run_queue.evaluation_class = nil
169
+ end
170
+
171
+ case result
172
+ when Evaluation
173
+ # The proc fiber if the proc cannot be evaluated without waiting.
174
+ # Inject the evaluation into it so it can be concluded later.
175
+ evaluation_bucket << result
176
+ result
177
+ when Exception
178
+ raise result
179
+ else
180
+ result
181
+ end
182
+ end
183
+
184
+ # Evaluates the concurrent proc detached from the current execution thread.
185
+ #
186
+ # Evaluating the proc this way detaches the evaluation from the current
187
+ # thread of execution and schedules its start during the next iteration of
188
+ # the event loop.
189
+ #
190
+ # @return [Evaluation]
191
+ #
192
+ # @example
193
+ # add = concurrent_proc do |a, b|
194
+ # a + b
195
+ # end
196
+ # evaluation = add.call_detached 5, 8
197
+ # evaluation.await_result # => 13
198
+ def call_detached(*args)
199
+ event_loop = EventLoop.current
200
+ evaluation = @evaluation_class.new(event_loop.proc_fiber_pool.take_fiber)
201
+ event_loop.run_queue.schedule_immediately evaluation, [self, args, [evaluation]]
202
+ evaluation
203
+ end
204
+
205
+ # Fire and forget variation of {#call_detached}.
206
+ #
207
+ # Once called, there is no way to control the evaluation anymore. But,
208
+ # because we save creating an {Evaluation} instance this is slightly faster
209
+ # than {#call_detached}.
210
+ #
211
+ # To execute code this way you can also use the shortcut
212
+ # {Kernel#concurrently}.
213
+ #
214
+ # @return [nil]
215
+ #
216
+ # @example
217
+ # add = concurrent_proc do |a, b|
218
+ # puts "detached!"
219
+ # end
220
+ # add.call_and_forget 5, 8
221
+ #
222
+ # # we need to enter the event loop to see an effect
223
+ # wait 0 # prints "detached!"
224
+ def call_and_forget(*args)
225
+ event_loop = EventLoop.current
226
+ # run without creating an Evaluation object at first. It will be created
227
+ # if the proc needs to wait for something.
228
+ event_loop.run_queue.schedule_immediately event_loop.proc_fiber_pool.take_fiber, [self, args]
229
+
230
+ nil
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,246 @@
1
+ module Concurrently
2
+ # @api public
3
+ # @since 1.0.0
4
+ #
5
+ # `Concurrently::Proc::Evaluation` represents the evaluation of a concurrent
6
+ # proc.
7
+ #
8
+ # @note Evaluations are **not thread safe**. They are operating on a fiber.
9
+ # Fibers cannot be resumed inside a thread they were not created in.
10
+ #
11
+ # An instance will be returned by {Evaluation.current} if called from inside
12
+ # a concurrent proc. It will also be returned by every call of
13
+ # {Concurrently::Proc#call_detached} and also by
14
+ # {Concurrently::Proc#call_nonblock} if the evaluation cannot be concluded in
15
+ # one go and needs to wait.
16
+ class Proc::Evaluation < Evaluation
17
+ # @private
18
+ def initialize(fiber)
19
+ super
20
+ @concluded = false
21
+ @awaiting_result = {}
22
+ @data = {}
23
+ end
24
+
25
+ # Attaches a value to the evaluation under the given key
26
+ #
27
+ # @param [Object] key The key to store the value under
28
+ # @param [Object] value The value to store
29
+ # @return [value]
30
+ #
31
+ # @example
32
+ # evaluation = concurrent_proc{ :result }.call_detached
33
+ # evaluation[:key] = :value
34
+ # evaluation[:key] # => :value
35
+ def []=(key, value)
36
+ @data[key] = value
37
+ end
38
+
39
+ # Retrieves the attached value under the given key
40
+ #
41
+ # @param [Object] key The key to look up
42
+ # @return [Object] the stored value
43
+ #
44
+ # @example
45
+ # evaluation = concurrent_proc{ :result }.call_detached
46
+ # evaluation[:key] = :value
47
+ # evaluation[:key] # => :value
48
+ def [](key)
49
+ @data[key]
50
+ end
51
+
52
+ # Checks if there is an attached value for the given key
53
+ #
54
+ # @param [Object] key The key to look up
55
+ # @return [Boolean]
56
+ #
57
+ # @example
58
+ # evaluation = concurrent_proc{ :result }.call_detached
59
+ # evaluation[:key] = :value
60
+ # evaluation.key? :key # => true
61
+ # evaluation.key? :another_key # => false
62
+ def key?(key)
63
+ @data.key? key
64
+ end
65
+
66
+ # Returns all keys with values
67
+ #
68
+ # @return [Array]
69
+ #
70
+ # @example
71
+ # evaluation = concurrent_proc{ :result }.call_detached
72
+ # evaluation[:key1] = :value1
73
+ # evaluation[:key2] = :value2
74
+ # evaluation.keys # => [:key1, :key2]
75
+ def keys
76
+ @data.keys
77
+ end
78
+
79
+ # Waits for the evaluation to be concluded with a result.
80
+ #
81
+ # The result can be awaited from multiple places at once. All of them are
82
+ # resumed once the result is available.
83
+ #
84
+ # @param [Hash] opts
85
+ # @option opts [Numeric] :within maximum time to wait *(defaults to: Float::INFINITY)*
86
+ # @option opts [Object] :timeout_result result to return in case of an exceeded
87
+ # waiting time *(defaults to raising {Concurrently::Evaluation::TimeoutError})*
88
+ #
89
+ # @return [Object] the result the evaluation is concluded with
90
+ # @raise [Exception] if the result is an exception.
91
+ # @raise [Concurrently::Evaluation::TimeoutError] if a given maximum waiting time
92
+ # is exceeded and no custom timeout result is given.
93
+ #
94
+ # @example Waiting inside another concurrent proc
95
+ # # Control flow is indicated by (N)
96
+ #
97
+ # # (1)
98
+ # evaluation = concurrent_proc do
99
+ # # (4)
100
+ # :result
101
+ # end.call_detached
102
+ #
103
+ # # (2)
104
+ # concurrent_proc do
105
+ # # (3)
106
+ # evaluation.await_result
107
+ # # (5)
108
+ # end.call # => :result
109
+ # # (6)
110
+ #
111
+ # @example Waiting outside a concurrent proc
112
+ # # Control flow is indicated by (N)
113
+ #
114
+ # # (1)
115
+ # evaluation = concurrent_proc do
116
+ # # (3)
117
+ # :result
118
+ # end.call_detached
119
+ #
120
+ # # (2)
121
+ # evaluation.await_result # => :result
122
+ # # (4)
123
+ #
124
+ # @example Waiting with a timeout
125
+ # evaluation = concurrent_proc do
126
+ # wait 1
127
+ # :result
128
+ # end.call_detached
129
+ #
130
+ # evaluation.await_result within: 0.1
131
+ # # => raises a TimeoutError after 0.1 second
132
+ #
133
+ # @example Waiting with a timeout and a timeout result
134
+ # evaluation = concurrent_proc do
135
+ # wait 1
136
+ # :result
137
+ # end.call_detached
138
+ #
139
+ # evaluation.await_result within: 0.1, timeout_result: false
140
+ # # => returns false after 0.1 second
141
+ #
142
+ # @example When the evaluation raises or returns an error
143
+ # evaluation = concurrent_proc do
144
+ # RuntimeError.new("self destruct!") # equivalent: raise "self destruct!"
145
+ # end.call_detached
146
+ #
147
+ # evaluation.await_result # => raises "self destruct!"
148
+ #
149
+ # @overload await_result(opts = {})
150
+ #
151
+ # @overload await_result(opts = {})
152
+ # Use the block to do something with the result before returning it. This
153
+ # can be used to validate or transform the result.
154
+ #
155
+ # @yieldparam result [Object] its result
156
+ # @yieldreturn [Object] a (potentially) transformed result
157
+ #
158
+ # @example Transforming a result
159
+ # evaluation = concurrent_proc do
160
+ # :result
161
+ # end.call_detached
162
+ #
163
+ # evaluation.await_result{ |result| "transformed_#{result}" }
164
+ # # => "transformed_result"
165
+ #
166
+ # @example Validating a result
167
+ # evaluation = concurrent_proc do
168
+ # :invalid_result
169
+ # end.call_detached
170
+ #
171
+ # evaluation.await_result{ |result| raise "invalid result" if result != :result }
172
+ # # => raises "invalid result"
173
+ def await_result(opts = {}) # &with_result
174
+ if @concluded
175
+ result = @result
176
+ else
177
+ result = begin
178
+ evaluation = Concurrently::Evaluation.current
179
+ @awaiting_result.store evaluation, true
180
+ await_resume! opts
181
+ rescue Exception => error
182
+ error
183
+ ensure
184
+ @awaiting_result.delete evaluation
185
+ end
186
+ end
187
+
188
+ result = yield result if block_given?
189
+
190
+ (Exception === result) ? (raise result) : result
191
+ end
192
+
193
+ # @!attribute [r] concluded?
194
+ #
195
+ # Checks if the evaluation is concluded
196
+ #
197
+ # @return [Boolean]
198
+ def concluded?
199
+ @concluded
200
+ end
201
+
202
+ # Cancels the concurrent evaluation prematurely by injecting a result.
203
+ #
204
+ # @param [Object] result
205
+ #
206
+ # @return [:concluded]
207
+ # @raise [Error] if it is already concluded
208
+ #
209
+ # @example
210
+ # # Control flow is indicated by (N)
211
+ #
212
+ # # (1)
213
+ # evaluation = concurrent_proc do
214
+ # # (4)
215
+ # wait 1
216
+ # # never reached
217
+ # :result
218
+ # end.call_nonblock
219
+ #
220
+ # # (2)
221
+ # concurrently do
222
+ # # (5)
223
+ # evaluation.conclude_to :premature_result
224
+ # end
225
+ #
226
+ # # (3)
227
+ # evaluation.await_result # => :premature_result
228
+ # # (6)
229
+ def conclude_to(result)
230
+ if @concluded
231
+ raise self.class::Error, "already concluded"
232
+ end
233
+
234
+ @result = result
235
+ @concluded = true
236
+
237
+ if Fiber.current != @fiber
238
+ # Cancel fiber by resuming it with itself as argument
239
+ @fiber.resume @fiber
240
+ end
241
+
242
+ @awaiting_result.each_key{ |evaluation| evaluation.resume! result }
243
+ :concluded
244
+ end
245
+ end
246
+ end