concurrently 1.0.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.
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