concurrently 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +4 -0
- data/.travis.yml +16 -0
- data/.yardopts +7 -0
- data/Gemfile +17 -0
- data/LICENSE +176 -0
- data/README.md +129 -0
- data/RELEASE_NOTES.md +49 -0
- data/Rakefile +28 -0
- data/concurrently.gemspec +33 -0
- data/ext/Ruby/thread.rb +28 -0
- data/ext/all/array.rb +24 -0
- data/ext/mruby/array.rb +19 -0
- data/ext/mruby/fiber.rb +5 -0
- data/ext/mruby/io.rb +54 -0
- data/guides/Installation.md +46 -0
- data/guides/Overview.md +335 -0
- data/guides/Performance.md +140 -0
- data/guides/Troubleshooting.md +262 -0
- data/lib/Ruby/concurrently.rb +12 -0
- data/lib/Ruby/concurrently/error.rb +4 -0
- data/lib/Ruby/concurrently/event_loop.rb +24 -0
- data/lib/Ruby/concurrently/event_loop/io_selector.rb +38 -0
- data/lib/all/concurrently/error.rb +10 -0
- data/lib/all/concurrently/evaluation.rb +109 -0
- data/lib/all/concurrently/evaluation/error.rb +18 -0
- data/lib/all/concurrently/event_loop.rb +101 -0
- data/lib/all/concurrently/event_loop/fiber.rb +37 -0
- data/lib/all/concurrently/event_loop/io_selector.rb +42 -0
- data/lib/all/concurrently/event_loop/proc_fiber_pool.rb +18 -0
- data/lib/all/concurrently/event_loop/run_queue.rb +111 -0
- data/lib/all/concurrently/proc.rb +233 -0
- data/lib/all/concurrently/proc/evaluation.rb +246 -0
- data/lib/all/concurrently/proc/fiber.rb +67 -0
- data/lib/all/concurrently/version.rb +8 -0
- data/lib/all/io.rb +248 -0
- data/lib/all/kernel.rb +201 -0
- data/lib/mruby/concurrently/proc.rb +21 -0
- data/lib/mruby/kernel.rb +15 -0
- data/mrbgem.rake +42 -0
- data/perf/_shared/stage.rb +33 -0
- data/perf/concurrent_proc_call.rb +13 -0
- data/perf/concurrent_proc_call_and_forget.rb +15 -0
- data/perf/concurrent_proc_call_detached.rb +15 -0
- data/perf/concurrent_proc_call_nonblock.rb +13 -0
- data/perf/concurrent_proc_calls.rb +49 -0
- data/perf/concurrent_proc_calls_awaiting.rb +48 -0
- 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
|