async 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,51 @@
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 'io'
22
+
23
+ require 'socket'
24
+
25
+ module Async
26
+ class BasicSocket < IO
27
+ wraps ::BasicSocket
28
+
29
+ # We provide non-blocking send:
30
+ alias send sendmsg
31
+ end
32
+
33
+ class Socket < BasicSocket
34
+ wraps ::Socket
35
+
36
+ module Connect
37
+ def connect(*args)
38
+ begin
39
+ super
40
+ rescue Errno::EISCONN
41
+ end
42
+ end
43
+ end
44
+
45
+ prepend Connect
46
+ end
47
+
48
+ class IPSocket < BasicSocket
49
+ wraps ::IPSocket
50
+ end
51
+ end
@@ -0,0 +1,30 @@
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 'io'
22
+
23
+ require 'openssl'
24
+
25
+ module Async
26
+ # This might be better in a nested module?
27
+ class SSLSocket < IO
28
+ wraps OpenSSL::SSL::SSLSocket
29
+ end
30
+ end
data/lib/async/task.rb ADDED
@@ -0,0 +1,102 @@
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 'fiber'
22
+ require 'forwardable'
23
+
24
+ module Async
25
+ class Interrupt < Exception
26
+ end
27
+
28
+ class Task
29
+ extend Forwardable
30
+
31
+ def initialize(ios, reactor, &block)
32
+ @ios = Hash[
33
+ ios.collect{|io| [io.fileno, reactor.wrap(io, self)]}
34
+ ]
35
+
36
+ @reactor = reactor
37
+
38
+ @fiber = Fiber.new do
39
+ set!
40
+
41
+ begin
42
+ yield(*@ios.values, self)
43
+ # Async.logger.debug("Task #{self} completed normally.")
44
+ rescue Interrupt
45
+ # Async.logger.debug("Task #{self} interrupted: #{$!}")
46
+ ensure
47
+ close
48
+ end
49
+ end
50
+ end
51
+
52
+ def_delegators :@reactor, :timeout, :sleep
53
+
54
+ def run
55
+ @fiber.resume
56
+
57
+ return @fiber
58
+ end
59
+
60
+ def stop!
61
+ if @fiber.alive?
62
+ exception = Interrupt.new("Stop right now!")
63
+ @fiber.resume(exception)
64
+ end
65
+ end
66
+
67
+ attr :ios
68
+ attr :reactor
69
+
70
+ def with(io)
71
+ wrapper = @reactor.wrap(io, self)
72
+
73
+ yield wrapper
74
+ ensure
75
+ wrapper.close
76
+ io.close
77
+ end
78
+
79
+ def bind(io)
80
+ @ios[io.fileno] ||= reactor.wrap(io, self)
81
+ end
82
+
83
+ def register(io, interests)
84
+ @reactor.register(io, interests)
85
+ end
86
+
87
+ def self.current
88
+ Thread.current[:async_task] or raise RuntimeError, "No async task available!"
89
+ end
90
+
91
+ private
92
+
93
+ def close
94
+ @ios.each_value(&:close)
95
+ end
96
+
97
+ def set!
98
+ # This is actually fiber-local:
99
+ Thread.current[:async_task] = self
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,54 @@
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 'socket'
22
+
23
+ module Async
24
+ # Asynchronous TCP socket/client.
25
+ class TCPSocket < IPSocket
26
+ wraps ::TCPSocket
27
+
28
+ def self.connect(remote_address, remote_port, local_address = nil, local_port = nil, task: Task.current, &block)
29
+ # This may block if remote_address is a hostname
30
+ remote = Addrinfo.tcp(remote_address, remote_port)
31
+
32
+ socket = ::Socket.new(remote.afamily, ::Socket::SOCK_STREAM, 0)
33
+ socket.bind Addrinfo.tcp(local_address, local_port) if local_address
34
+
35
+ if block_given?
36
+ task.with(socket) do |wrapper|
37
+ wrapper.connect(remote.to_sockaddr)
38
+
39
+ yield wrapper
40
+ end
41
+ else
42
+ task.bind(socket).connect(remote.to_sockaddr)
43
+
44
+ return socket
45
+ end
46
+ end
47
+ end
48
+
49
+ # Asynchronous TCP server
50
+ class TCPServer < TCPSocket
51
+ wraps ::TCPServer
52
+ end
53
+ end
54
+
@@ -0,0 +1,30 @@
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 'socket'
22
+
23
+ module Async
24
+ # Asynchronous UDP socket.
25
+ class UDPSocket < IPSocket
26
+ # We pass `send` through directly, but in theory it might block. Internally, it uses sendto.
27
+ wraps ::UDPSocket, :send
28
+ end
29
+ end
30
+
@@ -0,0 +1,27 @@
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 'socket'
22
+
23
+ module Async
24
+ module UNIXSocket < BasicSocket
25
+ wraps ::UNIXSocket
26
+ end
27
+ end
@@ -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
+ module Async
22
+ VERSION = "0.9.1"
23
+ end
@@ -0,0 +1,71 @@
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
+ module Async
22
+ # Represents an asynchronous IO within a reactor.
23
+ class Wrapper
24
+ def initialize(io, task)
25
+ @io = io
26
+ @task = task
27
+ @monitor = nil
28
+ end
29
+
30
+ attr :io
31
+ attr :task
32
+
33
+ def monitor(interests)
34
+ unless @monitor
35
+ @monitor = @task.register(@io, interests)
36
+ else
37
+ @monitor.interests = interests
38
+ end
39
+
40
+ @monitor.value = Fiber.current
41
+
42
+ yield
43
+
44
+ ensure
45
+ @monitor.value = nil if @monitor
46
+ end
47
+
48
+ def wait_readable
49
+ wait_any(:r)
50
+ end
51
+
52
+ def wait_writable
53
+ wait_any(:w)
54
+ end
55
+
56
+ def wait_any(interests = :rw)
57
+ monitor(interests) do
58
+ # Async.logger.debug "Fiber #{Fiber.current} yielding..."
59
+ result = Fiber.yield
60
+
61
+ # Async.logger.debug "Fiber #{Fiber.current} resuming with result #{result}..."
62
+ raise result if result.is_a? Exception
63
+ end
64
+ end
65
+
66
+ def close
67
+ @monitor.close if @monitor
68
+ @monitor = nil
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,173 @@
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
+ RSpec.describe Async::Reactor do
22
+ # Shared port for localhost network tests.
23
+ let(:port) {6778}
24
+
25
+ it "can run asynchronously" do
26
+ outer_fiber = Fiber.current
27
+ inner_fiber = nil
28
+
29
+ described_class.run do
30
+ inner_fiber = Fiber.current
31
+ end
32
+
33
+ expect(outer_fiber).not_to be == inner_fiber
34
+ end
35
+
36
+ it "can be stopped" do
37
+ state = nil
38
+
39
+ subject.async do |task|
40
+ state = :started
41
+ task.sleep(10)
42
+ state = :stopped
43
+ end
44
+
45
+ subject.stop
46
+
47
+ expect(state).to be == :started
48
+ end
49
+
50
+ describe 'basic tcp server' do
51
+ include_context "reactor"
52
+
53
+ # These may block:
54
+ let(:server) {TCPServer.new("localhost", port)}
55
+ let(:client) {TCPSocket.new("localhost", port)}
56
+
57
+ let(:data) {"The quick brown fox jumped over the lazy dog."}
58
+
59
+ after(:each) do
60
+ server.close
61
+ end
62
+
63
+ it "should start server and send data" do
64
+ subject.async(server) do |server, task|
65
+ task.with(server.accept) do |peer|
66
+ peer.write(peer.read(512))
67
+ end
68
+ end
69
+
70
+ subject.with(client) do |client|
71
+ client.write(data)
72
+
73
+ expect(client.read(512)).to be == data
74
+ end
75
+
76
+ subject.run
77
+
78
+ expect(client).to be_closed
79
+ end
80
+ end
81
+
82
+ describe 'non-blocking tcp connect' do
83
+ include_context "reactor"
84
+
85
+ # These may block:
86
+ let(:server) {TCPServer.new("localhost", port)}
87
+
88
+ let(:data) {"The quick brown fox jumped over the lazy dog."}
89
+
90
+ after(:each) do
91
+ server.close
92
+ end
93
+
94
+ it "should start server and send data" do
95
+ subject.async(server) do |server, task|
96
+ task.with(server.accept) do |peer|
97
+ peer.write(peer.read(512))
98
+ end
99
+ end
100
+
101
+ subject.async do |task|
102
+ Async::TCPSocket.connect("localhost", port) do |client|
103
+ client.write(data)
104
+ expect(client.read(512)).to be == data
105
+ end
106
+ end
107
+
108
+ subject.run
109
+ end
110
+
111
+ it "can connect socket and read/write in a different task" do
112
+ subject.async(server) do |server, task|
113
+ task.with(server.accept) do |peer|
114
+ peer.write(peer.read(512))
115
+ end
116
+ end
117
+
118
+ socket = nil
119
+
120
+ subject.async do |task|
121
+ socket = Async::TCPSocket.connect("localhost", port)
122
+
123
+ # Stop the reactor once the connection was made.
124
+ subject.stop
125
+ end
126
+
127
+ subject.run
128
+
129
+ expect(socket).to_not be_nil
130
+
131
+ subject.async(socket) do |client|
132
+ client.write(data)
133
+ expect(client.read(512)).to be == data
134
+ end
135
+
136
+ subject.run
137
+ end
138
+ end
139
+
140
+ describe 'basic udp server' do
141
+ include_context "reactor"
142
+
143
+ # These may block:
144
+ let(:server) {UDPSocket.new.tap{|socket| socket.bind("localhost", port)}}
145
+ let(:client) {UDPSocket.new}
146
+
147
+ let(:data) {"The quick brown fox jumped over the lazy dog."}
148
+
149
+ after(:each) do
150
+ server.close
151
+ end
152
+
153
+ it "should echo data back to peer" do
154
+ subject.async(server) do |server, task|
155
+ packet, (_, remote_port, remote_host) = server.recvfrom(512)
156
+
157
+ reactor.async do
158
+ server.send(packet, 0, remote_host, remote_port)
159
+ end
160
+ end
161
+
162
+ subject.async(client) do |client|
163
+ client.send(data, 0, "localhost", port)
164
+
165
+ response, _ = client.recvfrom(512)
166
+
167
+ expect(response).to be == data
168
+ end
169
+
170
+ subject.run
171
+ end
172
+ end
173
+ end