polyphony 0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +86 -0
  3. data/README.md +400 -0
  4. data/ext/ev/extconf.rb +19 -0
  5. data/lib/polyphony.rb +26 -0
  6. data/lib/polyphony/core.rb +45 -0
  7. data/lib/polyphony/core/async.rb +36 -0
  8. data/lib/polyphony/core/cancel_scope.rb +61 -0
  9. data/lib/polyphony/core/channel.rb +39 -0
  10. data/lib/polyphony/core/coroutine.rb +106 -0
  11. data/lib/polyphony/core/exceptions.rb +24 -0
  12. data/lib/polyphony/core/fiber_pool.rb +98 -0
  13. data/lib/polyphony/core/supervisor.rb +75 -0
  14. data/lib/polyphony/core/sync.rb +20 -0
  15. data/lib/polyphony/core/thread.rb +49 -0
  16. data/lib/polyphony/core/thread_pool.rb +58 -0
  17. data/lib/polyphony/core/throttler.rb +38 -0
  18. data/lib/polyphony/extensions/io.rb +62 -0
  19. data/lib/polyphony/extensions/kernel.rb +161 -0
  20. data/lib/polyphony/extensions/postgres.rb +96 -0
  21. data/lib/polyphony/extensions/redis.rb +68 -0
  22. data/lib/polyphony/extensions/socket.rb +85 -0
  23. data/lib/polyphony/extensions/ssl.rb +73 -0
  24. data/lib/polyphony/fs.rb +22 -0
  25. data/lib/polyphony/http/agent.rb +214 -0
  26. data/lib/polyphony/http/http1.rb +124 -0
  27. data/lib/polyphony/http/http1_request.rb +71 -0
  28. data/lib/polyphony/http/http2.rb +66 -0
  29. data/lib/polyphony/http/http2_request.rb +69 -0
  30. data/lib/polyphony/http/rack.rb +27 -0
  31. data/lib/polyphony/http/server.rb +43 -0
  32. data/lib/polyphony/line_reader.rb +82 -0
  33. data/lib/polyphony/net.rb +59 -0
  34. data/lib/polyphony/net_old.rb +299 -0
  35. data/lib/polyphony/resource_pool.rb +56 -0
  36. data/lib/polyphony/server_task.rb +18 -0
  37. data/lib/polyphony/testing.rb +34 -0
  38. data/lib/polyphony/version.rb +5 -0
  39. 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