├── .gitignore ├── .rspec ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── ruby_wolf ├── lib ├── ruby_wolf.rb └── ruby_wolf │ ├── cli.rb │ ├── configuration.rb │ ├── connection.rb │ ├── handler.rb │ ├── logger.rb │ ├── server.rb │ ├── version.rb │ └── worker.rb ├── ruby_wolf.gemspec └── spec ├── ruby_wolf ├── cli_spec.rb └── configuration_spec.rb ├── ruby_wolf_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | tags 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.2.2 5 | before_install: gem install bundler -v 1.13.6 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at nguyenquangminh0711@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rack', '< 3.0' 4 | gem 'byebug' 5 | gem 'rspec' 6 | 7 | gemspec 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Nguyễn Quang Minh 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RubyWolf 2 | 3 | [![Gem Version](https://badge.fury.io/rb/ruby_wolf.svg)](https://badge.fury.io/rb/ruby_wolf) 4 | [![CircleCI](https://circleci.com/gh/nguyenquangminh0711/ruby_wolf.svg?style=svg)](https://circleci.com/gh/nguyenquangminh0711/ruby_wolf) 5 | 6 | Ruby wolf is a tiny ruby web server for rack-based application. This server follows pre-forked and event driven approach. Honestly, this web server is written for study and research purpose. I'm sure it could be used anywhere. So, don't use it in real world :) 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile: 11 | 12 | ```ruby 13 | gem 'ruby_wolf' 14 | ``` 15 | 16 | And then execute: 17 | 18 | $ bundle 19 | 20 | Or install it yourself as: 21 | 22 | $ gem install ruby_wolf 23 | 24 | ## Usage 25 | 26 | Start your rack-based application with the following command: 27 | 28 | `ruby_wolf -p 3000` 29 | 30 | To explore the provided options, please use `ruby_wolf --help` 31 | 32 | ## Benchmark 33 | 34 | Benchmark with some Hello world application, tested with Apache Benchmark, 10000 requests, 12 concurrences under local environment (Macbook Pro 2015 - Core i7, 16gb Ram) 35 | 36 | ### RubyWolf 37 | 38 | ``` 39 | 50% 7ms 40 | 66% 11ms 41 | 75% 14ms 42 | 80% 15ms 43 | 90% 19ms 44 | 95% 24ms 45 | 98% 31ms 46 | 99% 33ms 47 | 100% 51ms (longest request) 48 | ``` 49 | 50 | ### Puma 51 | 52 | ``` 53 | 50% 25ms 54 | 66% 27ms 55 | 75% 28ms 56 | 80% 29ms 57 | 90% 31ms 58 | 95% 32ms 59 | 98% 35ms 60 | 99% 37ms 61 | 100% 39ms (longest request) 62 | ``` 63 | 64 | ### Thin 65 | 66 | ``` 67 | 50% 22ms 68 | 66% 23ms 69 | 75% 24ms 70 | 80% 24ms 71 | 90% 28ms 72 | 95% 30ms 73 | 98% 34ms 74 | 99% 36ms 75 | 100% 227ms (longest request) 76 | ``` 77 | 78 | Note that Hello world application is not considered to be a real application. Thus this benchmark doesn't mean much 79 | 80 | ## License 81 | 82 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 83 | 84 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/ruby_wolf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Copyright (c) 2017 Nguyễn Quang Minh 4 | # 5 | 6 | require 'ruby_wolf' 7 | 8 | cli = RubyWolf::CLI.new ARGV 9 | 10 | cli.run 11 | -------------------------------------------------------------------------------- /lib/ruby_wolf.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | 3 | require 'ruby_wolf/version' 4 | require 'ruby_wolf/logger' 5 | require 'ruby_wolf/configuration' 6 | require 'ruby_wolf/connection' 7 | require 'ruby_wolf/handler' 8 | require 'ruby_wolf/server' 9 | require 'ruby_wolf/worker' 10 | require 'ruby_wolf/cli' 11 | 12 | module RubyWolf 13 | MAIN_PID = Process.pid 14 | CRLF = "\r\n".freeze 15 | HEADER_ENDING = "\r\n\r\n".freeze 16 | READ_SIZE = 16 * 1024 17 | 18 | def self.logger 19 | @logger ||= RubyWolf::Logger.new(STDOUT) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/ruby_wolf/cli.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | 3 | module RubyWolf 4 | class CLI 5 | attr_reader :app, :server, :configs 6 | 7 | def initialize(args) 8 | @args = args 9 | @configs = RubyWolf::Configuration.new 10 | @app_root = `pwd`.to_s.strip 11 | end 12 | 13 | def run 14 | parse_options 15 | set_environment 16 | 17 | raise 'Rack file not found' unless File.exist?(rack_file) 18 | 19 | @server = RubyWolf::Server.new(rack_file, configs) 20 | @server.start 21 | end 22 | 23 | def parse_options 24 | opt_parser = OptionParser.new do |opts| 25 | opts.banner = 'Usage: ruby_wolf [options]' 26 | 27 | opts.on('-d', '--daemon', 'Demonize this web server to run background') do 28 | @configs[:daemon] = true 29 | end 30 | 31 | opts.on('-h HOST', '--port=HOST', 'Binding host') do |arg| 32 | @configs[:host] = arg 33 | end 34 | 35 | opts.on('-p PORT', '--port=PORT', 'Port of the program') do |arg| 36 | @configs[:port] = arg.to_i 37 | end 38 | 39 | opts.on('-w WORKER', '--worker=WORKER', 'Number of worker processes') do |arg| 40 | @configs[:worker] = arg.to_i 41 | end 42 | 43 | opts.on('-e ENVIRONMENT', '--environment=ENVIRONMENT', 'Current environment') do |arg| 44 | @configs[:environment] = arg 45 | end 46 | 47 | opts.on('-h', '--help', 'Show the usages') do 48 | puts opts 49 | exit 50 | end 51 | end 52 | 53 | opt_parser.parse!(@args) 54 | end 55 | 56 | private 57 | 58 | def rack_file 59 | "#{@app_root}/config.ru" 60 | end 61 | 62 | def set_environment 63 | ENV['RAILS_ENV'] = configs[:environment] 64 | ENV['RACK_ENV'] = configs[:environment] 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/ruby_wolf/configuration.rb: -------------------------------------------------------------------------------- 1 | module RubyWolf 2 | class Configuration < BasicObject 3 | DEFAULT_DAEMON = false 4 | DEFAULT_HOST = '0.0.0.0'.freeze 5 | DEFAULT_PORT = 3000 6 | DEFAULT_WORKER = 4 7 | DEFAULT_ENVIRONMENT = 'development'.freeze 8 | 9 | def initialize 10 | @configs = { 11 | daemon: DEFAULT_DAEMON, 12 | worker: DEFAULT_WORKER, 13 | host: DEFAULT_HOST, 14 | port: DEFAULT_PORT, 15 | environment: DEFAULT_ENVIRONMENT 16 | } 17 | end 18 | 19 | def []=(key, value) 20 | @configs[key] = value 21 | end 22 | 23 | def [](key) 24 | @configs[key] 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/ruby_wolf/connection.rb: -------------------------------------------------------------------------------- 1 | module RubyWolf 2 | class Connection 3 | attr_reader :socket, :read_chunk, :write_chunk, :headers, :body, :path, :query, :method 4 | 5 | def initialize(socket) 6 | @socket = socket 7 | @read_chunk = '' 8 | @write_chunk = '' 9 | @reading = true 10 | 11 | @headers = {} 12 | end 13 | 14 | def need_to_read? 15 | @reading 16 | end 17 | 18 | def read 19 | @read_chunk << socket.read_nonblock(RubyWolf::READ_SIZE) 20 | if @content_length.nil? 21 | read_headers 22 | else 23 | read_body 24 | end 25 | rescue EOFError 26 | @reading = false 27 | end 28 | 29 | def enqueue_write(data) 30 | @write_chunk += data 31 | end 32 | 33 | def write 34 | writen = socket.write_nonblock(@write_chunk) 35 | @write_chunk = @write_chunk.byteslice(writen, @write_chunk.bytesize) 36 | end 37 | 38 | def need_to_write? 39 | !@write_chunk.bytesize.zero? 40 | end 41 | 42 | def to_io 43 | @socket 44 | end 45 | 46 | def close 47 | @socket.close 48 | end 49 | 50 | private 51 | 52 | def read_headers 53 | header_ending = @read_chunk.index(RubyWolf::HEADER_ENDING) 54 | return if header_ending.nil? 55 | 56 | headers_chunk = @read_chunk.slice!( 57 | 0, header_ending + RubyWolf::HEADER_ENDING.size 58 | ) 59 | parse_headers(headers_chunk) 60 | @content_length = @headers['Content-Length'].to_i 61 | read_body 62 | end 63 | 64 | def parse_headers(headers_chunk) 65 | parser = Http::Parser.new 66 | parser.on_headers_complete = proc do 67 | @headers = parser.headers 68 | @method = parser.http_method 69 | uri = URI.parse(parser.request_url) 70 | @path = uri.path 71 | @query = uri.query 72 | :stop 73 | end 74 | parser << headers_chunk 75 | end 76 | 77 | def read_body 78 | @reading = false if @read_chunk.bytesize >= @content_length 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/ruby_wolf/handler.rb: -------------------------------------------------------------------------------- 1 | require 'http/parser' 2 | 3 | module RubyWolf 4 | class Handler 5 | attr_reader :app, :env, :connection, :response 6 | 7 | def initialize(app, connection, &callback) 8 | @app = app 9 | @env = {} 10 | @connection = connection 11 | @response = '' 12 | 13 | @callback = callback 14 | end 15 | 16 | def process 17 | prepare_rack_env 18 | log_request 19 | process_request 20 | compose_response 21 | log_response 22 | 23 | @callback.call(response) if @callback 24 | @response 25 | end 26 | 27 | private 28 | 29 | def prepare_rack_env 30 | @connection.headers.each do |key, value| 31 | @env[rack_key(key)] = value 32 | end 33 | 34 | @env = @env.merge( 35 | 'rack.version' => ::Rack::VERSION, 36 | 'rack.errors' => STDERR, 37 | 'rack.multithread' => false, 38 | 'rack.multiprocess' => true, 39 | 'rack.runonce' => true, 40 | 'rack.url_scheme' => ENV['HTTPS'] ? 'https' : 'http', 41 | 42 | 'REQUEST_METHOD' => @connection.method, 43 | 'REQUEST_PATH' => @connection.path, 44 | 'PATH_INFO' => @connection.path, 45 | 'QUERY_STRING' => @connection.query, 46 | 47 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 48 | 'SERVER_NAME' => 'Ruby Wolf', 49 | 'HTTP_VERSION' => 'HTTP/1.1', 50 | 51 | 'rack.input' => StringIO.new(@connection.read_chunk) 52 | ) 53 | end 54 | 55 | def process_request 56 | @status, @headers, @body = @app.call(env) 57 | rescue => e 58 | message = "Error while processing the request: #{e.message}\n#{e.backtrace.join("\n")}" 59 | @status = 500 60 | @body = [message] 61 | @headers = [ 62 | ['Content-Length', message.bytesize], 63 | ['Content-Type', 'text/plain; charset=utf-8'] 64 | ] 65 | RubyWolf.logger.error(message) 66 | end 67 | 68 | def compose_response 69 | @response += "HTTP/1.1 #{@status} #{RubyWolf::CRLF}" 70 | @headers.each do |key, value| 71 | @response += "#{key}: #{value}#{RubyWolf::CRLF}" 72 | end 73 | 74 | @response += RubyWolf::CRLF 75 | @body.each do |part| 76 | @response += part 77 | end 78 | ensure 79 | @body.close if @body.respond_to? :close 80 | end 81 | 82 | private 83 | 84 | def log_request 85 | RubyWolf.logger.info("HTTP/1.1 #{@connection.method} #{request_path}") 86 | end 87 | 88 | def log_response 89 | RubyWolf.logger.info("Response HTTP/1.1 #{@status}") 90 | end 91 | 92 | def request_path 93 | if @connection.query 94 | "#{@connection.path}?#{@connection.query}" 95 | else 96 | @connection.path 97 | end 98 | end 99 | 100 | def rack_key(key) 101 | "HTTP_#{key.upcase.gsub(/[^0-9A-Z]/, '_')}" 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/ruby_wolf/logger.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module RubyWolf 4 | class Logger < ::Logger 5 | def initialize(*args) 6 | super(*args) 7 | @formatter = proc do |severity, datetime, _progname, msg| 8 | date_format = datetime.strftime('%Y-%m-%d %H:%M:%S') 9 | "[#{severity.to_s[0]}] [#{date_format}] #{msg}\n" 10 | end 11 | end 12 | 13 | def info(contents = '') 14 | pre_process(contents) do |content| 15 | super(content) 16 | end 17 | end 18 | 19 | def warn(contents = '') 20 | pre_process(contents) do |content| 21 | super(content) 22 | end 23 | end 24 | 25 | def debug(contents = '') 26 | pre_process(contents) do |content| 27 | super(content) 28 | end 29 | end 30 | 31 | def error(contents = '') 32 | pre_process(contents) do |content| 33 | super(content) 34 | end 35 | end 36 | 37 | def fatal(contents = '') 38 | pre_process(contents) do |content| 39 | super(content) 40 | end 41 | end 42 | 43 | private 44 | 45 | def pre_process(contents) 46 | object = Process.pid == MAIN_PID ? '[Main]' : "[Worker #{Process.pid}]" 47 | contents.to_s.split("\n").each do |line| 48 | yield("#{object} #{line}") 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/ruby_wolf/server.rb: -------------------------------------------------------------------------------- 1 | module RubyWolf 2 | class Server 3 | attr_reader :app, :configs, :socket, :workers 4 | 5 | def initialize(rack_file, configs) 6 | @rack_file = rack_file 7 | @configs = configs 8 | @workers = [] 9 | end 10 | 11 | def start 12 | trap_signal 13 | setup_rack 14 | setup_socket 15 | configs[:worker].times do 16 | workers << RubyWolf::Worker.new(self) 17 | end 18 | Process.detach if configs[:daemon] 19 | workers.each(&:start) 20 | handle_loop 21 | end 22 | 23 | private 24 | 25 | def setup_rack 26 | RubyWolf.logger.info('~~~ Ruby Wolf ~~~') 27 | RubyWolf.logger.info('Loading Rack application') 28 | @app, _rack_options = ::Rack::Builder.parse_file(@rack_file) 29 | Rails.logger = RubyWolf.logger if defined?(Rails) 30 | ActiveRecord::Base.logger = RubyWolf.logger if defined?(ActiveRecord) 31 | end 32 | 33 | def setup_socket 34 | @socket = TCPServer.new(configs[:host], configs[:port]) 35 | RubyWolf.logger.info("Server is running on #{configs[:host]}:#{configs[:port]}") 36 | RubyWolf.logger.info("Process pid is #{Process.pid}") 37 | RubyWolf.logger.info("Number of worker: #{configs[:worker]}") 38 | end 39 | 40 | def handle_loop 41 | while stopped_pid = Process.wait do 42 | stopped_worker = workers.find { |w| w.pid == stopped_pid } 43 | next unless stopped_worker 44 | 45 | RubyWolf.logger.info("Worker with pid #{stopped_pid} suddenly stopped", :error) 46 | 47 | sleep(1) 48 | worker = RubyWolf::Worker.new(self) 49 | worker.start 50 | 51 | workers << worker 52 | end 53 | end 54 | 55 | def trap_signal 56 | Signal.trap(:INT) do 57 | if RubyWolf::MAIN_PID == Process.pid 58 | puts "Stopping server\n" 59 | else 60 | puts "Stopping worker #{Process.pid} \n" 61 | end 62 | exit 63 | end 64 | 65 | Signal.trap(:TERM) do 66 | if RubyWolf::MAIN_PID == Process.pid 67 | puts "Stopping server\n" 68 | workers.each do |w| 69 | Process.kill(:TERM, w.pid) 70 | end 71 | else 72 | puts "Stopping worker #{Process.pid} \n" 73 | end 74 | sleep 1 75 | exit 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/ruby_wolf/version.rb: -------------------------------------------------------------------------------- 1 | module RubyWolf 2 | VERSION = "0.4.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/ruby_wolf/worker.rb: -------------------------------------------------------------------------------- 1 | module RubyWolf 2 | class Worker 3 | attr_reader :pid, :server, :app, :socket, :connections 4 | 5 | def initialize(server) 6 | @server = server 7 | @app = server.app 8 | @socket = server.socket 9 | @connections = [] 10 | end 11 | 12 | def start 13 | @pid = fork do 14 | RubyWolf.logger.info('Worker is ready') 15 | handle_loop 16 | end 17 | end 18 | 19 | private 20 | 21 | def handle_loop 22 | loop do 23 | need_to_read = connections.select(&:need_to_read?) 24 | need_to_write = connections.select(&:need_to_write?) 25 | 26 | ready_to_read, ready_to_write, = IO.select( 27 | need_to_read + [socket], 28 | need_to_write 29 | ) 30 | 31 | handle_read(ready_to_read) 32 | handle_write(ready_to_write) 33 | end 34 | end 35 | 36 | def handle_read(ready_to_read) 37 | ready_to_read.each do |connection| 38 | if connection == socket 39 | accept_connection 40 | else 41 | connection.read 42 | handle_request(connection) unless connection.need_to_read? 43 | end 44 | end 45 | end 46 | 47 | def handle_write(ready_to_write) 48 | ready_to_write.each do |connection| 49 | connection.write 50 | close_connection(connection) unless connection.need_to_write? 51 | end 52 | end 53 | 54 | def handle_request(connection) 55 | handler = RubyWolf::Handler.new(app, connection) do |response| 56 | connection.enqueue_write(response) 57 | end 58 | handler.process 59 | end 60 | 61 | def accept_connection 62 | @connections << RubyWolf::Connection.new(socket.accept_nonblock) 63 | rescue IO::WaitReadable, Errno::EINTR 64 | end 65 | 66 | def close_connection(connection) 67 | connection.close 68 | @connections.delete(connection) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /ruby_wolf.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'ruby_wolf/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'ruby_wolf' 8 | spec.version = RubyWolf::VERSION 9 | spec.authors = ['Nguyễn Quang Minh'] 10 | spec.email = ['nguyenquangminh0711@gmail.com'] 11 | 12 | spec.summary = 'A simple ruby web server using pre-fork and event loop' 13 | spec.description = 'My simple implementation of Rack web server using pre-fork and event loop' 14 | spec.homepage = 'https://github.com/nguyenquangminh0711/ruby_wolf' 15 | 16 | spec.license = 'MIT' 17 | 18 | spec.files = `git ls-files`.split($/) 19 | spec.bindir = 'bin' 20 | spec.executables = ['ruby_wolf'] 21 | spec.require_paths = ['lib'] 22 | 23 | spec.add_runtime_dependency 'http_parser.rb', '~>0.6', '>=0.6.0' 24 | end 25 | -------------------------------------------------------------------------------- /spec/ruby_wolf/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RubyWolf::CLI do 4 | let(:cli) { RubyWolf::CLI.new(args) } 5 | 6 | before { cli.parse_options } 7 | 8 | describe '#run' do 9 | let(:args) { ['-d'] } 10 | let(:app_double) { double } 11 | 12 | context 'Rack file found' do 13 | before do 14 | allow(File).to receive(:exist?).and_return(true) 15 | allow(::Rack::Builder).to receive(:parse_file).and_return(app_double) 16 | allow_any_instance_of(RubyWolf::Server).to receive(:start) 17 | end 18 | 19 | it 'creates Rack app from the rack file' do 20 | cli.run 21 | expect(cli.app).to eql(app_double) 22 | end 23 | 24 | it 'starts ruby wolf server' do 25 | expect_any_instance_of(RubyWolf::Server).to receive(:start) 26 | cli.run 27 | expect(cli.server).to be_a(RubyWolf::Server) 28 | expect(cli.server.app).to eql(app_double) 29 | expect(cli.server.configs).to eq(cli.configs) 30 | end 31 | end 32 | 33 | context 'Rack file not found' do 34 | before do 35 | allow(File).to receive(:exist?).and_return(false) 36 | end 37 | 38 | it 'raises exception' do 39 | expect do 40 | cli.run 41 | end.to raise_error(/rack file not found/i) 42 | end 43 | end 44 | end 45 | 46 | describe '#parse_options' do 47 | describe 'daemon option' do 48 | context 'default daemon' do 49 | let(:args) { [] } 50 | it { expect(cli.configs[:daemon]).to eql(false) } 51 | end 52 | 53 | context 'short form' do 54 | let(:args) { ['-d'] } 55 | it { expect(cli.configs[:daemon]).to eql(true) } 56 | end 57 | 58 | context 'full form' do 59 | let(:args) { ['--daemon'] } 60 | it { expect(cli.configs[:daemon]).to eql(true) } 61 | end 62 | end 63 | 64 | describe 'host option' do 65 | context 'default host' do 66 | let(:args) { [] } 67 | it { expect(cli.configs[:host]).to eql('0.0.0.0') } 68 | end 69 | 70 | context 'short form' do 71 | let(:args) { ['-h localhost'] } 72 | it { expect(cli.configs[:host]).to eql('localhost') } 73 | end 74 | 75 | context 'full form' do 76 | let(:args) { ['--host=localhost'] } 77 | it { expect(cli.configs[:host]).to eql('localhost') } 78 | end 79 | end 80 | 81 | describe 'port option' do 82 | context 'default port' do 83 | let(:args) { [] } 84 | it { expect(cli.configs[:port]).to eql(3000) } 85 | end 86 | 87 | context 'short form' do 88 | let(:args) { ['-p 5000'] } 89 | it { expect(cli.configs[:port]).to eql(5000) } 90 | end 91 | 92 | context 'full form' do 93 | let(:args) { ['--port=5000'] } 94 | it { expect(cli.configs[:port]).to eql(5000) } 95 | end 96 | end 97 | 98 | describe 'worker option' do 99 | context 'default worker' do 100 | let(:args) { [] } 101 | it { expect(cli.configs[:worker]).to eql(4) } 102 | end 103 | 104 | context 'short form' do 105 | let(:args) { ['-w 20'] } 106 | it { expect(cli.configs[:worker]).to eql(20) } 107 | end 108 | 109 | context 'full form' do 110 | let(:args) { ['--worker=20'] } 111 | it { expect(cli.configs[:port]).to eql(20) } 112 | end 113 | end 114 | 115 | describe 'environment option' do 116 | context 'default environment' do 117 | let(:args) { [] } 118 | it { expect(cli.configs[:environment]).to eql('development') } 119 | end 120 | 121 | context 'short form' do 122 | let(:args) { ['-w production'] } 123 | it { expect(cli.configs[:environment]).to eql('production') } 124 | end 125 | 126 | context 'full form' do 127 | let(:args) { ['--environment=20'] } 128 | it { expect(cli.configs[:environment]).to eql('production') } 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /spec/ruby_wolf/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RubyWolf::Configuration do 4 | let(:configs) { RubyWolf::Configuration.new } 5 | 6 | describe '[]=' do 7 | context 'key not exists' do 8 | before do 9 | configs[:hihi] = 'Test' 10 | end 11 | 12 | it 'store the value under the key' do 13 | expect(configs[:hihi]).to eql('Test') 14 | end 15 | end 16 | 17 | context 'key exists' do 18 | before do 19 | configs[:hihi] = 'Test' 20 | configs[:hihi] = 'or not to test' 21 | end 22 | 23 | it 'updates the value under the key' do 24 | expect(configs[:hihi]).to eql('or not to test') 25 | end 26 | end 27 | end 28 | 29 | describe '[]' do 30 | context 'key exists' do 31 | before do 32 | configs[:hihi] = 'What' 33 | end 34 | 35 | it 'returns the value under the key' do 36 | expect(configs[:hihi]).to eql('What') 37 | end 38 | end 39 | 40 | context 'key not exists' do 41 | it 'returns nil' do 42 | expect(configs[:not_exist]).to eql(nil) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/ruby_wolf_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe RubyWolf do 4 | it "has a version number" do 5 | expect(RubyWolf::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "ruby_wolf" 3 | --------------------------------------------------------------------------------