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