polyphony 0.13
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/CHANGELOG.md +86 -0
- data/README.md +400 -0
- data/ext/ev/extconf.rb +19 -0
- data/lib/polyphony.rb +26 -0
- data/lib/polyphony/core.rb +45 -0
- data/lib/polyphony/core/async.rb +36 -0
- data/lib/polyphony/core/cancel_scope.rb +61 -0
- data/lib/polyphony/core/channel.rb +39 -0
- data/lib/polyphony/core/coroutine.rb +106 -0
- data/lib/polyphony/core/exceptions.rb +24 -0
- data/lib/polyphony/core/fiber_pool.rb +98 -0
- data/lib/polyphony/core/supervisor.rb +75 -0
- data/lib/polyphony/core/sync.rb +20 -0
- data/lib/polyphony/core/thread.rb +49 -0
- data/lib/polyphony/core/thread_pool.rb +58 -0
- data/lib/polyphony/core/throttler.rb +38 -0
- data/lib/polyphony/extensions/io.rb +62 -0
- data/lib/polyphony/extensions/kernel.rb +161 -0
- data/lib/polyphony/extensions/postgres.rb +96 -0
- data/lib/polyphony/extensions/redis.rb +68 -0
- data/lib/polyphony/extensions/socket.rb +85 -0
- data/lib/polyphony/extensions/ssl.rb +73 -0
- data/lib/polyphony/fs.rb +22 -0
- data/lib/polyphony/http/agent.rb +214 -0
- data/lib/polyphony/http/http1.rb +124 -0
- data/lib/polyphony/http/http1_request.rb +71 -0
- data/lib/polyphony/http/http2.rb +66 -0
- data/lib/polyphony/http/http2_request.rb +69 -0
- data/lib/polyphony/http/rack.rb +27 -0
- data/lib/polyphony/http/server.rb +43 -0
- data/lib/polyphony/line_reader.rb +82 -0
- data/lib/polyphony/net.rb +59 -0
- data/lib/polyphony/net_old.rb +299 -0
- data/lib/polyphony/resource_pool.rb +56 -0
- data/lib/polyphony/server_task.rb +18 -0
- data/lib/polyphony/testing.rb +34 -0
- data/lib/polyphony/version.rb +5 -0
- metadata +170 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :tcp_connect,
|
4
|
+
:tcp_listen,
|
5
|
+
:getaddrinfo
|
6
|
+
|
7
|
+
import('./extensions/socket')
|
8
|
+
import('./extensions/ssl')
|
9
|
+
|
10
|
+
def tcp_connect(host, port, opts = {})
|
11
|
+
socket = ::Socket.new(:INET, :STREAM).tap { |s|
|
12
|
+
addr = ::Socket.sockaddr_in(port, host)
|
13
|
+
s.connect(addr)
|
14
|
+
}
|
15
|
+
if opts[:secure_context] || opts[:secure]
|
16
|
+
secure_socket(socket, opts[:secure_context], opts)
|
17
|
+
else
|
18
|
+
socket
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def tcp_listen(host = nil, port = nil, opts = {})
|
23
|
+
host ||= '0.0.0.0'
|
24
|
+
raise "Port number not specified" unless port
|
25
|
+
socket = ::Socket.new(:INET, :STREAM).tap { |s|
|
26
|
+
s.reuse_addr if opts[:reuse_addr]
|
27
|
+
s.dont_linger if opts[:dont_linger]
|
28
|
+
addr = ::Socket.sockaddr_in(port, host)
|
29
|
+
s.bind(addr)
|
30
|
+
s.listen(0)
|
31
|
+
}
|
32
|
+
if opts[:secure_context] || opts[:secure]
|
33
|
+
secure_server(socket, opts[:secure_context], opts)
|
34
|
+
else
|
35
|
+
socket
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
DEFAULT_SSL_CONTEXT = OpenSSL::SSL::SSLContext.new
|
40
|
+
DEFAULT_SSL_CONTEXT.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
|
41
|
+
|
42
|
+
def secure_socket(socket, context, opts)
|
43
|
+
context ||= DEFAULT_SSL_CONTEXT
|
44
|
+
setup_alpn(context, opts[:alpn_protocols]) if opts[:alpn_protocols]
|
45
|
+
OpenSSL::SSL::SSLSocket.new(socket, context).tap { |s| s.connect }
|
46
|
+
end
|
47
|
+
|
48
|
+
def secure_server(socket, context, opts)
|
49
|
+
context ||= DEFAULT_SSL_CONTEXT
|
50
|
+
setup_alpn(context, opts[:alpn_protocols]) if opts[:alpn_protocols]
|
51
|
+
OpenSSL::SSL::SSLServer.new(socket, context)
|
52
|
+
end
|
53
|
+
|
54
|
+
def setup_alpn(context, protocols)
|
55
|
+
context.alpn_protocols = protocols
|
56
|
+
context.alpn_select_cb = ->(peer_protocols) {
|
57
|
+
(protocols & peer_protocols).first
|
58
|
+
}
|
59
|
+
end
|
@@ -0,0 +1,299 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :Server, :Socket
|
4
|
+
|
5
|
+
require 'socket'
|
6
|
+
require 'openssl'
|
7
|
+
|
8
|
+
Core = import('./core')
|
9
|
+
IO = import('./io')
|
10
|
+
|
11
|
+
# Implements a TCP server
|
12
|
+
class Server
|
13
|
+
# Initializes server
|
14
|
+
def initialize(opts = {})
|
15
|
+
@opts = opts
|
16
|
+
@callbacks = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Listens on host and port given in opts
|
20
|
+
# @param opts [Hash] options
|
21
|
+
# @return [void]
|
22
|
+
def listen(opts)
|
23
|
+
@secure_context = opts[:secure_context]
|
24
|
+
@server = opts[:socket] || create_server_socket(opts)
|
25
|
+
@watcher = EV::IO.new(@server, :r, true) { accept_from_socket }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates a server socket for listening to incoming connections
|
29
|
+
# @param opts [Hash] listening options
|
30
|
+
# @return [Net::Socket]
|
31
|
+
def create_server_socket(opts)
|
32
|
+
socket = TCPServer.new(opts[:host] || '127.0.0.1', opts[:port])
|
33
|
+
if @secure_context
|
34
|
+
socket = OpenSSL::SSL::SSLServer.new(socket, @secure_context)
|
35
|
+
socket.start_immediately = false
|
36
|
+
setup_alpn(opts[:alpn_protocols]) if opts[:alpn_protocols]
|
37
|
+
end
|
38
|
+
socket
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sets up ALPN protocols negotiated during handshake
|
42
|
+
# @param server_protocols [Array<String>] protocols supported by server
|
43
|
+
# @return [void]
|
44
|
+
def setup_alpn(server_protocols)
|
45
|
+
@secure_context.alpn_protocols = server_protocols
|
46
|
+
@secure_context.alpn_select_cb = proc do |client_protocols|
|
47
|
+
# select first common protocol
|
48
|
+
(server_protocols & client_protocols).first
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns true if server is listening
|
53
|
+
# @return [Boolean]
|
54
|
+
def listening?
|
55
|
+
@server
|
56
|
+
end
|
57
|
+
|
58
|
+
# Closes the server socket
|
59
|
+
# @return [void]
|
60
|
+
def close
|
61
|
+
Core.unwatch(@server)
|
62
|
+
@server.close
|
63
|
+
@server = nil
|
64
|
+
end
|
65
|
+
|
66
|
+
# Accepts an incoming connection, triggers the :connection callback
|
67
|
+
# @return [void]
|
68
|
+
def accept_from_socket
|
69
|
+
socket = @server.accept
|
70
|
+
setup_connection(socket) if socket
|
71
|
+
rescue StandardError => e
|
72
|
+
puts "error in accept_from_socket: #{e.inspect}"
|
73
|
+
puts e.backtrace.join("\n")
|
74
|
+
end
|
75
|
+
|
76
|
+
# Sets up an accepted connection
|
77
|
+
# @param socket [TCPSocket] accepted socket
|
78
|
+
# @return [Net::Socket]
|
79
|
+
def setup_connection(socket)
|
80
|
+
opts = { connected: true, secure_context: @secure_context }
|
81
|
+
if @secure_context
|
82
|
+
connection = SecureSocket.new(socket, opts)
|
83
|
+
connection.on(:handshake, &@callbacks[:connection])
|
84
|
+
else
|
85
|
+
connection = Socket.new(socket, opts)
|
86
|
+
@callbacks[:connection]&.(connection)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Registers a callback for given event
|
91
|
+
# @param event [Symbol] event kind
|
92
|
+
# @return [void]
|
93
|
+
def on(event, &block)
|
94
|
+
@callbacks[event] = block
|
95
|
+
end
|
96
|
+
|
97
|
+
# Returns a promise fulfilled upon the first incoming connection
|
98
|
+
# @return [Promise]
|
99
|
+
def connection(&block)
|
100
|
+
Core.promise(then: block, catch: block) do |p|
|
101
|
+
@callbacks[:connection] = p.to_proc
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Creates a generator promise, iterating asynchronously over incoming
|
106
|
+
# connections
|
107
|
+
# @return [void]
|
108
|
+
def each_connection(&block)
|
109
|
+
Core.promise(recurring: true) do |p|
|
110
|
+
@callbacks[:connection] = p.to_proc
|
111
|
+
end.each(&block)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Client connection functionality
|
116
|
+
module ClientConnection
|
117
|
+
# Connects to the given host & port, returning a promise fulfilled once
|
118
|
+
# connected. Options can include:
|
119
|
+
# :timeout => connection timeout in seconds
|
120
|
+
# @param host [String] host domain name or IP address
|
121
|
+
# @param port [Integer] port number
|
122
|
+
# @param opts [Hash] options
|
123
|
+
# @return [Promise] connection promise
|
124
|
+
def connect(host, port, opts = {})
|
125
|
+
Core.promise do |p|
|
126
|
+
socket = ::Socket.new(::Socket::AF_INET, ::Socket::SOCK_STREAM)
|
127
|
+
p.timeout(opts[:timeout]) { connect_timeout(socket) } if opts[:timeout]
|
128
|
+
connect_async(socket, host, port, p)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Connects asynchronously to a TCP Server
|
133
|
+
# @param socket [TCPSocket] socket to use for connection
|
134
|
+
# @param host [String] host domain name or ip address
|
135
|
+
# @param port [Integer] server port number
|
136
|
+
# @param promise [Promise] connection promise
|
137
|
+
# @return [void]
|
138
|
+
def connect_async(socket, host, port, promise)
|
139
|
+
addr = ::Socket.sockaddr_in(port, host)
|
140
|
+
result = socket.connect_nonblock addr, exception: false
|
141
|
+
handle_connect_result(result, socket, host, port, promise)
|
142
|
+
rescue StandardError => e
|
143
|
+
promise.reject(e)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Handles result of asynchronous connection
|
147
|
+
# @param result [Integer, Symbol, nil] result of call to IO#connect_nonblock
|
148
|
+
# @param socket [TCPSocket] socket to use for connection
|
149
|
+
# @param host [String] host domain name or ip address
|
150
|
+
# @param port [Integer] server port number
|
151
|
+
# @param promise [Promise] connection promise
|
152
|
+
# @return [void]
|
153
|
+
def handle_connect_result(result, socket, host, port, promise)
|
154
|
+
case result
|
155
|
+
when :wait_writable
|
156
|
+
connect_async_pending(socket, host, port, promise)
|
157
|
+
when 0
|
158
|
+
@connection_pending = false
|
159
|
+
connect_success(socket, promise)
|
160
|
+
else
|
161
|
+
handle_invalid_connect_result(result, socket)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Handles result of asynchronous connection
|
166
|
+
# @param result [Integer, Symbol, nil] result of call to IO#connect_nonblock
|
167
|
+
# @param socket [TCPSocket] socket to use for connection
|
168
|
+
def handle_invalid_connect_result(result, socket)
|
169
|
+
invalid_connect_result(result, socket)
|
170
|
+
@connection_pending = false
|
171
|
+
Core.unwatch(socket)
|
172
|
+
@monitor = nil
|
173
|
+
raise "Invalid result from connect_nonblock: #{result.inspect}"
|
174
|
+
end
|
175
|
+
|
176
|
+
# Sets connection pending state
|
177
|
+
# @param socket [TCPSocket] socket to use for connection
|
178
|
+
# @param host [String] host domain name or ip address
|
179
|
+
# @param port [Integer] server port number
|
180
|
+
# @param promise [Promise] connection promise
|
181
|
+
# @return [void]
|
182
|
+
def connect_async_pending(socket, host, port, promise)
|
183
|
+
create_watcher(socket, true, true)
|
184
|
+
@connection_pending = [socket, host, port, promise]
|
185
|
+
end
|
186
|
+
|
187
|
+
# Overrides IO#write_to_io to support async connection
|
188
|
+
# @return [void]
|
189
|
+
def write_to_io
|
190
|
+
@connection_pending ? connect_async(*@connection_pending) : super
|
191
|
+
end
|
192
|
+
|
193
|
+
# Sets socket and connected status on successful connection
|
194
|
+
# @param socket [TCPSocket] TCP socket
|
195
|
+
# @param promise [Promise] connection promise
|
196
|
+
# @return [void]
|
197
|
+
def connect_success(socket, promise)
|
198
|
+
@io = socket
|
199
|
+
@connected = true
|
200
|
+
promise.resolve(socket)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Called upon connection timeout, cleans up
|
204
|
+
# @param socket [TCPSocket] TCP socket
|
205
|
+
# @return [void]
|
206
|
+
def connect_timeout(socket)
|
207
|
+
@connection_pending = false
|
208
|
+
Core.unwatch(socket)
|
209
|
+
@monitor = nil
|
210
|
+
socket.close
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# ALPN protocol
|
215
|
+
module ALPN
|
216
|
+
# returns the ALPN protocol used for the given socket
|
217
|
+
# @return [String, nil]
|
218
|
+
def alpn_protocol
|
219
|
+
secure? && raw_io.alpn_protocol
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Encapsulates a TCP socket
|
224
|
+
class Socket < IO
|
225
|
+
include ClientConnection
|
226
|
+
include ALPN
|
227
|
+
|
228
|
+
# Initializes socket
|
229
|
+
def initialize(socket = nil, opts = {})
|
230
|
+
super(socket, opts)
|
231
|
+
@connected = opts[:connected]
|
232
|
+
end
|
233
|
+
|
234
|
+
# Returns true if socket is connected
|
235
|
+
# @return [Boolean]
|
236
|
+
def connected?
|
237
|
+
@connected
|
238
|
+
end
|
239
|
+
|
240
|
+
# Returns false
|
241
|
+
# @return [false]
|
242
|
+
def secure?
|
243
|
+
false
|
244
|
+
end
|
245
|
+
|
246
|
+
# Sets socket option
|
247
|
+
# @return [void]
|
248
|
+
def setsockopt(*args)
|
249
|
+
@io.setsockopt(*args)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Socket with TLS handshake functionality
|
254
|
+
class SecureSocket < Socket
|
255
|
+
# Initializes secure socket
|
256
|
+
def initialize(socket = nil, opts = {})
|
257
|
+
super
|
258
|
+
accept_secure_handshake
|
259
|
+
end
|
260
|
+
|
261
|
+
# Returns true
|
262
|
+
# @return [true]
|
263
|
+
def secure?
|
264
|
+
true
|
265
|
+
end
|
266
|
+
|
267
|
+
# accepts secure handshake asynchronously
|
268
|
+
# @return [void]
|
269
|
+
def accept_secure_handshake
|
270
|
+
@pending_secure_handshake = true
|
271
|
+
result = @io.accept_nonblock(exception: false)
|
272
|
+
handle_accept_secure_handshake_result(result)
|
273
|
+
rescue StandardError => e
|
274
|
+
close_on_error(e)
|
275
|
+
end
|
276
|
+
|
277
|
+
# Handles result of secure handshake
|
278
|
+
# @param result [Integer, any] result of call to accept_nonblock
|
279
|
+
# @return [void]
|
280
|
+
def handle_accept_secure_handshake_result(result)
|
281
|
+
case result
|
282
|
+
when :wait_readable
|
283
|
+
@watcher_r.start
|
284
|
+
when :wait_writable
|
285
|
+
@watcher_w.start
|
286
|
+
else
|
287
|
+
@pending_secure_handshake = false
|
288
|
+
@watcher_r.start
|
289
|
+
@watcher_w.stop
|
290
|
+
@callbacks[:handshake]&.(self)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# Overrides read_from_io to accept secure handshake
|
295
|
+
# @return [void]
|
296
|
+
def read_from_io
|
297
|
+
@pending_secure_handshake ? accept_secure_handshake : super
|
298
|
+
end
|
299
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :ResourcePool
|
4
|
+
|
5
|
+
# Implements a limited resource pool
|
6
|
+
class ResourcePool
|
7
|
+
# Initializes a new resource pool
|
8
|
+
# @param opts [Hash] options
|
9
|
+
# @param &block [Proc] allocator block
|
10
|
+
def initialize(opts, &block)
|
11
|
+
@allocator = block
|
12
|
+
|
13
|
+
@available = []
|
14
|
+
@waiting = []
|
15
|
+
|
16
|
+
@limit = opts[:limit] || 4
|
17
|
+
@count = 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def acquire
|
21
|
+
resource = wait
|
22
|
+
yield resource
|
23
|
+
ensure
|
24
|
+
@available << resource if resource
|
25
|
+
dequeue
|
26
|
+
end
|
27
|
+
|
28
|
+
def wait
|
29
|
+
fiber = Fiber.current
|
30
|
+
@waiting << fiber
|
31
|
+
dequeue
|
32
|
+
suspend
|
33
|
+
ensure
|
34
|
+
@waiting.delete(fiber)
|
35
|
+
end
|
36
|
+
|
37
|
+
def dequeue
|
38
|
+
return unless (resource = from_stock)
|
39
|
+
EV.next_tick { @waiting[0]&.transfer(resource) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def from_stock
|
43
|
+
@available.shift || (@count < @limit && allocate)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Allocates a resource
|
47
|
+
# @return [any] allocated resource
|
48
|
+
def allocate
|
49
|
+
@count += 1
|
50
|
+
@allocator.()
|
51
|
+
end
|
52
|
+
|
53
|
+
def preheat!
|
54
|
+
(@limit - @count).times { @available << from_stock }
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :ServerTask
|
4
|
+
|
5
|
+
Channel = import('./core/channel')
|
6
|
+
Task = import('./core/task')
|
7
|
+
|
8
|
+
class ServerTask
|
9
|
+
def initialize
|
10
|
+
super {
|
11
|
+
loop {
|
12
|
+
message = await @mailbox.receive
|
13
|
+
handle(message)
|
14
|
+
}
|
15
|
+
}
|
16
|
+
@mailbox = Channel.new
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Core = import('./core')
|
4
|
+
|
5
|
+
# Fiber used for running reactor loop
|
6
|
+
ReactorLoopFiber = Fiber.new do
|
7
|
+
Polyphony.run_reactor
|
8
|
+
end
|
9
|
+
|
10
|
+
# Monkey-patch core module with async/await methods
|
11
|
+
module Core
|
12
|
+
# Processes a promise by running reactor loop until promise is completed
|
13
|
+
# @param promise [Promise] promise
|
14
|
+
# @param more [Array<Promise>] more promises
|
15
|
+
# @return [any] resolved value
|
16
|
+
def self.await(promise = {}, *more)
|
17
|
+
return await_all(promise, *more) unless more.empty?
|
18
|
+
|
19
|
+
# raise FiberError, AWAIT_ERROR_MSG unless Fiber.current.async?
|
20
|
+
if promise.completed?
|
21
|
+
return_value = promise.clear_result
|
22
|
+
else
|
23
|
+
ReactorLoopFiber.transfer until promise.completed?
|
24
|
+
return_value = promise.result
|
25
|
+
end
|
26
|
+
return_value.is_a?(Exception) ? raise(return_value) : return_value
|
27
|
+
end
|
28
|
+
|
29
|
+
# Runs given block
|
30
|
+
# @return [void]
|
31
|
+
def self.async(&block)
|
32
|
+
block.()
|
33
|
+
end
|
34
|
+
end
|