right_speed 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a21c10b045dfb5591600c16d61a7fa9d09771db4960e0d3c3021b90c8fbdc48c
4
+ data.tar.gz: a71b06cde0b6b1028aabdab601673d827040824db08d6fce181c9c9816669d74
5
+ SHA512:
6
+ metadata.gz: 490fb8ff447db76d7cdc16980d547de1b078d9648afe9534804a638abb708d63f572b84a64489707fedad4dfd29caf046b96456b390064ada334a7b14953b556
7
+ data.tar.gz: 52e3d420c923a0593f463aeff793911000bce92b8855723f02fa7ffc4e3da3744779d264e29f5ee0b4e46f5a63e5570a39c33f4c79d8c1cd3c2f4e718c6315bf
@@ -0,0 +1,35 @@
1
+ name: Ruby
2
+
3
+ on: [push,pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v2
10
+ - name: Set up Ruby
11
+ uses: ruby/setup-ruby@v1
12
+ with:
13
+ ruby-version: 3.0.2
14
+ - name: Run the default task
15
+ run: |
16
+ gem install bundler
17
+ bundle install
18
+ bundle exec rake
19
+ - name: Run the actual server
20
+ run: |
21
+ bundle exec ruby bin/right_speed -c snippets/test.ru &
22
+ sleep 5
23
+ output=$(curl -s http://127.0.0.1:8080/)
24
+ kill %1
25
+ echo "Output: $output"
26
+ test "$output" = "Yaaay"
27
+ - name: Run rackup
28
+ # Using production not to use middlewares for development (lint, etc)
29
+ run: |
30
+ bundle exec rackup snippets/test.ru -s right_speed -E production -O Host=127.0.0.1 -O Port=8081 &
31
+ sleep 5
32
+ output=$(curl -s http://127.0.0.1:8081/)
33
+ kill %1
34
+ echo "Output: $output"
35
+ test "$output" = "Yaaay"
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in right_speed.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "test-unit", "~> 3.0"
11
+
12
+ # The updated libraries for ractor-safety
13
+ gem "http_parser.rb", git: 'https://github.com/tmm1/http_parser.rb.git'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Satoshi Moris Tagomori
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # RightSpeed
2
+
3
+ RightSpeed is **an experimental application server** to host Rack applications, on Ractor workers, to test/verify that your application is Ractor-safe/Ractor-ready or not.
4
+ Ractor is an experimental feature of Ruby 3.0, thus **this application server is also not for production environments**.
5
+
6
+ Currently, RightSpeed supports the very limited set of Rack protocol specifications. Unsupported features are, for example:
7
+
8
+ * Writing logs into files
9
+ * Daemonizing processes
10
+ * Reloading applications without downtime
11
+ * Handling session objects (using `rack.session`)
12
+ * Handling multipart contents flexisbly (using `rack.multipart.buffer_size` nor `rack.multipart.tempfile_factory`)
13
+ * [Hijacking](https://github.com/rack/rack/blob/master/SPEC.rdoc#label-Hijacking)
14
+
15
+ ## Changelog
16
+
17
+ * v0.1.0:
18
+ * The first release just before RubyKaigi Takeout 2021
19
+
20
+ ## Usage
21
+
22
+ Use the latest Ruby 3.x release!
23
+
24
+ Install `right_speed` by `gem` command (`gem i right_speed`), then use it directly:
25
+
26
+ ```
27
+ $ right_speed -c config.ru -p 8080 --workers 8
28
+
29
+ # See right_speed --help for full options:
30
+ $ right_speed --help
31
+ Usage: right_speed [options]
32
+
33
+ OPTIONS
34
+ --config, -c PATH The path of the rackup configuration file (default: config.ru)
35
+ --port, -p PORT The port number to listen (default: 8080)
36
+ --backlog NUM The number of backlog
37
+ --workers NUM The number of Ractors (default: CPU cores)
38
+ --worker-type TYPE The type of workers, available options are read/accept (default: read)
39
+ --help Show this message
40
+ ```
41
+
42
+ Or, use `rackup` with `-s right_speed`:
43
+
44
+ ```
45
+ $ rackup config.ru -s right_speed -p 8080 -O Workers=8
46
+ ```
47
+
48
+ The default number of worker Ractors is the number of CPU cores.
49
+
50
+ ## Contributing
51
+
52
+ Bug reports and pull requests are welcome on GitHub at https://github.com/tagomoris/right_speed.
53
+
54
+ ## License
55
+
56
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
data/bin/right_speed ADDED
@@ -0,0 +1,106 @@
1
+ #!ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/right_speed/env"
5
+ require_relative "../lib/right_speed/server"
6
+ require "getoptlong"
7
+
8
+ module RightSpeed
9
+ module Command
10
+ Options = Struct.new(
11
+ :rackup, :port, :backlog,
12
+ :workers, :worker_type,
13
+ keyword_init: true,
14
+ )
15
+
16
+ COMMAND_OPTIONS = [
17
+ ['--config', '-c', GetoptLong::REQUIRED_ARGUMENT],
18
+ ['--port', '-p', GetoptLong::REQUIRED_ARGUMENT],
19
+ ['--backlog', GetoptLong::REQUIRED_ARGUMENT],
20
+ ['--workers', GetoptLong::REQUIRED_ARGUMENT],
21
+ ['--worker-type', GetoptLong::REQUIRED_ARGUMENT],
22
+ ['--help', GetoptLong::NO_ARGUMENT],
23
+ ]
24
+
25
+ DEFAULT_RACKUP_PATH = 'config.ru'
26
+ DEFAULT_PORT = Server::DEFAULT_PORT
27
+ DEFAULT_WORKERS = Env.processors
28
+ DEFAULT_WORKER_TYPE = Server::DEFAULT_WORKER_TYPE
29
+ DEFAULT_LISTENER_TYPE = Server::DEFAULT_LISTENER_TYPE
30
+
31
+ AVAILABLE_WORKER_TYPES = Server::AVAILABLE_WORKER_TYPES.map(:to_s).join('/')
32
+ AVAILABLE_LISTENER_TYPES = Server::AVAILABLE_LISTENER_TYPES.map(:to_s).join('/')
33
+
34
+ def self.show_help(error: false, error_message: nil)
35
+ STDERR.puts(error_message, "\n") if error_message
36
+ STDERR.puts <<~EOS
37
+ Usage: right_speed [options]
38
+
39
+ OPTIONS
40
+ --config, -c PATH The path of the rackup configuration file (default: #{DEFAULT_RACKUP_PATH})
41
+ --port, -p PORT The port number to listen (default: #{DEFAULT_PORT})
42
+ --backlog NUM The number of backlog
43
+ --workers NUM The number of Ractors (default: CPU cores, #{DEFAULT_WORKERS})
44
+ --worker-type TYPE The type of workers (available: #{AVAILABLE_WORKER_TYPES}, default: #{DEFAULT_WORKER_TYPE})
45
+ --help Show this message
46
+ EOS
47
+ exit(error ? 1 : 0)
48
+ end
49
+
50
+ def self.integer_value(value, name)
51
+ Integer(value)
52
+ rescue
53
+ show_help(error: true, error_message: "#{name} should be an Integer: #{value}")
54
+ end
55
+
56
+ def self.parse_command_line_options
57
+ optparse = GetoptLong.new
58
+ optparse.set_options(*COMMAND_OPTIONS)
59
+ options = Options.new(
60
+ rackup: DEFAULT_RACKUP_PATH,
61
+ port: DEFAULT_PORT,
62
+ backlog: nil,
63
+ workers: DEFAULT_WORKERS,
64
+ worker_type: DEFAULT_WORKER_TYPE,
65
+ )
66
+ worker_type = :read
67
+ optparse.each_option do |name, value|
68
+ case name
69
+ when '--config'
70
+ options.rackup = value
71
+ when '--port'
72
+ options.port = integer_value(value, "Port number")
73
+ when '--backlog'
74
+ options.backlog = integer_value(value, "Backlog")
75
+ when '--workers'
76
+ options.workers = integer_value(value, "Workers")
77
+ when '--worker-type'
78
+ options.worker_type = value.to_sym
79
+ when '--help'
80
+ show_help
81
+ else
82
+ show_help(error: true, error_messsage: "Unknown option: #{name}")
83
+ end
84
+ end
85
+ options
86
+ end
87
+
88
+ def self.start
89
+ options = parse_command_line_options
90
+ server = begin
91
+ RightSpeed::Server.new(
92
+ port: options.port,
93
+ app: options.rackup,
94
+ backlog: options.backlog,
95
+ workers: options.workers,
96
+ worker_type: options.worker_type,
97
+ )
98
+ rescue => e
99
+ show_help(error: true, error_message: "Failed to launch the server, " + e.message)
100
+ end
101
+ server.run
102
+ end
103
+ end
104
+ end
105
+
106
+ RightSpeed::Command.start
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "right_speed/server"
4
+
5
+ module Rack
6
+ module Handler
7
+ class RightSpeed
8
+ def self.run(app, **options)
9
+ environment = ENV['RACK_ENV'] || 'development'
10
+ default_host = environment == 'development' ? '127.0.0.1' : '0.0.0.0'
11
+
12
+ host = options.delete(:Host) || default_host
13
+ port = options.delete(:Port) || 8080
14
+ workers = options.delete(:Workers) || ::RightSpeed::Env.processors
15
+ server = ::RightSpeed::Server.new(app: app, host: host, port: port, workers: workers)
16
+
17
+ yield server if block_given?
18
+
19
+ server.run
20
+ end
21
+
22
+ def self.valid_options
23
+ environment = ENV['RACK_ENV'] || 'development'
24
+ default_host = environment == 'development' ? '127.0.0.1' : '0.0.0.0'
25
+ {
26
+ "Host=HOST" => "Hostname to listen on (default: #{default_host})",
27
+ "Port=PORT" => "Port to listen on (default: 8080)",
28
+ "Workers=NUM" => "Number of workers (default: #{::RightSpeed::Env.processors})",
29
+ }
30
+ end
31
+ end
32
+
33
+ register :right_speed, ::Rack::Handler::RightSpeed
34
+ end
35
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "logger"
4
+
5
+ module RightSpeed
6
+ class ConnectionCloser
7
+ def run(workers)
8
+ @ractor = Ractor.new(workers) do |workers|
9
+ logger = RightSpeed.logger
10
+ while workers.size > 0
11
+ r, conn = Ractor.select(*workers, move: true)
12
+ if conn == :closing
13
+ workers.delete(r)
14
+ next
15
+ end
16
+ begin
17
+ conn.close
18
+ rescue => e
19
+ logger.debug { "Error while closing a connection #{conn}, #{e.class}:#{e.message}" }
20
+ end
21
+ end
22
+ rescue => e
23
+ logger.error { "Unexpected error, #{e.class}:#{e.message}" }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "version"
4
+
5
+ module RightSpeed
6
+ SOFTWARE_NAME = "RightSpeed #{VERSION} (#{RUBY_ENGINE} #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} [#{RUBY_PLATFORM}])".freeze
7
+ RACK_VERSION = Rack::VERSION.freeze
8
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ require "concurrent"
3
+
4
+ module RightSpeed
5
+ module Env
6
+ def self.processors
7
+ Concurrent.processor_count
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+ require "logger"
3
+ require "stringio"
4
+ require "http/parser"
5
+ require "rack"
6
+
7
+ require "pp"
8
+
9
+ require_relative "./const"
10
+
11
+ module RightSpeed
12
+ class Handler
13
+ def initialize(app)
14
+ @app = app
15
+ end
16
+
17
+ def session(conn)
18
+ Session.new(self, conn)
19
+ end
20
+
21
+ def process(session, client, request)
22
+ # https://github.com/rack/rack/blob/master/SPEC.rdoc
23
+ env = {
24
+ # TODO: replace the keys using constants: https://github.com/rack/rack/blob/master/lib/rack.rb
25
+ 'HTTP_VERSION' => request.http_version,
26
+ 'PATH_INFO' => request.path_info,
27
+ 'QUERY_STRING' => request.query_string || "",
28
+ 'REMOTE_ADDR' => client.addr,
29
+ 'REQUEST_METHOD' => request.http_method,
30
+ 'REQUEST_PATH' => request.path_info,
31
+ 'REQUEST_URI' => request.request_uri,
32
+ 'SCRIPT_NAME' => "",
33
+ 'SERVER_NAME' => client.server_addr,
34
+ 'SERVER_PORT' => client.server_port.to_s,
35
+ 'SERVER_PROTOCOL' => request.http_version,
36
+ 'SERVER_SOFTWARE' => RightSpeed::SOFTWARE_NAME,
37
+ **request.headers_in_env_style,
38
+ ### Rack specific keys
39
+ 'rack.version' => RightSpeed::RACK_VERSION,
40
+ 'rack.url_scheme' => 'http', # http or https, depending on the request URL.
41
+ 'rack.input' => request.body, # The input stream.
42
+ 'rack.errors' => $stderr, # The error stream.
43
+ 'rack.multithread' => true,
44
+ 'rack.multiprocess' => false,
45
+ 'rack.run_once' => false,
46
+ 'rack.hijack?' => false, # https://github.com/rack/rack/blob/master/SPEC.rdoc#label-Hijacking
47
+ ### Optional Rack keys
48
+ ## 'rack.session'
49
+ # A hash like interface for storing request session data.
50
+ # The store must implement:
51
+ # store(key, value) (aliased as []=); fetch(key, default = nil) (aliased as []);
52
+ # delete(key); clear; to_hash (returning unfrozen Hash instance);
53
+ 'rack.logger' => session.logger,
54
+ # A common object interface for logging messages.
55
+ # The object must implement:
56
+ # info(message, &block),debug(message, &block),warn(message, &block),error(message, &block),fatal(message, &block)
57
+ ## 'rack.multipart.buffer_size'
58
+ # An Integer hint to the multipart parser as to what chunk size to use for reads and writes.
59
+ ## 'rack.multipart.tempfile_factory'
60
+ # An object responding to #call with two arguments, the filename and content_type given for the multipart form field,
61
+ # and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate
62
+ # the tempfile for each multipart form file upload field, rather than the default class of Tempfile.
63
+ }
64
+ status, headers, body = @app.call(env)
65
+ Response.new(http_version: request.http_version, status_code: status, headers: headers, body: body)
66
+ end
67
+
68
+ class Client
69
+ attr_reader :addr, :port, :server_addr, :server_port
70
+
71
+ def initialize(conn)
72
+ _, @port, _, @addr = conn.peeraddr
73
+ _, @server_port, _, @server_addr = conn.addr
74
+ if @server_addr == "::1"
75
+ @server_addr = "localhost"
76
+ end
77
+ end
78
+ end
79
+
80
+ class Request
81
+ attr_reader :http_method, :http_version, :request_url, :headers, :body, :path_info, :query_string
82
+
83
+ def initialize(client:, http_method:, http_version:, request_url:, headers:, body:)
84
+ @client = client
85
+ @http_method = http_method
86
+ @http_version = "HTTP/" + http_version.map(&:to_s).join(".")
87
+ @request_url = request_url
88
+ @headers = headers
89
+ @body = StringIO.new(body)
90
+
91
+ @path_info, @query_string = request_url.split('?')
92
+ end
93
+
94
+ def request_uri
95
+ "http://#{@client.server_addr}:#{@client.server_port}#{request_url}"
96
+ end
97
+
98
+ def headers_in_env_style
99
+ headers = {}
100
+ @headers.each do |key, value|
101
+ headers["HTTP_" + key.gsub("-", "_").upcase] = value
102
+ end
103
+ headers
104
+ end
105
+ end
106
+
107
+ class Response
108
+ STATUS_MESSAGE_MAP = {
109
+ 200 => "OK",
110
+ }.freeze
111
+
112
+ attr_reader :body
113
+
114
+ def initialize(http_version:, status_code:, headers:, body:)
115
+ @http_version = http_version
116
+ @status_code = status_code
117
+ @status_message = STATUS_MESSAGE_MAP.fetch(status_code, "Unknown")
118
+ @headers = headers
119
+ @body = body
120
+ end
121
+
122
+ def status
123
+ "#{@http_version} #{@status_code} #{@status_message}\r\n"
124
+ end
125
+
126
+ def headers
127
+ @headers.map{|key, value| "#{key}: #{value}\r\n" }.join + "\r\n"
128
+ end
129
+ end
130
+
131
+ class Session
132
+ READ_CHUNK_LENGTH = 1024
133
+
134
+ attr_reader :logger
135
+
136
+ def initialize(handler, conn)
137
+ @logger = RightSpeed.logger
138
+ @handler = handler
139
+ @conn = conn
140
+ @client = Client.new(conn)
141
+
142
+ # https://github.com/tmm1/http_parser.rb
143
+ @parser = Http::Parser.new(self, default_header_value_type: :mixed)
144
+ @reading = true
145
+ @method = nil
146
+ @url = nil
147
+ @headers = nil
148
+ @body = String.new
149
+ end
150
+
151
+ # TODO: implement handling of "Connection" and "Keep-Alive"
152
+ # https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Connection
153
+ # https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Keep-Alive
154
+
155
+ def process
156
+ while @reading && !@conn.eof?
157
+ @parser << @conn.readpartial(READ_CHUNK_LENGTH)
158
+ end
159
+ end
160
+
161
+ def on_headers_complete(headers)
162
+ @headers = headers
163
+ @method = @parser.http_method
164
+ @url = @parser.request_url
165
+ end
166
+
167
+ def on_body(chunk)
168
+ @body << chunk
169
+ end
170
+
171
+ def on_message_complete
172
+ # @logger.debug {
173
+ # "complete to read the request, headers:#{@headers}, body:#{@body}"
174
+ # }
175
+ request = Request.new(
176
+ client: @client, http_method: @method, http_version: @parser.http_version,
177
+ request_url: @url, headers: @headers, body: @body
178
+ )
179
+ response = @handler.process(self, @client, request)
180
+ send_response(response)
181
+ @reading = false
182
+ end
183
+
184
+ def send_response(response)
185
+ @conn.write response.status
186
+ @conn.write response.headers
187
+ response.body.each do |part|
188
+ @conn.write part
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "logger"
4
+
5
+ module RightSpeed
6
+ module Listener
7
+ def self.setup(listener_type:, host:, port:, backlog: nil)
8
+ case listener_type
9
+ when :roundrobin
10
+ RoundRobinListener.new(host, port, backlog)
11
+ else
12
+ SimpleListener.new(host, port, backlog)
13
+ end
14
+ end
15
+
16
+ class SimpleListener
17
+ attr_reader :sock
18
+
19
+ def initialize(host, port, backlog)
20
+ @host = host
21
+ @port = port
22
+ @backlog = backlog
23
+ @sock = nil
24
+ end
25
+
26
+ def run(_processor)
27
+ @running = true
28
+ @sock = TCPServer.open(@host, @port)
29
+ @sock.listen(@backlog) if @backlog
30
+ @sock
31
+ end
32
+
33
+ def wait
34
+ # do nothing
35
+ end
36
+
37
+ def stop
38
+ @running = false
39
+ if @sock
40
+ @sock.close rescue nil
41
+ end
42
+ end
43
+ end
44
+
45
+ class RoundRobinListener < SimpleListener
46
+ def run(processor)
47
+ @running = true
48
+ @ractor = Ractor.new(@host, @port, @backlog, processor) do |host, port, backlog, processor|
49
+ logger = RightSpeed.logger
50
+ sock = TCPServer.open(host, port)
51
+ sock.listen(backlog) if backlog
52
+ logger.info { "listening #{host}:#{port}" }
53
+ while conn = sock.accept
54
+ processor.process(conn)
55
+ end
56
+ end
57
+ end
58
+
59
+ def wait
60
+ @ractor.take
61
+ end
62
+
63
+ def stop
64
+ @running = false
65
+ @ractor = nil # TODO: terminate the Ractor if possible
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,9 @@
1
+ module RightSpeed
2
+ def self.logger
3
+ return Ractor.current[:logger] if Ractor.current[:logger]
4
+ logger = Logger.new($stderr)
5
+ logger.formatter = lambda {|severity, datetime, progname, msg| "[#{datetime}] #{severity} #{msg}\n" }
6
+ Ractor.current[:logger] = logger
7
+ logger
8
+ end
9
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/builder'
4
+
5
+ require_relative 'worker/accepter'
6
+ require_relative 'worker/reader'
7
+ require_relative 'connection_closer'
8
+
9
+ module RightSpeed
10
+ module Processor
11
+ def self.setup(app:, worker_type:, workers:)
12
+ app = if app.respond_to?(:call)
13
+ app
14
+ elsif app.is_a?(String) # rackup config path
15
+ build_app(app)
16
+ else
17
+ raise "Unexpected app #{app}"
18
+ end
19
+ handler = Ractor.make_shareable(Handler.new(app))
20
+ case worker_type
21
+ when :read
22
+ ReadProcessor.new(workers, handler)
23
+ when :accept
24
+ AcceptProcessor.new(workers, handler)
25
+ else
26
+ raise "Unknown worker type #{worker_type}"
27
+ end
28
+ end
29
+
30
+ def self.build_app(ru)
31
+ app = Rack::Builder.parse_file(ru)
32
+ if app.respond_to?(:call)
33
+ app
34
+ elsif app.is_a?(Array) && app[0].respond_to?(:call)
35
+ # Rack::Builder returns [app, options] but options will be deprecated
36
+ app[0]
37
+ else
38
+ raise "Failed to build Rack app from #{ru}: #{app}"
39
+ end
40
+ end
41
+
42
+ class Base
43
+ def initialize(workers, handler)
44
+ raise "BUG: use implementation class"
45
+ end
46
+
47
+ def configure(listener:)
48
+ raise "BUG: not implemented"
49
+ end
50
+
51
+ def run
52
+ raise "BUG: not implemented"
53
+ end
54
+
55
+ def process(conn)
56
+ raise "BUG: not implemented"
57
+ end
58
+
59
+ def wait
60
+ raise "BUG: not implemented"
61
+ # ractors.each{|r| r.take}
62
+ # finalizer.close rescue nil
63
+ end
64
+ end
65
+
66
+ class ReadProcessor < Base
67
+ def initialize(workers, handler)
68
+ @worker_num = workers
69
+ @handler = handler
70
+ @workers = workers.times.map{|i| Worker::Reader.new(id: i, handler: @handler)}
71
+ @closer = ConnectionCloser.new
72
+ @counter = 0
73
+ end
74
+
75
+ def configure(listener:)
76
+ @listener = listener
77
+ end
78
+
79
+ def run
80
+ @workers.each{|w| w.run}
81
+ @closer.run(@workers.map{|w| w.ractor})
82
+ @listener.run(self)
83
+ end
84
+
85
+ def process(conn)
86
+ current, @counter = @counter, @counter + 1
87
+ @workers[current % @worker_num].process(conn)
88
+ end
89
+
90
+ def wait
91
+ @workers.each{|w| w.wait}
92
+ end
93
+ end
94
+
95
+ class AcceptProcessor < Base
96
+ def initialize(workers, handler)
97
+ @worker_num = workers
98
+ @handler = handler
99
+ @workers = workers.times.map{|i| Worker::Accepter.new(id: i, handler: @handler) }
100
+ end
101
+
102
+ def configure(listener:)
103
+ @listener = listener
104
+ @workers.each do |w|
105
+ w.configure(listener.sock)
106
+ end
107
+ end
108
+
109
+ def run
110
+ @workers.each do |w|
111
+ w.run
112
+ end
113
+ # TODO: connection closer
114
+ end
115
+
116
+ def wait
117
+ @workers.each{|w| w.wait}
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "rack"
5
+
6
+ module RightSpeed
7
+ module RactorHelper
8
+ def self.uri_hook
9
+ # Use 3.1.0-dev!
10
+ end
11
+
12
+ def self.rack_hook
13
+ ip_filter = Ractor.make_shareable(Rack::Request.ip_filter)
14
+ overwrite_method(Rack::Request::Helpers, :trusted_proxy?) do |ip|
15
+ ip_filter.call(ip)
16
+ end
17
+ overwrite_method(Rack::Request::Helpers, :query_parser, Rack::Utils.default_query_parser)
18
+ overwrite_const(Rack::ShowExceptions, :TEMPLATE, Rack::ShowExceptions::TEMPLATE)
19
+ freeze_all_constants(::Rack)
20
+ end
21
+
22
+ def self.freeze_all_constants(mojule, touch_list=[])
23
+ touch_list << mojule
24
+ mojule.constants.each do |const_name|
25
+ const = begin
26
+ mojule.const_get(const_name)
27
+ rescue LoadError
28
+ # ignore unloadable modules (autoload, probably)
29
+ nil
30
+ end
31
+ next unless const
32
+ if const.is_a?(Module) && !touch_list.include?(const)
33
+ # not freeze Module/Class because we're going to do monkey patching...
34
+ freeze_all_constants(const, touch_list)
35
+ else
36
+ const.freeze
37
+ end
38
+ end
39
+ end
40
+
41
+ def self.overwrite_method(mojule, name, value=nil, &block)
42
+ if block_given?
43
+ mojule.define_method(name, Ractor.make_shareable(block))
44
+ else
45
+ v = Ractor.make_shareable(value)
46
+ mojule.define_method(name, Ractor.make_shareable(->(){ v }))
47
+ end
48
+ end
49
+
50
+ def self.overwrite_const(mojule, name, value)
51
+ v = Ractor.make_shareable(value)
52
+ mojule.const_set(name, value)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "logger"
5
+ require "webrick"
6
+
7
+ require_relative "processor"
8
+ require_relative "listener"
9
+ require_relative "env"
10
+ require_relative "ractor_helper"
11
+
12
+ module RightSpeed
13
+ CONFIG_HOOK_KEY = 'right_speed_config_hooks'
14
+
15
+ class Server
16
+ DEFAULT_HOST = "127.0.0.1"
17
+ DEFAULT_PORT = 8080
18
+ DEFAULT_WORKER_TYPE = :read
19
+ DEFAULT_WORKERS = Env.processors
20
+ DEFAULT_SCHEDULER_TYPE = :roundrobin
21
+
22
+ AVAILABLE_WORKER_TYPES = [:read, :accept]
23
+ AVAILABLE_LISTENER_TYPES = [:roundrobin, :fair]
24
+
25
+ attr_reader :config_hooks
26
+
27
+ def initialize(
28
+ app:,
29
+ host: DEFAULT_HOST,
30
+ port: DEFAULT_PORT,
31
+ workers: DEFAULT_WORKERS,
32
+ worker_type: DEFAULT_WORKER_TYPE,
33
+ scheduler_type: DEFAULT_SCHEDULER_TYPE,
34
+ backlog: nil
35
+ )
36
+ @host = host
37
+ @port = port
38
+ @app = app
39
+ @workers = workers
40
+ @worker_type = worker_type
41
+ @listener_type = case @worker_type
42
+ when :read then scheduler_type
43
+ else :listen
44
+ end
45
+ @backlog = backlog
46
+ @config_hooks = []
47
+ @logger = nil
48
+ end
49
+
50
+ def run
51
+ logger = RightSpeed.logger
52
+ logger.info { "Start running with #{@workers} workers" }
53
+
54
+ hooks = @config_hooks + (Ractor.current[RightSpeed::CONFIG_HOOK_KEY] || [])
55
+ hooks.each do |hook|
56
+ if hook.respond_to?(:call)
57
+ hook.call
58
+ end
59
+ end
60
+
61
+ RactorHelper.uri_hook
62
+ RactorHelper.rack_hook
63
+
64
+ begin
65
+ processor = Processor.setup(app: @app, worker_type: @worker_type, workers: @workers)
66
+ listener = Listener.setup(listener_type: @listener_type, host: @host, port: @port, backlog: nil)
67
+ processor.configure(listener: listener)
68
+ processor.run
69
+ listener.wait
70
+ processor.wait
71
+ ensure
72
+ listener.stop rescue nil
73
+ processor.stop rescue nil
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RightSpeed
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,30 @@
1
+ require_relative 'base'
2
+
3
+ module RightSpeed
4
+ module Worker
5
+ class Accepter < Base
6
+ def configure(sock)
7
+ @sock = sock
8
+ end
9
+
10
+ def run
11
+ @ractor = Ractor.new(@id, @sock) do |id, sock|
12
+ while conn = sock.accept
13
+ begin
14
+ data = conn.read
15
+ # TODO: process it
16
+ logger.info "[read|#{id}] Data: #{data}"
17
+ conn.write "200 OK"
18
+ ensure
19
+ conn.close rescue nil
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def wait
26
+ @ractor.take
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ require_relative "../logger"
2
+ require_relative "../handler"
3
+
4
+ module RightSpeed
5
+ module Worker
6
+ class Base
7
+ def initialize(id:, handler:)
8
+ @id = id
9
+ @handler = handler
10
+ @ractor = nil
11
+ end
12
+
13
+ def ractor
14
+ @ractor
15
+ end
16
+
17
+ def stop
18
+ @ractor # TODO: terminate if possible
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ require_relative "base"
2
+ require_relative "../logger"
3
+
4
+ module RightSpeed
5
+ module Worker
6
+ class Reader < Base
7
+ def run
8
+ @ractor = Ractor.new(@id, @handler) do |id, handler|
9
+ logger = RightSpeed.logger
10
+ while conn = Ractor.receive
11
+ begin
12
+ handler.session(conn).process
13
+ # TODO: keep-alive?
14
+ Ractor.yield(conn, move: true) # to yield closing connections to ConnectionCloser
15
+ rescue => e
16
+ logger.error { "Unexpected error: #{e.message}\n" + e.backtrace.map{"\t#{_1}\n"}.join }
17
+ # TODO: print backtrace in better way
18
+ end
19
+ end
20
+ logger.info { "Worker#{id}: Finishing the Ractor" }
21
+ Ractor.yield(:closing) # to tell the outgoing path will be closed when stopping
22
+ end
23
+ end
24
+
25
+ def process(conn)
26
+ @ractor.send(conn, move: true)
27
+ end
28
+
29
+ def wait
30
+ @ractor.take
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "right_speed/version"
4
+
5
+ module RightSpeed
6
+ end
7
+
8
+ require_relative "right_speed/server"
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/right_speed/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "right_speed"
7
+ spec.version = RightSpeed::VERSION
8
+ spec.authors = ["Satoshi Moris Tagomori"]
9
+ spec.email = ["tagomoris@gmail.com"]
10
+
11
+ spec.summary = "HTTP server implementation using Ractor"
12
+ spec.description = "HTTP server, which provides traffic under the support of Ractor"
13
+ spec.homepage = "https://github.com/tagomoris/right_speed"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.2")
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features|snippets)/}) }
23
+ end
24
+ spec.bindir = "bin"
25
+ spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_runtime_dependency "webrick", "~> 1.7"
29
+ spec.add_runtime_dependency "rack", "~> 2.2"
30
+ spec.add_runtime_dependency "concurrent-ruby", "~> 1.1"
31
+ spec.add_runtime_dependency "http_parser.rb", "~> 0.8"
32
+
33
+ spec.add_development_dependency "test-unit"
34
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: right_speed
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Satoshi Moris Tagomori
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-09-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: webrick
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: concurrent-ruby
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: http_parser.rb
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.8'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: test-unit
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: HTTP server, which provides traffic under the support of Ractor
84
+ email:
85
+ - tagomoris@gmail.com
86
+ executables:
87
+ - right_speed
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".github/workflows/main.yml"
92
+ - ".gitignore"
93
+ - Gemfile
94
+ - LICENSE.txt
95
+ - README.md
96
+ - Rakefile
97
+ - bin/right_speed
98
+ - lib/rack/handler/right_speed.rb
99
+ - lib/right_speed.rb
100
+ - lib/right_speed/connection_closer.rb
101
+ - lib/right_speed/const.rb
102
+ - lib/right_speed/env.rb
103
+ - lib/right_speed/handler.rb
104
+ - lib/right_speed/listener.rb
105
+ - lib/right_speed/logger.rb
106
+ - lib/right_speed/processor.rb
107
+ - lib/right_speed/ractor_helper.rb
108
+ - lib/right_speed/server.rb
109
+ - lib/right_speed/version.rb
110
+ - lib/right_speed/worker/accepter.rb
111
+ - lib/right_speed/worker/base.rb
112
+ - lib/right_speed/worker/reader.rb
113
+ - right_speed.gemspec
114
+ homepage: https://github.com/tagomoris/right_speed
115
+ licenses:
116
+ - MIT
117
+ metadata:
118
+ homepage_uri: https://github.com/tagomoris/right_speed
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: 3.0.2
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubygems_version: 3.3.0.dev
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: HTTP server implementation using Ractor
138
+ test_files: []