spider-gazelle 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/sg +46 -17
- data/lib/spider-gazelle.rb +5 -0
- data/lib/spider-gazelle/app_store.rb +67 -0
- data/lib/spider-gazelle/binding.rb +72 -0
- data/lib/spider-gazelle/connection.rb +255 -25
- data/lib/spider-gazelle/gazelle.rb +52 -30
- data/lib/spider-gazelle/request.rb +40 -40
- data/lib/spider-gazelle/spider.rb +292 -139
- data/lib/spider-gazelle/upgrades/websocket.rb +83 -32
- data/lib/spider-gazelle/version.rb +1 -1
- data/spider-gazelle.gemspec +2 -0
- metadata +32 -2
@@ -9,17 +9,28 @@ module SpiderGazelle
|
|
9
9
|
REQUEST_METHOD = 'REQUEST_METHOD'.freeze # GET, POST, etc
|
10
10
|
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
attr_reader :parser_cache, :connections, :logger
|
13
|
+
|
14
|
+
|
15
|
+
def set_instance_type(inst)
|
16
|
+
inst.type = :request
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
def initialize(loop, logger, mode)
|
22
|
+
@gazelle = loop
|
14
23
|
@connections = Set.new # Set of active connections on this thread
|
15
24
|
@parser_cache = [] # Stale parser objects cached for reuse
|
16
|
-
@connection_queue = ::Libuv::Q::ResolvedPromise.new(@gazelle, true)
|
17
25
|
|
18
|
-
@
|
19
|
-
@
|
26
|
+
@mode = mode
|
27
|
+
@logger = logger
|
28
|
+
@app_cache = {}
|
29
|
+
@connection_queue = ::Libuv::Q::ResolvedPromise.new(@gazelle, true)
|
20
30
|
|
21
31
|
# A single parser instance for processing requests for each gazelle
|
22
32
|
@parser = ::HttpParser::Parser.new(self)
|
33
|
+
@set_instance_type = method(:set_instance_type)
|
23
34
|
|
24
35
|
# Single progress callback for each gazelle
|
25
36
|
@on_progress = method(:on_progress)
|
@@ -29,28 +40,28 @@ module SpiderGazelle
|
|
29
40
|
@gazelle.run do |logger|
|
30
41
|
logger.progress do |level, errorid, error|
|
31
42
|
begin
|
32
|
-
|
43
|
+
msg = "Gazelle log: #{level}: #{errorid}\n#{error.message}\n#{error.backtrace.join("\n") if error.backtrace}\n"
|
44
|
+
@logger.error msg
|
45
|
+
puts msg
|
33
46
|
rescue Exception
|
34
47
|
p 'error in gazelle logger'
|
35
48
|
end
|
36
49
|
end
|
37
50
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
@socket_server.
|
42
|
-
new_connection
|
51
|
+
unless @mode == :no_ipc
|
52
|
+
# A pipe used to forward connections to different threads
|
53
|
+
@socket_server = @gazelle.pipe(true)
|
54
|
+
@socket_server.connect(DELEGATE_PIPE) do
|
55
|
+
@socket_server.progress &method(:new_connection)
|
56
|
+
@socket_server.start_read2
|
43
57
|
end
|
44
|
-
@socket_server.start_read2
|
45
|
-
end
|
46
58
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
59
|
+
# A pipe used to signal various control commands (shutdown, etc)
|
60
|
+
@signal_server = @gazelle.pipe
|
61
|
+
@signal_server.connect(SIGNAL_PIPE) do
|
62
|
+
@signal_server.progress &method(:process_signal)
|
63
|
+
@signal_server.start_read
|
52
64
|
end
|
53
|
-
@signal_server.start_read
|
54
65
|
end
|
55
66
|
end
|
56
67
|
end
|
@@ -58,7 +69,7 @@ module SpiderGazelle
|
|
58
69
|
|
59
70
|
# HTTP Parser callbacks:
|
60
71
|
def on_message_begin(parser)
|
61
|
-
@connection.start_parsing
|
72
|
+
@connection.start_parsing
|
62
73
|
end
|
63
74
|
|
64
75
|
def on_url(parser, url)
|
@@ -100,6 +111,11 @@ module SpiderGazelle
|
|
100
111
|
@connection.finished_parsing
|
101
112
|
end
|
102
113
|
|
114
|
+
def discard(connection)
|
115
|
+
@connections.delete(connection)
|
116
|
+
@parser_cache << connection.state
|
117
|
+
end
|
118
|
+
|
103
119
|
|
104
120
|
protected
|
105
121
|
|
@@ -114,23 +130,28 @@ module SpiderGazelle
|
|
114
130
|
end
|
115
131
|
end
|
116
132
|
|
117
|
-
def new_connection(socket)
|
133
|
+
def new_connection(data, socket)
|
134
|
+
# Data == "TLS_indicator Port APP_ID"
|
135
|
+
tls, port, app_id = data.split(' ', 3)
|
136
|
+
app = @app_cache[app_id.to_sym] ||= AppStore.get(app_id)
|
137
|
+
inst = @parser_cache.pop || ::HttpParser::Parser.new_instance(&@set_instance_type)
|
138
|
+
|
139
|
+
# process any data coming from the socket
|
140
|
+
socket.progress @on_progress
|
141
|
+
if tls == 'T'
|
142
|
+
# TODO:: Allow some globals for supplying the certs
|
143
|
+
socket.start_tls(:server => true)
|
144
|
+
end
|
145
|
+
|
118
146
|
# Keep track of the connection
|
119
|
-
connection = Connection.new @gazelle, socket, @connection_queue
|
147
|
+
connection = Connection.new self, @gazelle, socket, port, inst, app, @connection_queue
|
120
148
|
@connections.add connection
|
121
149
|
socket.storage = connection # This allows us to re-use the one proc for parsing
|
122
150
|
|
123
|
-
# process any data coming from the socket
|
124
|
-
socket.progress @on_progress
|
125
151
|
socket.start_read
|
126
|
-
|
127
|
-
# Remove connection if the socket closes
|
128
|
-
socket.finally do
|
129
|
-
@connections.delete(connection)
|
130
|
-
end
|
131
152
|
end
|
132
153
|
|
133
|
-
def process_signal(data)
|
154
|
+
def process_signal(data, pipe)
|
134
155
|
if data == Spider::KILL_GAZELLE
|
135
156
|
shutdown
|
136
157
|
end
|
@@ -138,6 +159,7 @@ module SpiderGazelle
|
|
138
159
|
|
139
160
|
def shutdown
|
140
161
|
# TODO:: do this nicely
|
162
|
+
# Need to signal the connections to close
|
141
163
|
@gazelle.stop
|
142
164
|
end
|
143
165
|
end
|
@@ -1,12 +1,9 @@
|
|
1
1
|
require 'stringio'
|
2
|
-
require 'benchmark'
|
3
2
|
|
4
3
|
|
5
4
|
module SpiderGazelle
|
6
5
|
class Request
|
7
6
|
|
8
|
-
SERVER = 'SG'.freeze # The server name
|
9
|
-
|
10
7
|
# Based on http://rack.rubyforge.org/doc/SPEC.html
|
11
8
|
PATH_INFO = 'PATH_INFO'.freeze # Request path from the script name up
|
12
9
|
QUERY_STRING = 'QUERY_STRING'.freeze # portion of the request following a '?' (empty if none)
|
@@ -16,25 +13,23 @@ module SpiderGazelle
|
|
16
13
|
REQUEST_PATH = 'REQUEST_PATH'.freeze
|
17
14
|
RACK_URLSCHEME = 'rack.url_scheme'.freeze # http or https
|
18
15
|
RACK_INPUT = 'rack.input'.freeze # an IO like object containing all the request body
|
16
|
+
RACK_HIJACKABLE = 'rack.hijack?'.freeze # hijacking IO is supported
|
17
|
+
RACK_HIJACK = 'rack.hijack'.freeze # callback for indicating that this socket will be hijacked
|
18
|
+
RACK_HIJACK_IO = 'rack.hijack_io'.freeze # the object for performing IO on after hijack is called
|
19
|
+
RACK_ASYNC = 'async.callback'.freeze
|
19
20
|
|
20
21
|
GATEWAY_INTERFACE = "GATEWAY_INTERFACE".freeze
|
21
22
|
CGI_VER = "CGI/1.2".freeze
|
22
23
|
|
23
|
-
RACK = 'rack'.freeze # used for filtering headers
|
24
24
|
EMPTY = ''.freeze
|
25
25
|
|
26
26
|
HTTP_11 = 'HTTP/1.1'.freeze # used in PROTO_ENV
|
27
27
|
HTTP_URL_SCHEME = 'http'.freeze
|
28
28
|
HTTPS_URL_SCHEME = 'https'.freeze
|
29
29
|
HTTP_HOST = 'HTTP_HOST'.freeze
|
30
|
-
COLON_SPACE = ': '.freeze
|
31
|
-
CRLF = "\r\n".freeze
|
32
30
|
LOCALHOST = 'localhost'.freeze
|
33
31
|
|
34
|
-
CONTENT_LENGTH = "Content-Length".freeze
|
35
|
-
CONNECTION = "Connection".freeze
|
36
32
|
KEEP_ALIVE = "Keep-Alive".freeze
|
37
|
-
CLOSE = "close".freeze
|
38
33
|
|
39
34
|
HTTP_CONTENT_LENGTH = 'HTTP_CONTENT_LENGTH'.freeze
|
40
35
|
HTTP_CONTENT_TYPE = 'HTTP_CONTENT_TYPE'.freeze
|
@@ -43,6 +38,7 @@ module SpiderGazelle
|
|
43
38
|
|
44
39
|
SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze
|
45
40
|
SERVER = 'SpiderGazelle'.freeze
|
41
|
+
REMOTE_ADDR = 'REMOTE_ADDR'.freeze
|
46
42
|
|
47
43
|
|
48
44
|
#
|
@@ -60,22 +56,28 @@ module SpiderGazelle
|
|
60
56
|
'SCRIPT_NAME'.freeze => ENV['SCRIPT_NAME'] || EMPTY, # The virtual path of the app base (empty if root)
|
61
57
|
'CONTENT_TYPE'.freeze => 'text/plain', # works with Rack and Rack::Lint (source puma)
|
62
58
|
'SERVER_PROTOCOL'.freeze => HTTP_11,
|
63
|
-
RACK_URLSCHEME => HTTP_URL_SCHEME, # TODO:: check for / support ssl
|
64
59
|
|
65
60
|
GATEWAY_INTERFACE => CGI_VER,
|
66
61
|
SERVER_SOFTWARE => SERVER
|
67
62
|
}
|
68
63
|
|
69
64
|
|
70
|
-
attr_accessor :env, :url, :header, :body, :keep_alive, :upgrade, :
|
65
|
+
attr_accessor :env, :url, :header, :body, :keep_alive, :upgrade, :deferred
|
66
|
+
attr_reader :hijacked, :response
|
71
67
|
|
72
68
|
|
73
|
-
def initialize(
|
74
|
-
@app
|
69
|
+
def initialize(connection, app)
|
70
|
+
@app = app
|
75
71
|
@body = ''
|
76
72
|
@header = ''
|
77
73
|
@url = ''
|
74
|
+
@execute = method(:execute)
|
78
75
|
@env = PROTO_ENV.dup
|
76
|
+
@loop = connection.loop
|
77
|
+
@env[SERVER_PORT] = connection.port
|
78
|
+
@env[REMOTE_ADDR] = connection.remote_ip
|
79
|
+
@env[RACK_URLSCHEME] = connection.tls ? HTTPS_URL_SCHEME : HTTP_URL_SCHEME
|
80
|
+
@env[RACK_ASYNC] = connection.async_callback
|
79
81
|
end
|
80
82
|
|
81
83
|
def execute!
|
@@ -113,38 +115,36 @@ module SpiderGazelle
|
|
113
115
|
@env[SERVER_PORT] = PROTO_ENV[SERVER_PORT]
|
114
116
|
end
|
115
117
|
|
116
|
-
#
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
118
|
+
# Provide hijack options if this is an upgrade request
|
119
|
+
if @upgrade == true
|
120
|
+
@env[RACK_HIJACKABLE] = true
|
121
|
+
@env[RACK_HIJACK] = method(:hijack)
|
128
122
|
end
|
129
123
|
|
130
|
-
#
|
131
|
-
|
132
|
-
|
133
|
-
|
124
|
+
# Execute the request
|
125
|
+
@response = catch(:async, &@execute)
|
126
|
+
if @response.nil? || @response[0] == -1
|
127
|
+
@deferred = @loop.defer
|
128
|
+
end
|
129
|
+
@response
|
130
|
+
end
|
134
131
|
|
135
|
-
headers.each do |key, value|
|
136
|
-
next if key.start_with? RACK
|
137
132
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
133
|
+
protected
|
134
|
+
|
135
|
+
|
136
|
+
# Execute the request then close the body
|
137
|
+
# NOTE:: closing the body here might cause issues (see connection.rb)
|
138
|
+
def execute(*args)
|
139
|
+
result = @app.call(@env)
|
140
|
+
body = result[2]
|
141
|
+
body.close if body.respond_to?(:close)
|
142
|
+
result
|
143
|
+
end
|
145
144
|
|
146
|
-
|
147
|
-
@
|
145
|
+
def hijack
|
146
|
+
@hijacked = @loop.defer
|
147
|
+
@env[RACK_HIJACK_IO] = @hijacked.promise
|
148
148
|
end
|
149
149
|
end
|
150
150
|
end
|
@@ -1,210 +1,363 @@
|
|
1
1
|
require 'set'
|
2
|
+
require 'thread'
|
3
|
+
require 'logger'
|
4
|
+
require 'singleton'
|
5
|
+
require 'fileutils'
|
2
6
|
|
3
7
|
|
4
8
|
module SpiderGazelle
|
5
9
|
class Spider
|
10
|
+
include Singleton
|
6
11
|
|
7
12
|
|
8
|
-
|
9
|
-
|
10
|
-
:Host => '127.0.0.1',
|
11
|
-
:Port => 8081
|
12
|
-
}
|
13
|
-
|
14
|
-
NEW_SOCKET = 's'.freeze
|
13
|
+
USE_TLS = 'T'.freeze
|
14
|
+
NO_TLS = 'F'.freeze
|
15
15
|
KILL_GAZELLE = 'k'.freeze
|
16
16
|
|
17
|
-
STATES = [:
|
18
|
-
MODES = [:thread, :process] # TODO:: implement
|
17
|
+
STATES = [:reanimating, :running, :squashing, :dead]
|
18
|
+
MODES = [:thread, :process, :no_ipc] # TODO:: implement clustering using processes
|
19
19
|
|
20
20
|
|
21
|
-
|
22
|
-
@spider = Libuv::Loop.new
|
21
|
+
attr_reader :state, :mode, :threads, :logger
|
23
22
|
|
24
|
-
logger = options[:logger] || STDOUT
|
25
|
-
@app = Rack::CommonLogger.new(app, logger)
|
26
|
-
@options = DEFAULT_OPTIONS.merge(options)
|
27
23
|
|
28
|
-
|
29
|
-
|
30
|
-
@
|
31
|
-
@
|
24
|
+
def initialize
|
25
|
+
# Threaded mode by default
|
26
|
+
@status = :reanimating
|
27
|
+
@bindings = {
|
28
|
+
# id => [bind1, bind2]
|
29
|
+
}
|
32
30
|
|
33
|
-
|
34
|
-
@
|
35
|
-
@accept_gazella = method(:accept_gazella)
|
31
|
+
mode = ENV['SG_MODE'] || :thread
|
32
|
+
@mode = mode.to_sym
|
36
33
|
|
37
|
-
|
38
|
-
|
39
|
-
|
34
|
+
if @mode == :no_ipc
|
35
|
+
@delegate = method(:direct_delegate)
|
36
|
+
else
|
37
|
+
@delegate = method(:delegate)
|
38
|
+
end
|
39
|
+
@squash = method(:squash)
|
40
40
|
|
41
|
-
@status = :dead
|
42
|
-
@mode = :thread
|
43
41
|
|
44
|
-
|
45
|
-
|
42
|
+
log_path = ENV['SG_LOG'] || File.expand_path('../../../logs/server.log', __FILE__)
|
43
|
+
dirname = File.dirname(log_path)
|
44
|
+
unless File.directory?(dirname)
|
45
|
+
FileUtils.mkdir_p(dirname)
|
46
|
+
end
|
47
|
+
@logger = ::Logger.new(log_path.to_s, 10, 4194304)
|
48
|
+
|
49
|
+
# Keep track of the loading process
|
50
|
+
@waiting_gazelle = 0
|
51
|
+
@gazelle_count = 0
|
52
|
+
|
53
|
+
# Spider always runs on the default loop
|
54
|
+
@web = ::Libuv::Loop.default
|
55
|
+
@gazelles_loaded = @web.defer
|
56
|
+
|
57
|
+
# Start the server
|
58
|
+
if @web.reactor_running?
|
59
|
+
# Call run so we can be notified of errors
|
60
|
+
@web.run &method(:reanimate)
|
61
|
+
else
|
62
|
+
# Don't block on this thread if default reactor not running
|
63
|
+
Thread.new do
|
64
|
+
@web.run &method(:reanimate)
|
65
|
+
end
|
66
|
+
end
|
46
67
|
end
|
47
68
|
|
48
|
-
#
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
@
|
69
|
+
# Provides a promise that resolves when we are read to start binding applications
|
70
|
+
#
|
71
|
+
# @return [::Libuv::Q::Promise] that indicates when the gazelles are loaded
|
72
|
+
def loaded
|
73
|
+
@gazelles_loaded.promise unless @gazelles_loaded.nil?
|
53
74
|
end
|
54
75
|
|
55
|
-
#
|
56
|
-
|
57
|
-
|
58
|
-
|
76
|
+
# A thread safe method for loading and binding rack apps. The app can be pre-loaded or a rackup file
|
77
|
+
#
|
78
|
+
# @param app [String, Object] rackup filename or rack app
|
79
|
+
# @param ports [Hash, Array] binding config or array of binding config
|
80
|
+
# @return [::Libuv::Q::Promise] resolves once the app is loaded (and bound if SG is running)
|
81
|
+
def load(app, ports = [])
|
82
|
+
defer = @web.defer
|
59
83
|
|
84
|
+
ports = [ports] if ports.is_a? Hash
|
85
|
+
ports << {} if ports.empty?
|
60
86
|
|
61
|
-
|
87
|
+
@web.schedule do
|
88
|
+
begin
|
89
|
+
app_id = AppStore.load(app)
|
90
|
+
bindings = @bindings[app_id] ||= []
|
62
91
|
|
92
|
+
ports.each do |options|
|
93
|
+
binding = Binding.new(@web, @delegate, app_id, options)
|
94
|
+
bindings << binding
|
95
|
+
end
|
63
96
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
97
|
+
if @status == :running
|
98
|
+
defer.resolve(start(app, app_id))
|
99
|
+
else
|
100
|
+
defer.resolve(true)
|
101
|
+
end
|
102
|
+
rescue Exception => e
|
103
|
+
defer.reject(e)
|
104
|
+
end
|
105
|
+
end
|
69
106
|
|
70
|
-
|
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)
|
107
|
+
defer.promise
|
77
108
|
end
|
78
109
|
|
79
|
-
|
80
|
-
#
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
@
|
85
|
-
|
86
|
-
|
110
|
+
# Starts the app specified. It must already be loaded
|
111
|
+
#
|
112
|
+
# @param app [String, Object] rackup filename or the rack app instance
|
113
|
+
# @return [::Libuv::Q::Promise] resolves once the app is bound to the port
|
114
|
+
def start(app, app_id = nil)
|
115
|
+
defer = @web.defer
|
116
|
+
app_id = app_id || AppStore.lookup(app)
|
117
|
+
|
118
|
+
if app_id != nil && @status == :running
|
119
|
+
@web.schedule do
|
120
|
+
bindings = @bindings[app_id] ||= []
|
121
|
+
starting = []
|
122
|
+
|
123
|
+
bindings.each do |binding|
|
124
|
+
starting << binding.bind
|
125
|
+
end
|
126
|
+
defer.resolve(::Libuv::Q.all(@web, *starting))
|
127
|
+
end
|
128
|
+
elsif app_id.nil?
|
129
|
+
defer.reject('application not loaded')
|
130
|
+
else
|
131
|
+
defer.reject('server not running')
|
87
132
|
end
|
133
|
+
|
134
|
+
defer.promise
|
88
135
|
end
|
89
136
|
|
90
|
-
#
|
91
|
-
#
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
@
|
100
|
-
|
101
|
-
|
102
|
-
|
137
|
+
# Stops the app specified. If loaded
|
138
|
+
#
|
139
|
+
# @param app [String, Object] rackup filename or the rack app instance
|
140
|
+
# @return [::Libuv::Q::Promise] resolves once the app is no longer bound to the port
|
141
|
+
def stop(app, app_id = nil)
|
142
|
+
defer = @web.defer
|
143
|
+
app_id = app_id || AppStore.lookup(app)
|
144
|
+
|
145
|
+
if !app_id.nil?
|
146
|
+
@web.schedule do
|
147
|
+
bindings = @bindings[app_id]
|
148
|
+
closing = []
|
149
|
+
|
150
|
+
if bindings != nil
|
151
|
+
bindings.each do |binding|
|
152
|
+
result = binding.unbind
|
153
|
+
closing << result unless result.nil?
|
154
|
+
end
|
155
|
+
end
|
156
|
+
defer.resolve(::Libuv::Q.all(@web, *closing))
|
103
157
|
end
|
158
|
+
else
|
159
|
+
defer.reject('application not loaded')
|
104
160
|
end
|
105
161
|
|
106
|
-
|
107
|
-
|
162
|
+
defer.promise
|
163
|
+
end
|
108
164
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
165
|
+
# Signals spider gazelle to shutdown gracefully
|
166
|
+
def shutdown
|
167
|
+
@signal_squash.call
|
168
|
+
end
|
113
169
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
170
|
+
|
171
|
+
protected
|
172
|
+
|
173
|
+
|
174
|
+
# Called from the binding for sending to gazelles
|
175
|
+
def delegate(client, tls, port, app_id)
|
176
|
+
indicator = tls ? USE_TLS : NO_TLS
|
177
|
+
loop = @select_handler.next
|
178
|
+
loop.write2(client, "#{indicator} #{port} #{app_id}")
|
179
|
+
end
|
180
|
+
|
181
|
+
def direct_delegate(client, tls, port, app_id)
|
182
|
+
indicator = tls ? USE_TLS : NO_TLS
|
183
|
+
@gazelle.__send__(:new_connection, "#{indicator} #{port} #{app_id}", client)
|
118
184
|
end
|
119
185
|
|
120
186
|
# Triggers the creation of gazelles
|
121
187
|
def reanimate(logger)
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
188
|
+
# Manage the set of Gazelle socket listeners
|
189
|
+
@threads = Set.new
|
190
|
+
|
191
|
+
if @mode == :thread
|
192
|
+
cpus = ::Libuv.cpu_count || 1
|
193
|
+
cpus.times do
|
194
|
+
@threads << Libuv::Loop.new
|
127
195
|
end
|
196
|
+
elsif @mode == :no_ipc
|
197
|
+
# TODO:: need to perform process mode as well
|
198
|
+
@threads << @web
|
128
199
|
end
|
129
200
|
|
201
|
+
@handlers = Set.new
|
202
|
+
@select_handler = @handlers.cycle # provides a looping enumerator for our round robin
|
203
|
+
@accept_handler = method(:accept_handler)
|
204
|
+
|
205
|
+
# Manage the set of Gazelle signal pipes
|
206
|
+
@gazella = Set.new
|
207
|
+
@accept_gazella = method(:accept_gazella)
|
208
|
+
|
130
209
|
# Create a function for stopping the spider from another thread
|
131
|
-
@
|
132
|
-
squash
|
133
|
-
end
|
210
|
+
@signal_squash = @web.async @squash
|
134
211
|
|
135
|
-
#
|
136
|
-
|
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)
|
212
|
+
# Link up the loops logger
|
213
|
+
logger.progress method(:log)
|
145
214
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
215
|
+
if @mode == :no_ipc
|
216
|
+
@gazelle = Gazelle.new(@web, @logger, @mode)
|
217
|
+
@gazelle_count = 1
|
218
|
+
start_bindings
|
219
|
+
else
|
220
|
+
# Bind the pipe for sending sockets to gazelle
|
221
|
+
begin
|
222
|
+
File.unlink(DELEGATE_PIPE)
|
223
|
+
rescue
|
224
|
+
end
|
225
|
+
@delegator = @web.pipe(true)
|
226
|
+
@delegator.bind(DELEGATE_PIPE) do
|
227
|
+
@delegator.accept @accept_handler
|
228
|
+
end
|
229
|
+
@delegator.listen(16)
|
156
230
|
|
231
|
+
# Bind the pipe for communicating with gazelle
|
232
|
+
begin
|
233
|
+
File.unlink(SIGNAL_PIPE)
|
234
|
+
rescue
|
235
|
+
end
|
236
|
+
@signaller = @web.pipe(true)
|
237
|
+
@signaller.bind(SIGNAL_PIPE) do
|
238
|
+
@signaller.accept @accept_gazella
|
239
|
+
end
|
240
|
+
@signaller.listen(16)
|
157
241
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
242
|
+
# Launch the gazelle here
|
243
|
+
@threads.each do |thread|
|
244
|
+
Thread.new do
|
245
|
+
gazelle = Gazelle.new(thread, @logger, @mode)
|
246
|
+
gazelle.run
|
247
|
+
end
|
248
|
+
@waiting_gazelle += 1
|
163
249
|
end
|
164
250
|
end
|
165
251
|
|
166
252
|
# Signal gazelle death here
|
167
|
-
@
|
168
|
-
squash
|
169
|
-
end
|
253
|
+
@web.signal :INT, @squash
|
170
254
|
|
171
255
|
# Update state only once the event loop is ready
|
172
|
-
@
|
256
|
+
@gazelles_loaded.promise
|
173
257
|
end
|
174
258
|
|
175
|
-
|
176
259
|
# Triggers a shutdown of the gazelles.
|
177
260
|
# We ensure the process is running here as signals can be called multiple times
|
178
|
-
def squash
|
261
|
+
def squash(*args)
|
179
262
|
if @status == :running
|
180
263
|
|
181
264
|
# Update the state and close the socket
|
182
265
|
@status = :squashing
|
183
|
-
@
|
184
|
-
|
185
|
-
# Signal all the gazelle to shutdown
|
186
|
-
promises = []
|
187
|
-
@gazella.each do |gazelle|
|
188
|
-
promises << gazelle.write(KILL_GAZELLE)
|
266
|
+
@bindings.each_key do |key|
|
267
|
+
stop(key)
|
189
268
|
end
|
190
269
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
270
|
+
if @mode == :no_ipc
|
271
|
+
@web.stop
|
272
|
+
@status = :dead
|
273
|
+
else
|
274
|
+
# Signal all the gazelle to shutdown
|
275
|
+
promises = []
|
276
|
+
@gazella.each do |gazelle|
|
277
|
+
promises << gazelle.write(KILL_GAZELLE)
|
198
278
|
end
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
279
|
+
|
280
|
+
# Once the signal has been sent we can stop the spider loop
|
281
|
+
@web.finally(*promises).finally do
|
282
|
+
|
283
|
+
# TODO:: need a better system for ensuring these are cleaned up
|
284
|
+
# Especially when we implement live migrations and process clusters
|
285
|
+
begin
|
286
|
+
@delegator.close
|
287
|
+
File.unlink(DELEGATE_PIPE)
|
288
|
+
rescue
|
289
|
+
end
|
290
|
+
begin
|
291
|
+
@signaller.close
|
292
|
+
File.unlink(SIGNAL_PIPE)
|
293
|
+
rescue
|
294
|
+
end
|
295
|
+
|
296
|
+
@web.stop
|
297
|
+
@status = :dead
|
203
298
|
end
|
204
|
-
@spider.stop
|
205
|
-
@status = :dead
|
206
299
|
end
|
207
300
|
end
|
208
301
|
end
|
302
|
+
|
303
|
+
# A new gazelle is ready to accept commands
|
304
|
+
def accept_gazella(gazelle)
|
305
|
+
#puts "gazelle #{@gazella.size} signal port ready"
|
306
|
+
|
307
|
+
# add the signal port to the set
|
308
|
+
@gazella.add gazelle
|
309
|
+
gazelle.finally do
|
310
|
+
@gazella.delete gazelle
|
311
|
+
@waiting_gazelle -= 1
|
312
|
+
@gazelle_count -= 1
|
313
|
+
end
|
314
|
+
|
315
|
+
@gazelle_count += 1
|
316
|
+
if @waiting_gazelle == @gazelle_count
|
317
|
+
start_bindings
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def start_bindings
|
322
|
+
@status = :running
|
323
|
+
|
324
|
+
# Start any bindings that are already present
|
325
|
+
@bindings.each_key do |key|
|
326
|
+
start(key)
|
327
|
+
end
|
328
|
+
|
329
|
+
# Inform any listeners that we have completed loading
|
330
|
+
@gazelles_loaded.resolve(@gazelle_count)
|
331
|
+
end
|
332
|
+
|
333
|
+
# A new gazelle loop is ready to accept sockets
|
334
|
+
# We start the server as soon as the first gazelle is ready
|
335
|
+
def accept_handler(handler)
|
336
|
+
#puts "gazelle #{@handlers.size} loop running"
|
337
|
+
|
338
|
+
@handlers.add handler # add the new gazelle to the set
|
339
|
+
@select_handler.rewind # update the enumerator with the new gazelle
|
340
|
+
|
341
|
+
# If a gazelle dies or shuts down we update the set
|
342
|
+
handler.finally do
|
343
|
+
@handlers.delete handler
|
344
|
+
@select_handler.rewind
|
345
|
+
|
346
|
+
if @status == :running && @handlers.size == 0
|
347
|
+
# I assume if we made it here something went quite wrong
|
348
|
+
squash
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def log(*args)
|
354
|
+
msg = ''
|
355
|
+
if args[0].respond_to? :backtrace
|
356
|
+
msg << "unhandled exception: #{args[0]}\n #{args[0].backtrace}"
|
357
|
+
else
|
358
|
+
msg << "unhandled exception: #{args}"
|
359
|
+
end
|
360
|
+
@logger.error msg
|
361
|
+
end
|
209
362
|
end
|
210
363
|
end
|