right_speed 0.1.0

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.
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: []