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.
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