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,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ export :Client
4
+
5
+ require 'pg'
6
+
7
+ Core = import('../core')
8
+
9
+ module ::PG
10
+ def self.connect(*args)
11
+ Connection.connect_start(*args).tap(&method(:connect_async))
12
+ end
13
+
14
+ def self.connect_async(conn)
15
+ loop do
16
+ res = conn.connect_poll
17
+ case res
18
+ when PGRES_POLLING_FAILED then raise Error.new(conn.error_message)
19
+ when PGRES_POLLING_READING then conn.socket_io.read_watcher.await
20
+ when PGRES_POLLING_WRITING then conn.socket_io.write_watcher.await
21
+ when PGRES_POLLING_OK then
22
+ conn.setnonblocking(true)
23
+ return
24
+ end
25
+ end
26
+ ensure
27
+ conn.socket_io.stop_watchers
28
+ end
29
+
30
+ def self.connect_sync(conn)
31
+ loop do
32
+ res = conn.connect_poll
33
+ case res
34
+ when PGRES_POLLING_FAILED then raise Error.new(conn.error_message)
35
+ when PGRES_POLLING_OK then
36
+ conn.setnonblocking(true)
37
+ return
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ class ::PG::Connection
44
+ alias_method :orig_get_result, :get_result
45
+
46
+ def get_result(&block)
47
+ while is_busy
48
+ socket_io.read_watcher.await
49
+ consume_input
50
+ end
51
+ orig_get_result(&block)
52
+ ensure
53
+ socket_io.stop_watchers
54
+ end
55
+
56
+ alias_method :orig_async_exec, :async_exec
57
+ def async_exec(*args, &block)
58
+ send_query(*args)
59
+ result = get_result(&block)
60
+ while get_result; end
61
+ result
62
+ end
63
+
64
+ def block(timeout = 0)
65
+ while is_busy
66
+ socket_io.read_watcher.await
67
+ consume_input
68
+ end
69
+ end
70
+
71
+ SQL_BEGIN = 'begin'
72
+ SQL_COMMIT = 'commit'
73
+ SQL_ROLLBACK = 'rollback'
74
+
75
+ # Starts a transaction, runs given block, and commits transaction. If an
76
+ # error is raised, the transaction is rolled back and the error is raised
77
+ # again.
78
+ # @return [void]
79
+ def transaction
80
+ began = false
81
+ return yield if @transaction # allow nesting of calls to #transactions
82
+
83
+ query(SQL_BEGIN)
84
+ began = true
85
+ @transaction = true
86
+ yield
87
+ query(SQL_COMMIT)
88
+ rescue StandardError => e
89
+ (query(SQL_ROLLBACK) rescue nil) if began
90
+ raise e
91
+ ensure
92
+ @transaction = false if began
93
+ end
94
+
95
+ self.async_api = true
96
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ export :Connection
4
+
5
+ require "redis"
6
+ require "hiredis/reader"
7
+
8
+ Net = import('../net')
9
+
10
+ class Driver
11
+ def self.connect(config)
12
+ if config[:scheme] == "unix"
13
+ raise "unix sockets not supported"
14
+ # connection.connect_unix(config[:path], connect_timeout)
15
+ elsif config[:scheme] == "rediss" || config[:ssl]
16
+ raise "ssl not supported"
17
+ # raise NotImplementedError, "SSL not supported by hiredis driver"
18
+ else
19
+ new(config[:host], config[:port])
20
+ # connection.connect(config[:host], config[:port], connect_timeout)
21
+ end
22
+ end
23
+
24
+ def initialize(host, port)
25
+ @connection = Net.tcp_connect(host, port)
26
+ @reader = ::Hiredis::Reader.new
27
+ end
28
+
29
+ def connected?
30
+ @connection && !@connection.closed?
31
+ end
32
+
33
+ def timeout=(timeout)
34
+ # ignore timeout for now
35
+ end
36
+
37
+ def disconnect
38
+ @connection.close
39
+ @connection = nil
40
+ end
41
+
42
+ def write(command)
43
+ @connection.write(format_command(command))
44
+ end
45
+
46
+ def format_command(args)
47
+ (+"*#{args.size}\r\n").tap do |s|
48
+ args.each do |a|
49
+ a = a.to_s
50
+ s << "$#{a.bytesize}\r\n#{a}\r\n"
51
+ end
52
+ end
53
+ end
54
+
55
+ def read
56
+ reply = @reader.gets
57
+ return reply if reply
58
+
59
+ loop do
60
+ data = @connection.read
61
+ @reader.feed(data)
62
+ reply = @reader.gets
63
+ return reply if reply
64
+ end
65
+ end
66
+ end
67
+
68
+ Redis::Connection.drivers << Driver
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+
5
+ import('./io')
6
+
7
+ class ::Socket
8
+ def accept
9
+ loop do
10
+ result, client_addr = accept_nonblock(::IO::NO_EXCEPTION)
11
+ case result
12
+ when Socket then return result
13
+ when :wait_readable then read_watcher.await
14
+ else
15
+ raise "failed to accept (#{result.inspect})"
16
+ end
17
+ end
18
+ ensure
19
+ @read_watcher&.stop
20
+ end
21
+
22
+ def connect(remotesockaddr)
23
+ loop do
24
+ result = connect_nonblock(remotesockaddr, ::IO::NO_EXCEPTION)
25
+ case result
26
+ when 0 then return
27
+ when :wait_writable then write_watcher.await
28
+ else raise IOError
29
+ end
30
+ end
31
+ ensure
32
+ @write_watcher&.stop
33
+ end
34
+
35
+ def recvfrom(maxlen, flags = 0)
36
+ @read_buffer ||= +''
37
+ loop do
38
+ result = recvfrom_nonblock(maxlen, flags, @read_buffer, ::IO::NO_EXCEPTION)
39
+ case result
40
+ when nil then raise IOError
41
+ when :wait_readable then read_watcher.await
42
+ else return result
43
+ end
44
+ end
45
+ ensure
46
+ @read_watcher&.stop
47
+ end
48
+
49
+ ZERO_LINGER = [0, 0].pack("ii")
50
+
51
+ def dont_linger
52
+ setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, ZERO_LINGER)
53
+ end
54
+
55
+ def no_delay
56
+ setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
57
+ end
58
+
59
+ def reuse_addr
60
+ setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
61
+ end
62
+
63
+ class << self
64
+ alias_method :orig_getaddrinfo, :getaddrinfo
65
+ def getaddrinfo(*args)
66
+ Polyphony::ThreadPool.process { orig_getaddrinfo(*args) }
67
+ end
68
+ end
69
+ end
70
+
71
+ class ::TCPServer
72
+ def accept
73
+ loop do
74
+ result, client_addr = accept_nonblock(::IO::NO_EXCEPTION)
75
+ case result
76
+ when TCPSocket then return result
77
+ when :wait_readable then read_watcher.await
78
+ else
79
+ raise "failed to accept (#{result.inspect})"
80
+ end
81
+ end
82
+ ensure
83
+ @read_watcher&.stop
84
+ end
85
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ import('./socket')
6
+
7
+ class ::OpenSSL::SSL::SSLSocket
8
+ def accept
9
+ loop do
10
+ result = accept_nonblock(::IO::NO_EXCEPTION)
11
+ case result
12
+ when :wait_readable then io.read_watcher.await
13
+ when :wait_writable then io.write_watcher.await
14
+ else return true
15
+ end
16
+ end
17
+ ensure
18
+ io.stop_watchers
19
+ end
20
+
21
+ def connect
22
+ loop do
23
+ result = connect_nonblock(::IO::NO_EXCEPTION)
24
+ case result
25
+ when :wait_readable then io.read_watcher.await
26
+ when :wait_writable then io.write_watcher.await
27
+ else return true
28
+ end
29
+ end
30
+ ensure
31
+ io.stop_watchers
32
+ end
33
+
34
+ def read(max = 8192)
35
+ @read_buffer ||= +''
36
+ loop do
37
+ result = read_nonblock(max, @read_buffer, ::IO::NO_EXCEPTION)
38
+ case result
39
+ when nil then raise ::IOError
40
+ when :wait_readable then io.read_watcher.await
41
+ else return result
42
+ end
43
+ end
44
+ ensure
45
+ io.stop_watchers
46
+ end
47
+
48
+ def write(data)
49
+ loop do
50
+ result = write_nonblock(data, ::IO::NO_EXCEPTION)
51
+ case result
52
+ when nil then raise ::IOError
53
+ when :wait_writable then io.write_watcher.await
54
+ else
55
+ (result == data.bytesize) ? (return result) : (data = data[result..-1])
56
+ end
57
+ end
58
+ ensure
59
+ io.stop_watchers
60
+ end
61
+
62
+ def dont_linger
63
+ io.dont_linger
64
+ end
65
+
66
+ def no_delay
67
+ io.no_delay
68
+ end
69
+
70
+ def reuse_addr
71
+ io.reuse_addr
72
+ end
73
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ export :stat,
4
+ :read
5
+
6
+ require 'fileutils'
7
+
8
+ ThreadPool = import('./core/thread_pool')
9
+
10
+ ::File.singleton_class.instance_eval do
11
+ alias_method :orig_stat, :stat
12
+ def stat(path)
13
+ ThreadPool.process { orig_stat(path) }
14
+ end
15
+ end
16
+
17
+ ::IO.singleton_class.instance_eval do
18
+ alias_method :orig_read, :read
19
+ def read(path)
20
+ ThreadPool.process { orig_read(path) }
21
+ end
22
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ export_default :Agent
4
+
5
+ require 'uri'
6
+ require 'http/parser'
7
+ require 'http/2'
8
+ require 'json'
9
+
10
+ ResourcePool = import('../resource_pool')
11
+
12
+ module ResponseMixin
13
+ def body
14
+ self[:body]
15
+ end
16
+
17
+ def json
18
+ @json ||= ::JSON.parse(self[:body])
19
+ end
20
+ end
21
+
22
+ # Implements an HTTP agent
23
+ class Agent
24
+ def self.get(url, query = nil)
25
+ default.get(url, query)
26
+ end
27
+
28
+ def self.post(url, query = nil)
29
+ default.post(url, query)
30
+ end
31
+
32
+ def self.default
33
+ @default ||= new
34
+ end
35
+
36
+ def initialize(max_conns = 6)
37
+ @pools = Hash.new do |h, k|
38
+ h[k] = ResourcePool.new(limit: max_conns) { {} }
39
+ end
40
+ end
41
+
42
+ def get(url, query = nil)
43
+ request(url, method: :GET, query: query)
44
+ end
45
+
46
+ def post(url, query = nil)
47
+ request(url, method: :POST, query: query)
48
+ end
49
+
50
+ S_LOCATION = 'Location'
51
+
52
+ OPTS_DEFAULT = {}.freeze
53
+
54
+ def request(url, opts = OPTS_DEFAULT)
55
+ ctx = request_ctx(url, opts)
56
+ response = do_request(ctx)
57
+
58
+ case response[:status_code]
59
+ when 301, 302
60
+ request(response[:headers][S_LOCATION])
61
+ when 200, 204
62
+ response.extend(ResponseMixin)
63
+ else
64
+ raise "Error received from server: #{response[:status_code]}"
65
+ end
66
+ end
67
+
68
+ def request_ctx(url, opts)
69
+ {
70
+ method: opts[:method] || :GET,
71
+ uri: url_to_uri(url, opts),
72
+ opts: opts
73
+ }
74
+ end
75
+
76
+ def url_to_uri(url, opts)
77
+ uri = URI(url)
78
+ if opts[:query]
79
+ query = opts[:query].map { |k, v| "#{k}=#{v}" }.join("&")
80
+ if uri.query
81
+ v.query = "#{uri.query}&#{query}"
82
+ else
83
+ uri.query = query
84
+ end
85
+ end
86
+ uri
87
+ end
88
+
89
+ def do_request(ctx)
90
+ key = uri_key(ctx[:uri])
91
+ @pools[key].acquire do |state|
92
+ state[:socket] ||= connect(key)
93
+ state[:protocol_method] ||= protocol_method(state[:socket], ctx)
94
+ send(state[:protocol_method], state, ctx)
95
+ rescue => e
96
+ state[:socket]&.close rescue nil
97
+ state.clear
98
+ raise e
99
+ end
100
+ end
101
+
102
+ S_H2 = 'h2'
103
+
104
+ def protocol_method(socket, ctx)
105
+ if socket.is_a?(::OpenSSL::SSL::SSLSocket) && (socket.alpn_protocol == S_H2)
106
+ :do_http2
107
+ else
108
+ :do_http1
109
+ end
110
+ end
111
+
112
+ def do_http1(state, ctx)
113
+ done = false
114
+ body = +''
115
+ parser = HTTP::Parser.new
116
+ parser.on_message_complete = proc { done = true }
117
+ parser.on_body = proc { |data| body << data }
118
+ request = format_http1_request(ctx)
119
+
120
+ state[:socket] << request
121
+ while !done
122
+ parser << state[:socket].read
123
+ end
124
+
125
+ {
126
+ protocol: 'http1.1',
127
+ status_code: parser.status_code,
128
+ headers: parser.headers,
129
+ body: body
130
+ }
131
+ end
132
+
133
+ def do_http2(state, ctx)
134
+ unless state[:http2_client]
135
+ socket, client = state[:socket], HTTP2::Client.new
136
+ client.on(:frame) {|bytes| socket << bytes }
137
+ state[:http2_client] = client
138
+ end
139
+
140
+ stream = state[:http2_client].new_stream # allocate new stream
141
+
142
+ headers = {
143
+ ':scheme' => ctx[:uri].scheme,
144
+ ':method' => ctx[:method].to_s,
145
+ ':path' => ctx[:uri].request_uri,
146
+ ':authority' => [ctx[:uri].host, ctx[:uri].port].join(':'),
147
+ }
148
+ headers.merge!(ctx[:opts][:headers]) if ctx[:opts][:headers]
149
+
150
+ if ctx[:opts][:payload]
151
+ stream.headers(headers, end_stream: false)
152
+ stream.data(ctx[:opts][:payload], end_stream: true)
153
+ else
154
+ stream.headers(headers, end_stream: true)
155
+ end
156
+
157
+ headers = nil
158
+ body = +''
159
+ done = nil
160
+
161
+ stream.on(:headers) { |h| headers = h.to_h }
162
+ stream.on(:data) { |c| body << c }
163
+ stream.on(:close) {
164
+ done = true
165
+ return {
166
+ protocol: 'http1.1',
167
+ status_code: headers && headers[':status'].to_i,
168
+ headers: headers || {},
169
+ body: body
170
+ }
171
+ }
172
+
173
+ while data = state[:socket].read
174
+ state[:http2_client] << data
175
+ end
176
+ ensure
177
+ (stream.close rescue nil) unless done
178
+ end
179
+
180
+ HTTP1_REQUEST = "%<method>s %<request>s HTTP/1.1\r\nHost: %<host>s\r\n\r\n"
181
+
182
+ def format_http1_request(ctx)
183
+ HTTP1_REQUEST % {
184
+ method: ctx[:method],
185
+ request: ctx[:uri].request_uri,
186
+ host: ctx[:uri].host
187
+ }
188
+ end
189
+
190
+ def uri_key(uri)
191
+ {
192
+ scheme: uri.scheme,
193
+ host: uri.host,
194
+ port: uri.port
195
+ }
196
+ end
197
+
198
+ S_HTTP = 'http'
199
+ S_HTTPS = 'https'
200
+ SECURE_OPTS = { secure: true, alpn_protocols: ['h2', 'http/1.1'] }
201
+
202
+ def connect(key)
203
+ case key[:scheme]
204
+ when S_HTTP
205
+ Polyphony::Net.tcp_connect(key[:host], key[:port])
206
+ when S_HTTPS
207
+ Polyphony::Net.tcp_connect(key[:host], key[:port], SECURE_OPTS).tap do |socket|
208
+ socket.post_connection_check(key[:host])
209
+ end
210
+ else
211
+ raise "Invalid scheme #{key[:scheme].inspect}"
212
+ end
213
+ end
214
+ end