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,33 @@
1
+ require_relative 'lib/all/concurrently/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "concurrently"
5
+ spec.version = Concurrently::VERSION
6
+ spec.summary = %q{A concurrency framework based on fibers}
7
+ spec.description = <<-DESC
8
+ Concurrently is a concurrency framework for Ruby and mruby. With it, concurrent
9
+ code can be written sequentially similar to async/await.
10
+
11
+ The concurrency primitive of Concurrently is the concurrent proc. It is very
12
+ similar to a regular proc. Calling a concurrent proc creates a concurrent
13
+ evaluation which is kind of a lightweight thread: It can wait for stuff without
14
+ blocking other concurrent evaluations.
15
+
16
+ Under the hood, concurrent procs are evaluated inside fibers. They can wait for
17
+ readiness of I/O or a period of time (or the result of other concurrent
18
+ evaluations).
19
+ DESC
20
+
21
+ spec.homepage = "https://github.com/christopheraue/m-ruby-concurrently"
22
+ spec.license = "Apache-2.0"
23
+ spec.authors = ["Christopher Aue"]
24
+ spec.email = ["rubygems@christopheraue.net"]
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.require_paths = ["lib/Ruby"]
27
+
28
+ spec.required_ruby_version = ">= 2.2.7"
29
+
30
+ spec.add_dependency "nio4r", "~> 2.1"
31
+ spec.add_dependency "hitimes", "~> 1.2"
32
+ spec.add_dependency "callbacks_attachable", "~> 2.2"
33
+ end
@@ -0,0 +1,28 @@
1
+ # @api ruby_patches
2
+ # @since 1.0.0
3
+ class Thread
4
+ # Attach an event loop to every thread in Ruby.
5
+ def __concurrently_event_loop__
6
+ @__concurrently_event_loop__ ||= Concurrently::EventLoop.new
7
+ end
8
+
9
+ # Disable fiber-local variables and treat variables using the fiber-local
10
+ # interface as thread-local. Most of the code out there is not using
11
+ # fibers explicitly and really intends to attach values to the current
12
+ # thread instead to the current fiber.
13
+ #
14
+ # This also makes sure we can safely reuse fibers without worrying about
15
+ # lost or leaked fiber-local variables.
16
+
17
+ # Redirect getting fiber locals to getting thread locals
18
+ alias_method :[], :thread_variable_get
19
+
20
+ # Redirect setting fiber locals to setting thread locals
21
+ alias_method :[]=, :thread_variable_set
22
+
23
+ # Redirect checking fiber local to checking thread local
24
+ alias_method :key?, :thread_variable?
25
+
26
+ # Redirect getting names for fiber locals to getting names of thread locals
27
+ alias_method :keys, :thread_variables
28
+ end
data/ext/all/array.rb ADDED
@@ -0,0 +1,24 @@
1
+ # @private
2
+ class Array
3
+ unless method_defined? :bsearch_index
4
+ # Implements Array#bsearch_index for mruby and Ruby < 2.3.
5
+ def bsearch_index
6
+ # adapted from https://github.com/python-git/python/blob/7e145963cd67c357fcc2e0c6aca19bc6ec9e64bb/Lib/bisect.py#L67
7
+ len = length
8
+ lo = 0
9
+ hi = len
10
+
11
+ while lo < hi
12
+ mid = (lo + hi).div(2)
13
+
14
+ if yield self[mid]
15
+ hi = mid
16
+ else
17
+ lo = mid + 1
18
+ end
19
+ end
20
+
21
+ lo == len ? nil : lo
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ # @api mruby_patches
2
+ # @since 1.0.0
3
+ class Array
4
+ # Alias for original Array#pop
5
+ alias_method :pop_single, :pop
6
+
7
+ # Reimplements Array#pop to add support for popping multiple items at once.
8
+ #
9
+ # By default, Array#pop can only pop a single item in mruby
10
+ def pop(n = nil)
11
+ if n
12
+ res = []
13
+ n.times{ res << pop_single }
14
+ res.reverse!
15
+ else
16
+ pop_single
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ # @api mruby_patches
2
+ # @since 1.0.0
3
+
4
+ # let mruby created the root fiber with the correct class
5
+ Fiber.current
data/ext/mruby/io.rb ADDED
@@ -0,0 +1,54 @@
1
+ # @api mruby_patches
2
+ # @since 1.0.0
3
+ #
4
+ # mruby-io does not support non-blocking io operations.
5
+ class IO
6
+ unless const_defined? :EAGAIN
7
+ # raised if {IO#read_nonblock} or {IO#write_nonblock} would block
8
+ class EAGAIN < Exception; end
9
+ end
10
+
11
+ unless const_defined? :WaitReadable
12
+ # raised if IO#read_nonblock would block
13
+ module WaitReadable; end
14
+ class EAGAIN
15
+ include WaitReadable
16
+ end
17
+ end
18
+
19
+ unless const_defined? :WaitWritable
20
+ # raised if IO#write_nonblock would block
21
+ module WaitWritable; end
22
+ class EAGAIN
23
+ include WaitWritable
24
+ end
25
+ end
26
+
27
+ unless method_defined? :read_nonblock
28
+ # Implements IO#read_nonblock for mruby
29
+ #
30
+ # @see https://ruby-doc.org/core-1.9.3/IO.html#method-i-read_nonblock
31
+ # Ruby's documentation for IO#read_nonblock
32
+ def read_nonblock(maxlen, outbuf = nil)
33
+ if IO.select [self], nil, nil, 0
34
+ sysread(maxlen, outbuf)
35
+ else
36
+ raise EAGAIN, 'Resource temporarily unavailable - read would block'
37
+ end
38
+ end
39
+ end
40
+
41
+ unless method_defined? :write_nonblock
42
+ # Implements IO#write_nonblock for mruby
43
+ #
44
+ # @see https://ruby-doc.org/core-1.9.3/IO.html#method-i-write_nonblock
45
+ # Ruby's documentation for `IO#write_nonblock`
46
+ def write_nonblock(string)
47
+ if IO.select nil, [self], nil, 0
48
+ syswrite(string)
49
+ else
50
+ raise EAGAIN, 'Resource temporarily unavailable - write would block'
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,46 @@
1
+ # How to Install Concurrently
2
+
3
+ ## Ruby
4
+
5
+ Install the gem manually with
6
+
7
+ $ gem install concurrently
8
+
9
+ or manage your application's dependencies with [Bundler](https://bundler.io/):
10
+ Run
11
+
12
+ $ bundle
13
+
14
+ after you added
15
+
16
+ gem 'concurrently'
17
+
18
+ to your Gemfile.
19
+
20
+ Finally,
21
+
22
+ ```ruby
23
+ require 'concurrently'
24
+ ```
25
+
26
+ in your application.
27
+
28
+
29
+ ## mruby
30
+
31
+ To build Concurrently into mruby directly add it to mruby's build config or a
32
+ gem box:
33
+
34
+ ```ruby
35
+ MRuby::Build.new do |conf| # or MRuby::GemBox.new do |conf|
36
+ conf.gem mgem: 'mruby-concurrently'
37
+ end
38
+ ```
39
+
40
+ To use it in an mruby gem add it to the gem's specification as dependency:
41
+
42
+ ```ruby
43
+ MRuby::Gem::Specification.new('mruby-your-gem') do |spec|
44
+ spec.add_dependency 'mruby-concurrently'
45
+ end
46
+ ```
@@ -0,0 +1,335 @@
1
+ # An Overview of Concurrently
2
+
3
+ This document is meant as a general overview of what can be done with
4
+ Concurrently and how all its parts work together. For more information and
5
+ examples about a topic follow the interspersed links to the documentation.
6
+
7
+ ## Evaluations
8
+
9
+ An evaluation is an atomic thread of execution leading to a result. It is
10
+ similar to a thread or a fiber. It can be suspended and resumed independently
11
+ from other evaluations. It is also similar to a future or a promise by
12
+ providing access to its future result or offering the ability to inject a
13
+ result manually. Once the evaluation has a result it is *concluded*.
14
+
15
+ Every ruby program already has an implicit [root evaluation][Concurrently::Evaluation]
16
+ running. Calling a concurrent proc creates a [proc evaluation][Concurrently::Proc::Evaluation].
17
+
18
+ ## Concurrent Procs
19
+
20
+ The [concurrent proc][Concurrently::Proc] is Concurrently's concurrency
21
+ primitive. It looks and feels just like a regular proc. In fact,
22
+ [Concurrently::Proc][] inherits from `Proc`.
23
+
24
+ Concurrent procs are created with [Kernel#concurrent_proc][]:
25
+
26
+ ```ruby
27
+ concurrent_proc do
28
+ # code to run concurrently
29
+ end
30
+ ```
31
+
32
+ Concurrent procs can be used the same way regular procs are. For example, they
33
+ can be passed around or called multiple times with different arguments.
34
+
35
+ [Kernel#concurrently] is a shortcut for [Concurrently::Proc#call_and_forget][]:
36
+
37
+ ```ruby
38
+ concurrently do
39
+ # code to run concurrently
40
+ end
41
+
42
+ # is equivalent to:
43
+
44
+ concurrent_proc do
45
+ # code to run concurrently
46
+ end.call_and_forget
47
+ ```
48
+
49
+ ### Calling Concurrent Procs
50
+
51
+ A concurrent proc has four methods to call it.
52
+
53
+ The first two evaluate the concurrent proc immediately in the foreground:
54
+
55
+ * [Concurrently::Proc#call][] blocks the (root or proc) evaluation it has been
56
+ called from until its own evaluation is concluded. Then it returns the
57
+ result. This behaves just like `Proc#call`.
58
+ * [Concurrently::Proc#call_nonblock][] will not block the (root or proc)
59
+ evaluation it has been called from if it needs to wait. Instead, it
60
+ immediately returns its [evaluation][Concurrently::Proc::Evaluation]. If it
61
+ can be evaluated without waiting it returns the result.
62
+
63
+ The other two schedule the concurrent proc to be run in the background. The
64
+ evaluation is not started right away but is deferred until the the next
65
+ iteration of the event loop:
66
+
67
+ * [Concurrently::Proc#call_detached][] returns an [evaluation][Concurrently::Proc::Evaluation].
68
+ * [Concurrently::Proc#call_and_forget][] does not give access to the evaluation
69
+ and returns `nil`.
70
+
71
+
72
+ ## Timing Code
73
+
74
+ To defer the current evaluation for a fixed time use [Kernel#wait][].
75
+
76
+ * Doing something after X seconds:
77
+
78
+ ```ruby
79
+ concurrent_proc do
80
+ wait X
81
+ do_it!
82
+ end
83
+ ```
84
+
85
+ * Doing something every X seconds. This is a timer:
86
+
87
+ ```ruby
88
+ concurrent_proc do
89
+ loop do
90
+ wait X
91
+ do_it!
92
+ end
93
+ end
94
+ ```
95
+
96
+ * Doing something after X seconds, every Y seconds, Z times:
97
+
98
+ ```ruby
99
+ concurrent_proc do
100
+ wait X
101
+ Z.times do
102
+ do_it!
103
+ wait Y
104
+ end
105
+ end
106
+ ```
107
+
108
+
109
+ ## Handling I/O
110
+
111
+ Readiness of I/O is awaited with [IO#await_readable][] and [IO#await_writable][].
112
+ To read and write from an IO concurrently you can use [IO#concurrently_read][]
113
+ and [IO#concurrently_write][].
114
+
115
+ ```ruby
116
+ r,w = IO.pipe
117
+
118
+ concurrently do
119
+ wait 1
120
+ w.concurrently_write "Continue!"
121
+ end
122
+
123
+ concurrently do
124
+ # This runs while r awaits readability.
125
+ end
126
+
127
+ concurrently do
128
+ # This runs while r awaits readability.
129
+ end
130
+
131
+ # Read from r. It will take one second until there is input.
132
+ message = r.concurrently_read 1024
133
+
134
+ puts message # prints "Continue!"
135
+
136
+ r.close
137
+ w.close
138
+ ```
139
+
140
+ Other operations like accepting from a server socket need to be done by using
141
+ the corresponding `#*_nonblock` methods along with [IO#await_readable][] or
142
+ [IO#await_writable][]:
143
+
144
+ ```ruby
145
+ require 'socket'
146
+
147
+ server = UNIXServer.new "/tmp/sock"
148
+
149
+ begin
150
+ socket = server.accept_nonblock
151
+ rescue IO::WaitReadable
152
+ server.await_readable
153
+ retry
154
+ end
155
+
156
+ # socket is an accepted socket.
157
+ ```
158
+
159
+
160
+ ## Flow of Control
161
+
162
+ To understand when code is run (and when it is not) it is necessary to know
163
+ a little bit more about the way Concurrently works.
164
+
165
+ Concurrently lets every (real) thread run an [event loop][Concurrently::EventLoop].
166
+ These event loops are responsible for watching IOs and scheduling evaluations
167
+ of concurrent procs. Evaluations are scheduled by putting them into a run queue
168
+ ordered by the time they are supposed to run. The run queue is then worked off
169
+ sequentially. If two evaluations are scheduled to run at the same time the
170
+ evaluation scheduled first is run first.
171
+
172
+ Event loops *do not* run parallel to your application's code at the exact same
173
+ time (e.g. on another cpu core). Instead, your code yields to them if it
174
+ waits for something: **The event loop is (and only is) entered if your code
175
+ calls `#wait` or one of the `#await_*` methods.** Later, when your code can
176
+ be resumed the event loop schedules the corresponding evaluation to run again.
177
+
178
+ Keep in mind, that an event loop **must never be interrupted, blocked or
179
+ overloaded.** A healthy event loop is one that can respond to new events
180
+ immediately.
181
+
182
+ If you are experiencing issues when using Concurrently it is probably due to
183
+ these properties of event loops. Have a look at the [Troubleshooting][] page.
184
+
185
+
186
+ ## Implementing a Server Application
187
+
188
+ This is a blueprint how to build an application listening to a server socket,
189
+ accepting connections and serving requests through them.
190
+
191
+ At first, lets implement the server. It is initialized with a socket to listen
192
+ to. Listening calls the concurrent proc stored in the `RECEIVER` constant. It
193
+ then accepts or waits for incoming connections until the server is closed.
194
+
195
+ ```ruby
196
+ class ConcurrentServer
197
+ def initialize(socket)
198
+ @socket = socket
199
+ @listening = false
200
+ end
201
+
202
+ def listening?
203
+ @listening
204
+ end
205
+
206
+ def listen
207
+ @listening = true
208
+ RECEIVER.call_nonblock self, @socket
209
+ end
210
+
211
+ def close
212
+ @listening = false
213
+ @socket.close
214
+ end
215
+
216
+ RECEIVER = concurrent_proc do |server, socket|
217
+ while server.listening?
218
+ begin
219
+ Connection.new(socket.accept_nonblock).open
220
+ rescue IO::WaitReadable
221
+ socket.await_readable
222
+ retry
223
+ end
224
+ end
225
+ end
226
+ end
227
+ ```
228
+
229
+ The implementation of the connection is structurally similar to the one of the
230
+ server. But because receiving data is a little bit more complex it is done in
231
+ an additional receive buffer object. Received requests are processed in their
232
+ own concurrent proc to not block the receiver loop if `request.process` calls
233
+ one of the wait methods.
234
+
235
+ ```ruby
236
+ class ConcurrentServer::Connection
237
+ def initialize(socket)
238
+ @socket = socket
239
+ @receive_buffer = ReceiveBuffer.new socket
240
+ @open = false
241
+ end
242
+
243
+ def open?
244
+ @open
245
+ end
246
+
247
+ def open
248
+ @open = true
249
+ RECEIVER.call_nonblock self, @receive_buffer
250
+ end
251
+
252
+ def close
253
+ @open = false
254
+ @socket.close
255
+ end
256
+
257
+ RECEIVER = concurrent_proc do |connection, receive_buffer|
258
+ while connection.open?
259
+ receive_buffer.receive
260
+ receive_buffer.shift_complete_requests.each do |request|
261
+ REQUEST_PROC.call_nonblock request
262
+ end
263
+ end
264
+ end
265
+
266
+ REQUEST_PROC = concurrent_proc do |request|
267
+ request.process
268
+ end
269
+ end
270
+ ```
271
+
272
+ The receive buffer is responsible for reading from the connection's socket and
273
+ deserializing the received data.
274
+
275
+ ```ruby
276
+ class ConcurrentServer::Connection::ReceiveBuffer
277
+ def initialize(socket)
278
+ @socket = socket
279
+ @buffer = ''
280
+ end
281
+
282
+ def receive
283
+ @buffer << @socket.read_nonblock(32768)
284
+ rescue IO::WaitReadable
285
+ @socket.await_readable
286
+ retry
287
+ end
288
+
289
+ def shift_complete_requests
290
+ # Deserializes the buffer according to the used wire protocol, removes
291
+ # the consumed bytes of all completely received requests from the buffer
292
+ # and returns the requests.
293
+ end
294
+ end
295
+ ```
296
+
297
+ Finally, this is a script bootstrapping two concurrent servers. The script
298
+ terminates after both servers were closed.
299
+
300
+ ```ruby
301
+ #!/bin/env ruby
302
+
303
+ require 'socket'
304
+
305
+ socket1 = UNIXServer.new "/tmp/sock1"
306
+ socket2 = UNIXServer.new "/tmp/sock2"
307
+
308
+ server_evaluation1 = ConcurrentServer.new(socket1).listen
309
+ server_evaluation2 = ConcurrentServer.new(socket2).listen
310
+
311
+ server_evaluation1.await_result # blocks until server 1 is closed
312
+ server_evaluation2.await_result # returns immediately if server 2 is already
313
+ # closed or blocks until it happens
314
+ ```
315
+
316
+ Keep in mind, that to focus on the use of Concurrently the example does not
317
+ take error handling for I/O, properly closing all connections and other details
318
+ into account.
319
+
320
+ [Concurrently::Evaluation]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/Concurrently/Evaluation
321
+ [Concurrently::Proc]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/Concurrently/Proc
322
+ [Concurrently::Proc#call]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/Concurrently/Proc#call-instance_method
323
+ [Concurrently::Proc#call_nonblock]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/Concurrently/Proc#call_nonblock-instance_method
324
+ [Concurrently::Proc#call_detached]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/Concurrently/Proc#call_detached-instance_method
325
+ [Concurrently::Proc#call_and_forget]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/Concurrently/Proc#call_and_forget-instance_method
326
+ [Concurrently::Proc::Evaluation]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/Concurrently/Proc/Evaluation
327
+ [Concurrently::EventLoop]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/Concurrently/EventLoop
328
+ [Kernel#concurrent_proc]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/Kernel#concurrent_proc-instance_method
329
+ [Kernel#concurrently]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/Kernel#concurrently-instance_method
330
+ [Kernel#wait]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/Kernel#wait-instance_method
331
+ [IO#await_readable]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/IO#await_readable-instance_method
332
+ [IO#await_writable]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/IO#await_writable-instance_method
333
+ [IO#concurrently_read]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/IO#concurrently_read-instance_method
334
+ [IO#concurrently_write]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/IO#concurrently_write-instance_method
335
+ [Troubleshooting]: http://www.rubydoc.info/github/christopheraue/m-ruby-concurrently/file/guides/Troubleshooting.md