spider-gazelle 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 +7 -0
- data/LICENSE +20 -0
- data/README.md +9 -0
- data/Rakefile +7 -0
- data/bin/sg +64 -0
- data/lib/rack/handler/spider-gazelle.rb +47 -0
- data/lib/spider-gazelle/connection.rb +104 -0
- data/lib/spider-gazelle/error.rb +16 -0
- data/lib/spider-gazelle/gazelle.rb +144 -0
- data/lib/spider-gazelle/request.rb +150 -0
- data/lib/spider-gazelle/spider.rb +210 -0
- data/lib/spider-gazelle/upgrades/websocket.rb +37 -0
- data/lib/spider-gazelle/version.rb +3 -0
- data/lib/spider-gazelle.rb +17 -0
- data/spider-gazelle.gemspec +37 -0
- metadata +162 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3081d50c47f8dd30b20deca6f8ff4ab4f6b96b04
|
4
|
+
data.tar.gz: 6d33aab9f1fe72354d919b59d6232918cd452c2b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fe5cc932cb5a8616da336f5806ff8c46ae7fba0743f42e688d9e9c6a3e9371c05cf1a01081e3f077283a9b8c35b4c93263e75fb6875a7039ef030aaf4314704e
|
7
|
+
data.tar.gz: 1a1ef9210f7edd32884f5e7dba6f7257ff8c4c33b2ee18ab83a2f209e48aa5bc13cf0a455b61316cd7d78702cf0f9b2d89cb41f57f5d9247711b390cdc662f79
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 CoTag Media
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
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, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
spider-gazelle
|
2
|
+
==============
|
3
|
+
|
4
|
+
A fast, parallel and concurrent web server for ruby
|
5
|
+
|
6
|
+
Spidergazelle, spidergazelle amazingly agile, she leaps through the veldt,
|
7
|
+
Spidergazelle, spidergazelle! She don’t care what you think, she says what the hell!
|
8
|
+
Look out! Here comes the Spidergazelle!
|
9
|
+
|
data/Rakefile
ADDED
data/bin/sg
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
|
4
|
+
require 'spider-gazelle'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
|
8
|
+
options = {
|
9
|
+
Host: "0.0.0.0",
|
10
|
+
Port: 3000,
|
11
|
+
environment: ENV['RACK_ENV'] || 'development',
|
12
|
+
rackup: "#{Dir.pwd}/config.ru"
|
13
|
+
}
|
14
|
+
|
15
|
+
parser = OptionParser.new do |opts|
|
16
|
+
opts.on "-p", "--port PORT", Integer, "Define what port TCP port to bind to (default: 3000)" do |arg|
|
17
|
+
options[:Port] = arg
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on "-a", "--address HOST", "bind to HOST address (default: 0.0.0.0)" do |arg|
|
21
|
+
options[:Host] = arg
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.on "-q", "--quiet", "Quiet down the output" do
|
25
|
+
options[:Quiet] = true
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on "-e", "--environment ENVIRONMENT", "The environment to run the Rack app on (default: development)" do |arg|
|
29
|
+
options[:environment] = arg
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on "-r", "--rackup FILE", "Load Rack config from this file (default: config.ru)" do |arg|
|
33
|
+
options[:rackup] = arg
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
parser.banner = "sg <options> <rackup file>"
|
38
|
+
parser.on_tail "-h", "--help", "Show help" do
|
39
|
+
puts parser
|
40
|
+
exit 1
|
41
|
+
end
|
42
|
+
|
43
|
+
parser.parse!(ARGV)
|
44
|
+
|
45
|
+
if ARGV.last =~ /\.ru$/
|
46
|
+
options[:rackup] = ARGV.shift
|
47
|
+
end
|
48
|
+
|
49
|
+
unless File.exists?(options[:rackup])
|
50
|
+
abort "No rackup found at #{options[:rackup]}"
|
51
|
+
end
|
52
|
+
|
53
|
+
app, rack_options = Rack::Builder.parse_file options[:rackup]
|
54
|
+
server = ::SpiderGazelle::Spider.new(app, options)
|
55
|
+
|
56
|
+
puts "Look out! Here comes Spider-Gazelle #{::SpiderGazelle::VERSION}!"
|
57
|
+
puts "* Environment: #{ENV['RACK_ENV']}"
|
58
|
+
puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
|
59
|
+
|
60
|
+
begin
|
61
|
+
server.run
|
62
|
+
ensure
|
63
|
+
puts "\nSpider-Gazelle leaps through the veldt"
|
64
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rack/handler'
|
2
|
+
require 'spider-gazelle'
|
3
|
+
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
module Handler
|
7
|
+
module SpiderGazelle
|
8
|
+
DEFAULT_OPTIONS = {
|
9
|
+
:Host => '0.0.0.0',
|
10
|
+
:Port => 8080,
|
11
|
+
:Verbose => false
|
12
|
+
}
|
13
|
+
|
14
|
+
def self.run(app, options = {})
|
15
|
+
options = DEFAULT_OPTIONS.merge(options)
|
16
|
+
|
17
|
+
if options[:Verbose]
|
18
|
+
app = Rack::CommonLogger.new(app, STDOUT)
|
19
|
+
end
|
20
|
+
|
21
|
+
if options[:environment]
|
22
|
+
ENV['RACK_ENV'] = options[:environment].to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
server = ::SpiderGazelle::Spider.new(app, options)
|
26
|
+
|
27
|
+
puts "Spider-Gazelle #{::SpiderGazelle::VERSION} starting..."
|
28
|
+
puts "* Environment: #{ENV['RACK_ENV']}"
|
29
|
+
puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
|
30
|
+
|
31
|
+
yield server if block_given?
|
32
|
+
|
33
|
+
server.run
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.valid_options
|
37
|
+
{
|
38
|
+
"Host=HOST" => "Hostname to listen on (default: 0.0.0.0)",
|
39
|
+
"Port=PORT" => "Port to listen on (default: 8080)",
|
40
|
+
"Quiet" => "Don't report each request"
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
register :sg, SpiderGazelle
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
|
4
|
+
module SpiderGazelle
|
5
|
+
class Connection
|
6
|
+
|
7
|
+
|
8
|
+
SET_INSTANCE_TYPE = proc {|inst| inst.type = :request}
|
9
|
+
|
10
|
+
|
11
|
+
attr_reader :state, :parsing
|
12
|
+
attr_accessor :queue_worker
|
13
|
+
|
14
|
+
|
15
|
+
def initialize(loop, socket, queue) # TODO:: port information
|
16
|
+
# A single parser instance per-connection (supports pipelining)
|
17
|
+
@state = ::HttpParser::Parser.new_instance &SET_INSTANCE_TYPE
|
18
|
+
@pending = []
|
19
|
+
|
20
|
+
# Work callback for thread pool processing
|
21
|
+
@request = nil
|
22
|
+
@work = method(:work)
|
23
|
+
|
24
|
+
# Called after the work on the thread pool is complete
|
25
|
+
@send_response = method(:send_response)
|
26
|
+
@send_error = method(:send_error)
|
27
|
+
|
28
|
+
# Used to chain promises (ensures requests are processed in order)
|
29
|
+
@process_next = method(:process_next)
|
30
|
+
@current_worker = queue # keep track of work queue head to prevent unintentional GC
|
31
|
+
@queue_worker = queue # start queue with an existing resolved promise (::Libuv::Q::ResolvedPromise.new(@loop, true))
|
32
|
+
|
33
|
+
# Socket for writing the response
|
34
|
+
@socket = socket
|
35
|
+
@loop = loop
|
36
|
+
end
|
37
|
+
|
38
|
+
# Creates a new request state object
|
39
|
+
def start_parsing(request)
|
40
|
+
@parsing = request
|
41
|
+
end
|
42
|
+
|
43
|
+
# Chains the work in a promise queue
|
44
|
+
def finished_parsing
|
45
|
+
if !@state.keep_alive?
|
46
|
+
@parsing.keep_alive = false
|
47
|
+
@socket.stop_read # we don't want to do any more work then we need to
|
48
|
+
end
|
49
|
+
@parsing.upgrade = @state.upgrade?
|
50
|
+
@pending.push @parsing
|
51
|
+
@queue_worker = @queue_worker.then @process_next
|
52
|
+
end
|
53
|
+
|
54
|
+
# The parser encountered an error
|
55
|
+
def parsing_error
|
56
|
+
# TODO::log error (available in the @request object)
|
57
|
+
p "parsing error #{@state.error}"
|
58
|
+
|
59
|
+
# We no longer care for any further requests from this client
|
60
|
+
# however we will finish processing any valid pipelined requests before shutting down
|
61
|
+
@socket.stop_read
|
62
|
+
@queue_worker = @queue_worker.then do
|
63
|
+
# TODO:: send response (400 bad request)
|
64
|
+
@socket.shutdown
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
protected
|
70
|
+
|
71
|
+
|
72
|
+
def send_response(result)
|
73
|
+
# As we have come back from another thread the socket may have closed
|
74
|
+
# This check is an optimisation, the call to write and shutdown would fail safely
|
75
|
+
if !@socket.closed
|
76
|
+
@socket.write @request.response
|
77
|
+
if @request.keep_alive == false
|
78
|
+
@socket.shutdown
|
79
|
+
end
|
80
|
+
end
|
81
|
+
# continue processing (don't wait for write to complete)
|
82
|
+
# if the write fails it will close the socket
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def send_error(reason)
|
87
|
+
p "send error: #{reason.message}\n#{reason.backtrace.join("\n")}\n"
|
88
|
+
# log error reason
|
89
|
+
# TODO:: send response (500 internal error)
|
90
|
+
# no need to close the socket as this isn't fatal
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def process_next(result)
|
95
|
+
@request = @pending.shift
|
96
|
+
@current_worker = @loop.work @work
|
97
|
+
@current_worker.then @send_response, @send_error # resolves the promise with a promise
|
98
|
+
end
|
99
|
+
|
100
|
+
def work
|
101
|
+
@request.execute!
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
module SpiderGazelle
|
3
|
+
module Error
|
4
|
+
|
5
|
+
# Indicate that we couldn't parse the request
|
6
|
+
ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n".freeze
|
7
|
+
|
8
|
+
# The standard empty 404 response for bad requests.
|
9
|
+
ERROR_404_RESPONSE = "HTTP/1.1 404 Not Found\r\n\r\nNOT FOUND".freeze
|
10
|
+
|
11
|
+
# Indicate that there was an internal error, obviously.
|
12
|
+
ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze
|
13
|
+
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
|
4
|
+
module SpiderGazelle
|
5
|
+
class Gazelle
|
6
|
+
|
7
|
+
|
8
|
+
HTTP_META = 'HTTP_'.freeze
|
9
|
+
REQUEST_METHOD = 'REQUEST_METHOD'.freeze # GET, POST, etc
|
10
|
+
|
11
|
+
|
12
|
+
def initialize(app, options)
|
13
|
+
@gazelle = Libuv::Loop.new
|
14
|
+
@connections = Set.new # Set of active connections on this thread
|
15
|
+
@parser_cache = [] # Stale parser objects cached for reuse
|
16
|
+
@connection_queue = ::Libuv::Q::ResolvedPromise.new(@gazelle, true)
|
17
|
+
|
18
|
+
@app = app
|
19
|
+
@options = options
|
20
|
+
|
21
|
+
# A single parser instance for processing requests for each gazelle
|
22
|
+
@parser = ::HttpParser::Parser.new(self)
|
23
|
+
|
24
|
+
# Single progress callback for each gazelle
|
25
|
+
@on_progress = method(:on_progress)
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
@gazelle.run do |logger|
|
30
|
+
logger.progress do |level, errorid, error|
|
31
|
+
begin
|
32
|
+
p "Log called: #{level}: #{errorid}\n#{error.message}\n#{error.backtrace.join("\n")}\n"
|
33
|
+
rescue Exception
|
34
|
+
p 'error in gazelle logger'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# A pipe used to forward connections to different threads
|
39
|
+
@socket_server = @gazelle.pipe(true)
|
40
|
+
@socket_server.connect(DELEGATE_PIPE) do
|
41
|
+
@socket_server.progress do |data, socket|
|
42
|
+
new_connection(socket)
|
43
|
+
end
|
44
|
+
@socket_server.start_read2
|
45
|
+
end
|
46
|
+
|
47
|
+
# A pipe used to signal various control commands (shutdown, etc)
|
48
|
+
@signal_server = @gazelle.pipe
|
49
|
+
@signal_server.connect(SIGNAL_PIPE) do
|
50
|
+
@signal_server.progress do |data|
|
51
|
+
process_signal(data)
|
52
|
+
end
|
53
|
+
@signal_server.start_read
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# HTTP Parser callbacks:
|
60
|
+
def on_message_begin(parser)
|
61
|
+
@connection.start_parsing(Request.new(@app, @options))
|
62
|
+
end
|
63
|
+
|
64
|
+
def on_url(parser, url)
|
65
|
+
@connection.parsing.url << url
|
66
|
+
end
|
67
|
+
|
68
|
+
def on_header_field(parser, header)
|
69
|
+
req = @connection.parsing
|
70
|
+
if req.header.frozen?
|
71
|
+
req.header = header
|
72
|
+
else
|
73
|
+
req.header << header
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def on_header_value(parser, value)
|
78
|
+
req = @connection.parsing
|
79
|
+
if req.header.frozen?
|
80
|
+
req.env[req.header] << value
|
81
|
+
else
|
82
|
+
header = req.header
|
83
|
+
header.upcase!
|
84
|
+
header.gsub!('-', '_')
|
85
|
+
header.prepend(HTTP_META)
|
86
|
+
header.freeze
|
87
|
+
req.env[header] = value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def on_headers_complete(parser)
|
92
|
+
@connection.parsing.env[REQUEST_METHOD] = @connection.state.http_method.to_s
|
93
|
+
end
|
94
|
+
|
95
|
+
def on_body(parser, data)
|
96
|
+
@connection.parsing.body << data
|
97
|
+
end
|
98
|
+
|
99
|
+
def on_message_complete(parser)
|
100
|
+
@connection.finished_parsing
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
protected
|
105
|
+
|
106
|
+
|
107
|
+
def on_progress(data, socket)
|
108
|
+
# Keep track of which connection we are processing for the callbacks
|
109
|
+
@connection = socket.storage
|
110
|
+
|
111
|
+
# Check for errors during the parsing of the request
|
112
|
+
if @parser.parse(@connection.state, data)
|
113
|
+
@connection.parsing_error
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def new_connection(socket)
|
118
|
+
# Keep track of the connection
|
119
|
+
connection = Connection.new @gazelle, socket, @connection_queue
|
120
|
+
@connections.add connection
|
121
|
+
socket.storage = connection # This allows us to re-use the one proc for parsing
|
122
|
+
|
123
|
+
# process any data coming from the socket
|
124
|
+
socket.progress @on_progress
|
125
|
+
socket.start_read
|
126
|
+
|
127
|
+
# Remove connection if the socket closes
|
128
|
+
socket.finally do
|
129
|
+
@connections.delete(connection)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def process_signal(data)
|
134
|
+
if data == Spider::KILL_GAZELLE
|
135
|
+
shutdown
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def shutdown
|
140
|
+
# TODO:: do this nicely
|
141
|
+
@gazelle.stop
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
|
5
|
+
module SpiderGazelle
|
6
|
+
class Request
|
7
|
+
|
8
|
+
SERVER = 'SG'.freeze # The server name
|
9
|
+
|
10
|
+
# Based on http://rack.rubyforge.org/doc/SPEC.html
|
11
|
+
PATH_INFO = 'PATH_INFO'.freeze # Request path from the script name up
|
12
|
+
QUERY_STRING = 'QUERY_STRING'.freeze # portion of the request following a '?' (empty if none)
|
13
|
+
SERVER_NAME = 'SERVER_NAME'.freeze # required although HTTP_HOST takes priority if set
|
14
|
+
SERVER_PORT = 'SERVER_PORT'.freeze # required (set in spider.rb init)
|
15
|
+
REQUEST_URI = 'REQUEST_URI'.freeze
|
16
|
+
REQUEST_PATH = 'REQUEST_PATH'.freeze
|
17
|
+
RACK_URLSCHEME = 'rack.url_scheme'.freeze # http or https
|
18
|
+
RACK_INPUT = 'rack.input'.freeze # an IO like object containing all the request body
|
19
|
+
|
20
|
+
GATEWAY_INTERFACE = "GATEWAY_INTERFACE".freeze
|
21
|
+
CGI_VER = "CGI/1.2".freeze
|
22
|
+
|
23
|
+
RACK = 'rack'.freeze # used for filtering headers
|
24
|
+
EMPTY = ''.freeze
|
25
|
+
|
26
|
+
HTTP_11 = 'HTTP/1.1'.freeze # used in PROTO_ENV
|
27
|
+
HTTP_URL_SCHEME = 'http'.freeze
|
28
|
+
HTTPS_URL_SCHEME = 'https'.freeze
|
29
|
+
HTTP_HOST = 'HTTP_HOST'.freeze
|
30
|
+
COLON_SPACE = ': '.freeze
|
31
|
+
CRLF = "\r\n".freeze
|
32
|
+
LOCALHOST = 'localhost'.freeze
|
33
|
+
|
34
|
+
CONTENT_LENGTH = "Content-Length".freeze
|
35
|
+
CONNECTION = "Connection".freeze
|
36
|
+
KEEP_ALIVE = "Keep-Alive".freeze
|
37
|
+
CLOSE = "close".freeze
|
38
|
+
|
39
|
+
HTTP_CONTENT_LENGTH = 'HTTP_CONTENT_LENGTH'.freeze
|
40
|
+
HTTP_CONTENT_TYPE = 'HTTP_CONTENT_TYPE'.freeze
|
41
|
+
HTTP_AUTHORIZATION = 'AUTHORIZATION'.freeze
|
42
|
+
HTTP_CONNECTION = 'HTTP_CONNECTION'.freeze
|
43
|
+
|
44
|
+
SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze
|
45
|
+
SERVER = 'SpiderGazelle'.freeze
|
46
|
+
|
47
|
+
|
48
|
+
#
|
49
|
+
# TODO:: Add HTTP headers to the env and capitalise them and prefix them with HTTP_
|
50
|
+
# convert - signs to underscores
|
51
|
+
# => copy puma with a const file
|
52
|
+
#
|
53
|
+
PROTO_ENV = {
|
54
|
+
'rack.version'.freeze => ::Rack::VERSION, # Should be an array of integers
|
55
|
+
'rack.errors'.freeze => $stderr, # An error stream that supports: puts, write and flush
|
56
|
+
'rack.multithread'.freeze => true, # can the app be simultaneously invoked by another thread?
|
57
|
+
'rack.multiprocess'.freeze => false, # will the app be simultaneously be invoked in a separate process?
|
58
|
+
'rack.run_once'.freeze => false, # this isn't CGI so will always be false
|
59
|
+
|
60
|
+
'SCRIPT_NAME'.freeze => ENV['SCRIPT_NAME'] || EMPTY, # The virtual path of the app base (empty if root)
|
61
|
+
'CONTENT_TYPE'.freeze => 'text/plain', # works with Rack and Rack::Lint (source puma)
|
62
|
+
'SERVER_PROTOCOL'.freeze => HTTP_11,
|
63
|
+
RACK_URLSCHEME => HTTP_URL_SCHEME, # TODO:: check for / support ssl
|
64
|
+
|
65
|
+
GATEWAY_INTERFACE => CGI_VER,
|
66
|
+
SERVER_SOFTWARE => SERVER
|
67
|
+
}
|
68
|
+
|
69
|
+
|
70
|
+
attr_accessor :env, :url, :header, :body, :keep_alive, :upgrade, :response
|
71
|
+
|
72
|
+
|
73
|
+
def initialize(app, options)
|
74
|
+
@app, @options = app, options
|
75
|
+
@body = ''
|
76
|
+
@header = ''
|
77
|
+
@url = ''
|
78
|
+
@env = PROTO_ENV.dup
|
79
|
+
end
|
80
|
+
|
81
|
+
def execute!
|
82
|
+
@env.delete(HTTP_CONTENT_LENGTH)
|
83
|
+
@env.delete(HTTP_CONTENT_TYPE)
|
84
|
+
@env.delete(HTTP_AUTHORIZATION)
|
85
|
+
@env.delete(HTTP_CONNECTION)
|
86
|
+
|
87
|
+
@env[REQUEST_URI] = @url.freeze
|
88
|
+
@env[RACK_INPUT] = StringIO.new(@body)
|
89
|
+
|
90
|
+
# Break the request into its components
|
91
|
+
query_start = @url.index('?')
|
92
|
+
if query_start
|
93
|
+
path = @url[0...query_start].freeze
|
94
|
+
@env[PATH_INFO] = path
|
95
|
+
@env[REQUEST_PATH] = path
|
96
|
+
@env[QUERY_STRING] = @url[query_start + 1..-1].freeze
|
97
|
+
else
|
98
|
+
@env[PATH_INFO] = @url
|
99
|
+
@env[REQUEST_PATH] = @url
|
100
|
+
end
|
101
|
+
|
102
|
+
# Grab the host name from the request
|
103
|
+
if host = @env[HTTP_HOST]
|
104
|
+
if colon = host.index(':')
|
105
|
+
@env[SERVER_NAME] = host[0, colon]
|
106
|
+
@env[SERVER_PORT] = host[colon+1, host.bytesize]
|
107
|
+
else
|
108
|
+
@env[SERVER_NAME] = host
|
109
|
+
@env[SERVER_PORT] = PROTO_ENV[SERVER_PORT]
|
110
|
+
end
|
111
|
+
else
|
112
|
+
@env[SERVER_NAME] = LOCALHOST
|
113
|
+
@env[SERVER_PORT] = PROTO_ENV[SERVER_PORT]
|
114
|
+
end
|
115
|
+
|
116
|
+
# Process the request
|
117
|
+
#p @env
|
118
|
+
status, headers, body = nil, nil, nil
|
119
|
+
puts Benchmark.measure {
|
120
|
+
status, headers, body = @app.call(@env)
|
121
|
+
}
|
122
|
+
# TODO:: check if upgrades were handled here (hijack_io)
|
123
|
+
|
124
|
+
# Collect the body
|
125
|
+
resp_body = ''
|
126
|
+
body.each do |val|
|
127
|
+
resp_body << val
|
128
|
+
end
|
129
|
+
|
130
|
+
# Build the response
|
131
|
+
resp = "HTTP/1.1 #{status}\r\n"
|
132
|
+
headers[CONTENT_LENGTH] = resp_body.size.to_s # ensure correct size
|
133
|
+
headers[CONNECTION] = CLOSE if @keep_alive == false # ensure appropriate keep alive is set (http 1.1 way)
|
134
|
+
|
135
|
+
headers.each do |key, value|
|
136
|
+
next if key.start_with? RACK
|
137
|
+
|
138
|
+
resp << key
|
139
|
+
resp << COLON_SPACE
|
140
|
+
resp << value
|
141
|
+
resp << CRLF
|
142
|
+
end
|
143
|
+
resp << CRLF
|
144
|
+
resp << resp_body
|
145
|
+
|
146
|
+
# TODO:: streaming responses (using async and a queue object?)
|
147
|
+
@response = resp
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
|
4
|
+
module SpiderGazelle
|
5
|
+
class Spider
|
6
|
+
|
7
|
+
|
8
|
+
DEFAULT_OPTIONS = {
|
9
|
+
:gazelle_count => ::Libuv.cpu_count || 1,
|
10
|
+
:Host => '127.0.0.1',
|
11
|
+
:Port => 8081
|
12
|
+
}
|
13
|
+
|
14
|
+
NEW_SOCKET = 's'.freeze
|
15
|
+
KILL_GAZELLE = 'k'.freeze
|
16
|
+
|
17
|
+
STATES = [:dead, :reanimating, :running, :squashing]
|
18
|
+
MODES = [:thread, :process] # TODO:: implement process
|
19
|
+
|
20
|
+
|
21
|
+
def initialize(app, options = {})
|
22
|
+
@spider = Libuv::Loop.new
|
23
|
+
|
24
|
+
logger = options[:logger] || STDOUT
|
25
|
+
@app = Rack::CommonLogger.new(app, logger)
|
26
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
27
|
+
|
28
|
+
# Manage the set of Gazelle socket listeners
|
29
|
+
@loops = Set.new
|
30
|
+
@select_loop = @loops.cycle # provides a looping enumerator for our round robin
|
31
|
+
@accept_loop = method(:accept_loop)
|
32
|
+
|
33
|
+
# Manage the set of Gazelle signal pipes
|
34
|
+
@gazella = Set.new
|
35
|
+
@accept_gazella = method(:accept_gazella)
|
36
|
+
|
37
|
+
# Connection management
|
38
|
+
@accept_connection = method(:accept_connection)
|
39
|
+
@new_connection = method(:new_connection)
|
40
|
+
|
41
|
+
@status = :dead
|
42
|
+
@mode = :thread
|
43
|
+
|
44
|
+
# Update the base request environment
|
45
|
+
Request::PROTO_ENV[Request::SERVER_PORT] = @options[:port]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Start the server (this method blocks until completion)
|
49
|
+
def run
|
50
|
+
return unless @status == :dead
|
51
|
+
@status = :reanimating
|
52
|
+
@spider.run &method(:reanimate)
|
53
|
+
end
|
54
|
+
|
55
|
+
# If the spider is running we will request to squash it (thread safe)
|
56
|
+
def stop
|
57
|
+
@squash.call
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
|
64
|
+
# There is a new connection pending
|
65
|
+
# We accept it
|
66
|
+
def new_connection(server)
|
67
|
+
server.accept @accept_connection
|
68
|
+
end
|
69
|
+
|
70
|
+
# Once the connection is accepted we disable Nagles Algorithm
|
71
|
+
# This improves performance as we are using vectored or scatter/gather IO
|
72
|
+
# Then we send the socket, round robin, to the gazelle loops
|
73
|
+
def accept_connection(client)
|
74
|
+
client.enable_nodelay
|
75
|
+
loop = @select_loop.next
|
76
|
+
loop.write2(client, NEW_SOCKET)
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# A new gazelle is ready to accept commands
|
81
|
+
def accept_gazella(gazelle)
|
82
|
+
p "gazelle #{@gazella.size} signal port ready"
|
83
|
+
# add the signal port to the set
|
84
|
+
@gazella.add gazelle
|
85
|
+
gazelle.finally do
|
86
|
+
@gazella.delete gazelle
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# A new gazelle loop is ready to accept sockets
|
91
|
+
# We start the server as soon as the first gazelle is ready
|
92
|
+
def accept_loop(loop)
|
93
|
+
p "gazelle #{@loops.size} loop running"
|
94
|
+
|
95
|
+
# start accepting connections
|
96
|
+
if @loops.size == 0
|
97
|
+
# Bind the socket
|
98
|
+
@tcp = @spider.tcp
|
99
|
+
@tcp.bind(@options[:Host], @options[:Port], @new_connection)
|
100
|
+
@tcp.listen(1024)
|
101
|
+
@tcp.catch do |e|
|
102
|
+
p "tcp bind error: #{e}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
@loops.add loop # add the new gazelle to the set
|
107
|
+
@select_loop.rewind # update the enumerator with the new gazelle
|
108
|
+
|
109
|
+
# If a gazelle dies or shuts down we update the set
|
110
|
+
loop.finally do
|
111
|
+
@loops.delete loop
|
112
|
+
@select_loop.rewind
|
113
|
+
|
114
|
+
if @loops.size == 0
|
115
|
+
@tcp.close
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Triggers the creation of gazelles
|
121
|
+
def reanimate(logger)
|
122
|
+
logger.progress do |level, errorid, error|
|
123
|
+
begin
|
124
|
+
p "Log called: #{level}: #{errorid}\n#{error.message}\n#{error.backtrace.join("\n")}\n"
|
125
|
+
rescue Exception
|
126
|
+
p 'error in gazelle logger'
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Create a function for stopping the spider from another thread
|
131
|
+
@squash = @spider.async do
|
132
|
+
squash
|
133
|
+
end
|
134
|
+
|
135
|
+
# Bind the pipe for sending sockets to gazelle
|
136
|
+
begin
|
137
|
+
File.unlink(DELEGATE_PIPE)
|
138
|
+
rescue
|
139
|
+
end
|
140
|
+
@delegator = @spider.pipe(true)
|
141
|
+
@delegator.bind(DELEGATE_PIPE) do
|
142
|
+
@delegator.accept @accept_loop
|
143
|
+
end
|
144
|
+
@delegator.listen(128)
|
145
|
+
|
146
|
+
# Bind the pipe for communicating with gazelle
|
147
|
+
begin
|
148
|
+
File.unlink(SIGNAL_PIPE)
|
149
|
+
rescue
|
150
|
+
end
|
151
|
+
@signaller = @spider.pipe(true)
|
152
|
+
@signaller.bind(SIGNAL_PIPE) do
|
153
|
+
@signaller.accept @accept_gazella
|
154
|
+
end
|
155
|
+
@signaller.listen(128)
|
156
|
+
|
157
|
+
|
158
|
+
# Launch the gazelle here
|
159
|
+
@options[:gazelle_count].times do
|
160
|
+
Thread.new do
|
161
|
+
gazelle = Gazelle.new(@app, @options)
|
162
|
+
gazelle.run
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Signal gazelle death here
|
167
|
+
@spider.signal(:INT) do
|
168
|
+
squash
|
169
|
+
end
|
170
|
+
|
171
|
+
# Update state only once the event loop is ready
|
172
|
+
@status = :running
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
# Triggers a shutdown of the gazelles.
|
177
|
+
# We ensure the process is running here as signals can be called multiple times
|
178
|
+
def squash
|
179
|
+
if @status == :running
|
180
|
+
|
181
|
+
# Update the state and close the socket
|
182
|
+
@status = :squashing
|
183
|
+
@tcp.close
|
184
|
+
|
185
|
+
# Signal all the gazelle to shutdown
|
186
|
+
promises = []
|
187
|
+
@gazella.each do |gazelle|
|
188
|
+
promises << gazelle.write(KILL_GAZELLE)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Once the signal has been sent we can stop the spider loop
|
192
|
+
@spider.finally(*promises).finally do
|
193
|
+
# TODO:: need a better system for ensuring these are cleaned up
|
194
|
+
begin
|
195
|
+
@delegator.close
|
196
|
+
File.unlink(DELEGATE_PIPE)
|
197
|
+
rescue
|
198
|
+
end
|
199
|
+
begin
|
200
|
+
@signaller.close
|
201
|
+
File.unlink(SIGNAL_PIPE)
|
202
|
+
rescue
|
203
|
+
end
|
204
|
+
@spider.stop
|
205
|
+
@status = :dead
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'websocket/driver'
|
2
|
+
|
3
|
+
|
4
|
+
module SpiderGazelle
|
5
|
+
class Websocket
|
6
|
+
attr_reader :env, :url, :driver, :socket
|
7
|
+
|
8
|
+
|
9
|
+
def initialize(tcp, env)
|
10
|
+
@socket, @env = tcp, env
|
11
|
+
|
12
|
+
scheme = Rack::Request.new(env).ssl? ? 'wss://' : 'ws://'
|
13
|
+
@url = scheme + env['HTTP_HOST'] + env['REQUEST_URI']
|
14
|
+
@driver = ::WebSocket::Driver.rack(self)
|
15
|
+
|
16
|
+
# Pass data from the socket to the driver
|
17
|
+
@socket.progress do |data|
|
18
|
+
@driver.parse(data)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Driver has indicated that it is closing
|
22
|
+
# We'll close the socket after writing any remaining data
|
23
|
+
@driver.on(:close) {
|
24
|
+
@socket.shutdown
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def start
|
29
|
+
@driver.start
|
30
|
+
@socket.start_read
|
31
|
+
end
|
32
|
+
|
33
|
+
def write(string)
|
34
|
+
@socket.write(string)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "http-parser" # C based, fast, http parser
|
2
|
+
require "libuv" # Ruby Libuv FFI wrapper
|
3
|
+
require "rack" # Ruby webserver abstraction
|
4
|
+
|
5
|
+
require "spider-gazelle/version"
|
6
|
+
require "spider-gazelle/request" # Holds request information and handles request processing
|
7
|
+
require "spider-gazelle/connection" # Holds connection information and handles request pipelining
|
8
|
+
require "spider-gazelle/gazelle" # Processes data received from connections
|
9
|
+
require "spider-gazelle/spider" # Accepts connections and offloads them to gazelles
|
10
|
+
|
11
|
+
|
12
|
+
module SpiderGazelle
|
13
|
+
# Delegate pipe used for passing sockets to the gazelles
|
14
|
+
# Signal pipe used to pass control signals
|
15
|
+
DELEGATE_PIPE = "/tmp/spider-gazelle.delegate"
|
16
|
+
SIGNAL_PIPE = "/tmp/spider-gazelle.signal"
|
17
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "spider-gazelle/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "spider-gazelle"
|
7
|
+
s.version = SpiderGazelle::VERSION
|
8
|
+
s.authors = ["Stephen von Takach"]
|
9
|
+
s.email = ["steve@cotag.me"]
|
10
|
+
s.license = 'MIT'
|
11
|
+
s.homepage = "https://github.com/cotag/spider-gazelle"
|
12
|
+
s.summary = "A fast, parallel and concurrent web server for ruby"
|
13
|
+
s.description = <<-EOF
|
14
|
+
Spidergazelle, spidergazelle, amazingly agile, she leaps through the veldt,
|
15
|
+
Spidergazelle, spidergazelle! She don’t care what you think, she says what the hell!
|
16
|
+
Look out! Here comes the Spidergazelle!
|
17
|
+
EOF
|
18
|
+
|
19
|
+
s.add_dependency 'rake'
|
20
|
+
s.add_dependency 'http-parser' # Ruby FFI bindings for https://github.com/joyent/http-parser
|
21
|
+
s.add_dependency 'libuv' # Ruby FFI bindings for https://github.com/joyent/libuv
|
22
|
+
s.add_dependency 'rack', '>= 1.0.0' # Ruby web server interface
|
23
|
+
s.add_dependency 'websocket-driver' # Websocket parser
|
24
|
+
|
25
|
+
s.add_development_dependency 'rspec' # Testing framework
|
26
|
+
s.add_development_dependency 'yard' # Comment based documentation generation
|
27
|
+
|
28
|
+
|
29
|
+
s.files = Dir["{lib,bin}/**/*"] + %w(Rakefile spider-gazelle.gemspec README.md LICENSE)
|
30
|
+
s.test_files = Dir["spec/**/*"]
|
31
|
+
s.extra_rdoc_files = ["README.md"]
|
32
|
+
|
33
|
+
s.bindir = 'bin'
|
34
|
+
s.executables = ['sg']
|
35
|
+
|
36
|
+
s.require_paths = ["lib"]
|
37
|
+
end
|
metadata
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spider-gazelle
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephen von Takach
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: http-parser
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: libuv
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rack
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.0.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.0.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: websocket-driver
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: yard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: |2
|
112
|
+
Spidergazelle, spidergazelle, amazingly agile, she leaps through the veldt,
|
113
|
+
Spidergazelle, spidergazelle! She don’t care what you think, she says what the hell!
|
114
|
+
Look out! Here comes the Spidergazelle!
|
115
|
+
email:
|
116
|
+
- steve@cotag.me
|
117
|
+
executables:
|
118
|
+
- sg
|
119
|
+
extensions: []
|
120
|
+
extra_rdoc_files:
|
121
|
+
- README.md
|
122
|
+
files:
|
123
|
+
- lib/rack/handler/spider-gazelle.rb
|
124
|
+
- lib/spider-gazelle/connection.rb
|
125
|
+
- lib/spider-gazelle/error.rb
|
126
|
+
- lib/spider-gazelle/gazelle.rb
|
127
|
+
- lib/spider-gazelle/request.rb
|
128
|
+
- lib/spider-gazelle/spider.rb
|
129
|
+
- lib/spider-gazelle/upgrades/websocket.rb
|
130
|
+
- lib/spider-gazelle/version.rb
|
131
|
+
- lib/spider-gazelle.rb
|
132
|
+
- bin/sg
|
133
|
+
- Rakefile
|
134
|
+
- spider-gazelle.gemspec
|
135
|
+
- README.md
|
136
|
+
- LICENSE
|
137
|
+
homepage: https://github.com/cotag/spider-gazelle
|
138
|
+
licenses:
|
139
|
+
- MIT
|
140
|
+
metadata: {}
|
141
|
+
post_install_message:
|
142
|
+
rdoc_options: []
|
143
|
+
require_paths:
|
144
|
+
- lib
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
requirements: []
|
156
|
+
rubyforge_project:
|
157
|
+
rubygems_version: 2.0.3
|
158
|
+
signing_key:
|
159
|
+
specification_version: 4
|
160
|
+
summary: A fast, parallel and concurrent web server for ruby
|
161
|
+
test_files: []
|
162
|
+
has_rdoc:
|