async 0.9.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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +4 -0
- data/.travis.yml +14 -0
- data/Gemfile +19 -0
- data/Guardfile +10 -0
- data/README.md +100 -0
- data/Rakefile +6 -0
- data/async.gemspec +33 -0
- data/examples/aio.rb +42 -0
- data/lib/async.rb +23 -0
- data/lib/async/io.rb +93 -0
- data/lib/async/logger.rb +39 -0
- data/lib/async/reactor.rb +166 -0
- data/lib/async/socket.rb +51 -0
- data/lib/async/ssl_socket.rb +30 -0
- data/lib/async/task.rb +102 -0
- data/lib/async/tcp_socket.rb +54 -0
- data/lib/async/udp_socket.rb +30 -0
- data/lib/async/unix_socket.rb +27 -0
- data/lib/async/version.rb +23 -0
- data/lib/async/wrapper.rb +71 -0
- data/spec/async/reactor_spec.rb +173 -0
- data/spec/async/task_spec.rb +95 -0
- data/spec/spec_helper.rb +41 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0c677beaeb3fb10276210be29052382b077ac6ce
|
4
|
+
data.tar.gz: bc0719a32fbffef3861f111d7a245c45b6e24786
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ad2df57e9e6ee7a4280a1b64c6429f6e8c69c2de40dcdd5cb9091c0e8f8b7debdd9f88d4ff3e049900aa16f97c48a3735630725ff4459db1e240360731844712
|
7
|
+
data.tar.gz: 1773d262ce191fcf566a385620ae7fd7f880f1e886f99e0f8bc4fd1829662b83e824cc3d694d6d4e37b25622608eff1a3452eb82b95e4f297f3aa9c41553d7d4
|
data/.gitignore
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
.tags*
|
19
|
+
documentation/run/*
|
20
|
+
documentation/public/code/*
|
21
|
+
.rspec_status
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in utopia.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :development do
|
7
|
+
gem 'pry'
|
8
|
+
gem 'guard-rspec'
|
9
|
+
|
10
|
+
gem 'yard'
|
11
|
+
end
|
12
|
+
|
13
|
+
group :test do
|
14
|
+
gem 'benchmark-ips'
|
15
|
+
gem 'ruby-prof', platforms: :mri
|
16
|
+
|
17
|
+
gem 'simplecov'
|
18
|
+
gem 'coveralls', require: false
|
19
|
+
end
|
data/Guardfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# Async
|
2
|
+
|
3
|
+
Asynchronous I/O framework for Ruby based on [nio4r] and [timers].
|
4
|
+
|
5
|
+
[timers]: https://github.com/socketry/timers
|
6
|
+
[nio4r]: https://github.com/socketry/nio4r
|
7
|
+
|
8
|
+
[](http://travis-ci.org/socketry/async)
|
9
|
+
[](https://codeclimate.com/github/socketry/async)
|
10
|
+
[](https://coveralls.io/r/socketry/async)
|
11
|
+
|
12
|
+
## Motivation
|
13
|
+
|
14
|
+
Several years ago, I was hosting websites on a server in my garage. Back then, my ADSL modem was very basic, and I wanted to have a DNS server which would resolve to an internal IP address when the domain itself resolved to my public IP. Thus was born [RubyDNS]. This project [was originally built on](https://github.com/ioquatix/rubydns/tree/v0.8.5) top of [EventMachine], but a lack of support for [IPv6 at the time](https://github.com/ioquatix/rubydns/issues/45) and [other problems](https://github.com/ioquatix/rubydns/issues/14), meant that I started looking for other options. Around that time [Celluloid] was picking up steam. I had not encountered actors before and I wanted to learn more about it. So, [I reimplemented RubyDNS on top of Celluloid](https://github.com/ioquatix/rubydns/tree/v0.9.0) and this eventually became the first stable release.
|
15
|
+
|
16
|
+
Moving forward, I refactored the internals of RubyDNS into [Celluloid::DNS]. This rewrite helped solidify the design of RubyDNS and to a certain extent it works. However, [unfixed bugs and design problems](https://github.com/celluloid/celluloid/pull/710) in Celluloid meant that RubyDNS 2.0 was delayed by almost 2 years. I wasn't happy releasing things with known bugs and problems. Anyway, a lot of discussion and thinking, I decided to build a small event reactor using [nio4r] and [timers], the core parts of [Celluloid::IO] which made it work so well.
|
17
|
+
|
18
|
+
[Celluloid]: https://github.com/celluloid/celluloid
|
19
|
+
[Celluloid::IO]: https://github.com/celluloid/celluloid-io
|
20
|
+
[Celluloid::DNS]: https://github.com/celluloid/celluloid-dns
|
21
|
+
[EventMachine]: https://github.com/eventmachine/eventmachine
|
22
|
+
[RubyDNS]: https://github.com/ioquatix/rubydns
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Add this line to your application's Gemfile:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
gem "async"
|
30
|
+
```
|
31
|
+
|
32
|
+
And then execute:
|
33
|
+
|
34
|
+
$ bundle
|
35
|
+
|
36
|
+
Or install it yourself as:
|
37
|
+
|
38
|
+
$ gem install async
|
39
|
+
|
40
|
+
## Supported Ruby Versions
|
41
|
+
|
42
|
+
This library aims to support and is [tested against][travis] the following Ruby
|
43
|
+
versions:
|
44
|
+
|
45
|
+
* Ruby 2.2.6+
|
46
|
+
* Ruby 2.3
|
47
|
+
* Ruby 2.4
|
48
|
+
* JRuby 9.1.6.0+
|
49
|
+
|
50
|
+
If something doesn't work on one of these versions, it's a bug.
|
51
|
+
|
52
|
+
This library may inadvertently work (or seem to work) on other Ruby versions,
|
53
|
+
however support will only be provided for the versions listed above.
|
54
|
+
|
55
|
+
If you would like this library to support another Ruby version or
|
56
|
+
implementation, you may volunteer to be a maintainer. Being a maintainer
|
57
|
+
entails making sure all tests run and pass on that implementation. When
|
58
|
+
something breaks on your implementation, you will be responsible for providing
|
59
|
+
patches in a timely fashion. If critical issues for a particular implementation
|
60
|
+
exist at the time of a major release, support for that Ruby version may be
|
61
|
+
dropped.
|
62
|
+
|
63
|
+
[travis]: http://travis-ci.org/socketry/async
|
64
|
+
|
65
|
+
## Contributing
|
66
|
+
|
67
|
+
1. Fork it
|
68
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
69
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
70
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
71
|
+
5. Create new Pull Request
|
72
|
+
|
73
|
+
## See Also
|
74
|
+
|
75
|
+
- [async-dns](https://github.com/socketry/async-dns) — Asynchronous DNS resolver and server.
|
76
|
+
- [rubydns](https://github.com/socketry/rubydns) — A easy to use Ruby DNS server.
|
77
|
+
|
78
|
+
## License
|
79
|
+
|
80
|
+
Released under the MIT license.
|
81
|
+
|
82
|
+
Copyright, 2017, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
|
83
|
+
|
84
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
85
|
+
of this software and associated documentation files (the "Software"), to deal
|
86
|
+
in the Software without restriction, including without limitation the rights
|
87
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
88
|
+
copies of the Software, and to permit persons to whom the Software is
|
89
|
+
furnished to do so, subject to the following conditions:
|
90
|
+
|
91
|
+
The above copyright notice and this permission notice shall be included in
|
92
|
+
all copies or substantial portions of the Software.
|
93
|
+
|
94
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
95
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
96
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
97
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
98
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
99
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
100
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
data/async.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require_relative 'lib/async/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = "async"
|
6
|
+
spec.version = Async::VERSION
|
7
|
+
spec.authors = ["Samuel Williams"]
|
8
|
+
spec.email = ["samuel.williams@oriontransfer.co.nz"]
|
9
|
+
spec.description = <<-EOF
|
10
|
+
Async provides a modern asynchronous I/O framework for Ruby, based
|
11
|
+
on nio4r. It implements the reactor pattern, providing both IO and timer
|
12
|
+
based events.
|
13
|
+
EOF
|
14
|
+
spec.summary = "Async is an asynchronous I/O framework based on nio4r."
|
15
|
+
spec.homepage = "https://github.com/socketry/async"
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
spec.files = `git ls-files`.split($/)
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
spec.has_rdoc = "yard"
|
23
|
+
|
24
|
+
spec.required_ruby_version = ">= 2.2.6"
|
25
|
+
|
26
|
+
spec.add_runtime_dependency "nio4r", "~> 2"
|
27
|
+
spec.add_runtime_dependency "timers", "~> 4.1"
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
30
|
+
spec.add_development_dependency "process-daemon", "~> 1.0.0"
|
31
|
+
spec.add_development_dependency "rspec", "~> 3.4.0"
|
32
|
+
spec.add_development_dependency "rake"
|
33
|
+
end
|
data/examples/aio.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'async'
|
4
|
+
|
5
|
+
reactor = Async::Reactor.new
|
6
|
+
|
7
|
+
puts "Creating server"
|
8
|
+
server = TCPServer.new("localhost", 6777)
|
9
|
+
|
10
|
+
REPEATS = 10
|
11
|
+
|
12
|
+
timer = reactor.after(1) do
|
13
|
+
puts "Reactor timed out!"
|
14
|
+
reactor.stop
|
15
|
+
end
|
16
|
+
|
17
|
+
reactor.async(server) do |server, task|
|
18
|
+
REPEATS.times do |i|
|
19
|
+
puts "Accepting peer on server #{server}"
|
20
|
+
task.with(server.accept) do |peer|
|
21
|
+
puts "Sending data to peer"
|
22
|
+
peer << "data #{i}"
|
23
|
+
peer.shutdown
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
puts "Server finished, canceling timer"
|
28
|
+
timer.cancel
|
29
|
+
end
|
30
|
+
|
31
|
+
REPEATS.times do |i|
|
32
|
+
# This aspect of the connection is synchronous.
|
33
|
+
puts "Creating client #{i}"
|
34
|
+
client = TCPSocket.new("localhost", 6777)
|
35
|
+
|
36
|
+
reactor.async(client) do |client|
|
37
|
+
puts "Reading data on client #{i}"
|
38
|
+
puts client.read(1024)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
reactor.run
|
data/lib/async.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative "async/version"
|
22
|
+
require_relative "async/logger"
|
23
|
+
require_relative "async/reactor"
|
data/lib/async/io.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'wrapper'
|
22
|
+
|
23
|
+
require 'forwardable'
|
24
|
+
|
25
|
+
module Async
|
26
|
+
# Represents an asynchronous IO within a reactor.
|
27
|
+
class IO < Wrapper
|
28
|
+
extend Forwardable
|
29
|
+
|
30
|
+
WRAPPERS = {}
|
31
|
+
|
32
|
+
def self.[] instance
|
33
|
+
WRAPPERS[instance.class]
|
34
|
+
end
|
35
|
+
|
36
|
+
class << self
|
37
|
+
if RUBY_VERSION >= "2.3"
|
38
|
+
def wrap_blocking_method(new_name, method_name)
|
39
|
+
# puts "#{self}\##{$1} -> #{method_name}"
|
40
|
+
define_method(new_name) do |*args|
|
41
|
+
async do
|
42
|
+
@io.__send__(method_name, *args, exception: false)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
else
|
47
|
+
def wrap_blocking_method(new_name, method_name)
|
48
|
+
# puts "#{self}\##{$1} -> #{method_name}"
|
49
|
+
define_method(new_name) do |*args|
|
50
|
+
async do
|
51
|
+
@io.__send__(method_name, *args)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def wraps(klass, *additional_methods)
|
58
|
+
WRAPPERS[klass] = self
|
59
|
+
|
60
|
+
klass.instance_methods(false).grep(/(.*)_nonblock/) do |method_name|
|
61
|
+
wrap_blocking_method($1, method_name)
|
62
|
+
end
|
63
|
+
|
64
|
+
def_delegators :@io, *(additional_methods - instance_methods(false))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
wraps ::IO
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def async
|
73
|
+
while true
|
74
|
+
begin
|
75
|
+
result = yield
|
76
|
+
|
77
|
+
case result
|
78
|
+
when :wait_readable
|
79
|
+
wait_readable
|
80
|
+
when :wait_writable
|
81
|
+
wait_writable
|
82
|
+
else
|
83
|
+
return result
|
84
|
+
end
|
85
|
+
rescue ::IO::WaitReadable
|
86
|
+
wait_readable
|
87
|
+
rescue ::IO::WaitWritable
|
88
|
+
wait_writable
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/async/logger.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'logger'
|
22
|
+
|
23
|
+
module Async
|
24
|
+
class << self
|
25
|
+
attr :logger
|
26
|
+
|
27
|
+
def default_log_level
|
28
|
+
if $DEBUG
|
29
|
+
Logger::DEBUG
|
30
|
+
elsif $VERBOSE
|
31
|
+
Logger::INFO
|
32
|
+
else
|
33
|
+
Logger::WARN
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
@logger = Logger.new($stderr, level: default_log_level)
|
39
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'logger'
|
22
|
+
require_relative 'task'
|
23
|
+
require_relative 'wrapper'
|
24
|
+
|
25
|
+
require 'nio'
|
26
|
+
require 'timers'
|
27
|
+
require 'forwardable'
|
28
|
+
|
29
|
+
module Async
|
30
|
+
class TimeoutError < RuntimeError
|
31
|
+
end
|
32
|
+
|
33
|
+
class Reactor
|
34
|
+
extend Forwardable
|
35
|
+
|
36
|
+
def self.run(*args, &block)
|
37
|
+
reactor = self.new
|
38
|
+
|
39
|
+
reactor.async(*args, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(wrappers: IO)
|
43
|
+
@wrappers = wrappers
|
44
|
+
|
45
|
+
@selector = NIO::Selector.new
|
46
|
+
@timers = Timers::Group.new
|
47
|
+
|
48
|
+
@fibers = []
|
49
|
+
|
50
|
+
@stopped = true
|
51
|
+
end
|
52
|
+
|
53
|
+
attr :wrappers
|
54
|
+
attr :stopped
|
55
|
+
|
56
|
+
def_delegators :@timers, :every, :after
|
57
|
+
|
58
|
+
def wrap(io, task)
|
59
|
+
@wrappers[io].new(io, task)
|
60
|
+
end
|
61
|
+
|
62
|
+
def with(io, &block)
|
63
|
+
async do |task|
|
64
|
+
task.with(io, &block)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def async(*ios, &block)
|
69
|
+
task = Task.new(ios, self, &block)
|
70
|
+
|
71
|
+
# I want to take a moment to explain the logic of this.
|
72
|
+
# When calling an async block, we deterministically execute it until the
|
73
|
+
# first blocking operation. We don't *have* to do this - we could schedule
|
74
|
+
# it for later execution, but it's useful to:
|
75
|
+
# - Fail at the point of call where possible.
|
76
|
+
# - Execute determinstically where possible.
|
77
|
+
# - Avoid overhead if no blocking operation is performed.
|
78
|
+
fiber = task.run
|
79
|
+
|
80
|
+
# We only start tracking this if the fiber is still alive:
|
81
|
+
@fibers << fiber if fiber.alive?
|
82
|
+
|
83
|
+
# Async.logger.debug "Initial execution of task #{fiber} complete (#{result} -> #{fiber.alive?})..."
|
84
|
+
return task
|
85
|
+
end
|
86
|
+
|
87
|
+
def register(*args)
|
88
|
+
@selector.register(*args)
|
89
|
+
end
|
90
|
+
|
91
|
+
def stop
|
92
|
+
@stopped = true
|
93
|
+
end
|
94
|
+
|
95
|
+
def run(*args, &block)
|
96
|
+
@stopped = false
|
97
|
+
|
98
|
+
# Allow the user to kick of the initial async tasks.
|
99
|
+
async(*args, &block) if block_given?
|
100
|
+
|
101
|
+
@timers.wait do |interval|
|
102
|
+
# - nil: no timers
|
103
|
+
# - -ve: timers expired already
|
104
|
+
# - 0: timers ready to fire
|
105
|
+
# - +ve: timers waiting to fire
|
106
|
+
interval = 0 if interval && interval < 0
|
107
|
+
|
108
|
+
# Async.logger.debug "[#{self} Pre] Updating #{@fibers.count} fibers..."
|
109
|
+
# Async.logger.debug @fibers.collect{|fiber| [fiber, fiber.alive?]}.inspect
|
110
|
+
# As timeouts may have been updated, and caused fibers to complete, we should check this.
|
111
|
+
@fibers.delete_if{|fiber| !fiber.alive?}
|
112
|
+
|
113
|
+
# If there is nothing to do, then finish:
|
114
|
+
# Async.logger.debug "[#{self}] @fibers.empty? = #{@fibers.empty?} && interval #{interval.inspect}"
|
115
|
+
return if @fibers.empty? && interval.nil?
|
116
|
+
|
117
|
+
# Async.logger.debug "Selecting with #{@fibers.count} fibers interval = #{interval}..."
|
118
|
+
if monitors = @selector.select(interval)
|
119
|
+
monitors.each do |monitor|
|
120
|
+
if task = monitor.value
|
121
|
+
# Async.logger.debug "Resuming task #{task} due to IO..."
|
122
|
+
task.resume
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end until @stopped
|
127
|
+
ensure
|
128
|
+
# Async.logger.debug "[#{self} Ensure] Exiting run-loop (stopped: #{@stopped} exception: #{$!})..."
|
129
|
+
# Async.logger.debug @fibers.collect{|fiber| [fiber, fiber.alive?]}.inspect
|
130
|
+
@stopped = true
|
131
|
+
end
|
132
|
+
|
133
|
+
def sleep(duration)
|
134
|
+
task = Fiber.current
|
135
|
+
|
136
|
+
timer = self.after(duration) do
|
137
|
+
if task.alive?
|
138
|
+
task.resume
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
result = Fiber.yield
|
143
|
+
|
144
|
+
raise result if result.is_a? Exception
|
145
|
+
ensure
|
146
|
+
timer.cancel if timer
|
147
|
+
end
|
148
|
+
|
149
|
+
def timeout(duration)
|
150
|
+
backtrace = caller
|
151
|
+
task = Fiber.current
|
152
|
+
|
153
|
+
timer = self.after(duration) do
|
154
|
+
if task.alive?
|
155
|
+
error = TimeoutError.new("execution expired")
|
156
|
+
error.set_backtrace backtrace
|
157
|
+
task.resume error
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
yield
|
162
|
+
ensure
|
163
|
+
timer.cancel if timer
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|