spider-gazelle 0.1.0 → 0.1.1
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 +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
|