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 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
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rspec/core/rake_task'
4
+
5
+ task :default => [:test]
6
+
7
+ RSpec::Core::RakeTask.new(:test)
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,3 @@
1
+ module SpiderGazelle
2
+ VERSION = "0.1.0"
3
+ 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: