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.
@@ -9,17 +9,28 @@ module SpiderGazelle
9
9
  REQUEST_METHOD = 'REQUEST_METHOD'.freeze # GET, POST, etc
10
10
 
11
11
 
12
- def initialize(app, options)
13
- @gazelle = Libuv::Loop.new
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
- @app = app
19
- @options = options
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
- p "Log called: #{level}: #{errorid}\n#{error.message}\n#{error.backtrace.join("\n")}\n"
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
- # 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)
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
- # 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)
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(Request.new(@app, @options))
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, :response
65
+ attr_accessor :env, :url, :header, :body, :keep_alive, :upgrade, :deferred
66
+ attr_reader :hijacked, :response
71
67
 
72
68
 
73
- def initialize(app, options)
74
- @app, @options = app, options
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
- # 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
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
- # 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)
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
- resp << key
139
- resp << COLON_SPACE
140
- resp << value
141
- resp << CRLF
142
- end
143
- resp << CRLF
144
- resp << resp_body
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
- # TODO:: streaming responses (using async and a queue object?)
147
- @response = resp
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
- 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
13
+ USE_TLS = 'T'.freeze
14
+ NO_TLS = 'F'.freeze
15
15
  KILL_GAZELLE = 'k'.freeze
16
16
 
17
- STATES = [:dead, :reanimating, :running, :squashing]
18
- MODES = [:thread, :process] # TODO:: implement process
17
+ STATES = [:reanimating, :running, :squashing, :dead]
18
+ MODES = [:thread, :process, :no_ipc] # TODO:: implement clustering using processes
19
19
 
20
20
 
21
- def initialize(app, options = {})
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
- # 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)
24
+ def initialize
25
+ # Threaded mode by default
26
+ @status = :reanimating
27
+ @bindings = {
28
+ # id => [bind1, bind2]
29
+ }
32
30
 
33
- # Manage the set of Gazelle signal pipes
34
- @gazella = Set.new
35
- @accept_gazella = method(:accept_gazella)
31
+ mode = ENV['SG_MODE'] || :thread
32
+ @mode = mode.to_sym
36
33
 
37
- # Connection management
38
- @accept_connection = method(:accept_connection)
39
- @new_connection = method(:new_connection)
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
- # Update the base request environment
45
- Request::PROTO_ENV[Request::SERVER_PORT] = @options[:port]
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
- # Start the server (this method blocks until completion)
49
- def run
50
- return unless @status == :dead
51
- @status = :reanimating
52
- @spider.run &method(:reanimate)
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
- # If the spider is running we will request to squash it (thread safe)
56
- def stop
57
- @squash.call
58
- end
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
- protected
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
- # There is a new connection pending
65
- # We accept it
66
- def new_connection(server)
67
- server.accept @accept_connection
68
- end
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
- # 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)
107
+ defer.promise
77
108
  end
78
109
 
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
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
- # 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}"
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
- @loops.add loop # add the new gazelle to the set
107
- @select_loop.rewind # update the enumerator with the new gazelle
162
+ defer.promise
163
+ end
108
164
 
109
- # If a gazelle dies or shuts down we update the set
110
- loop.finally do
111
- @loops.delete loop
112
- @select_loop.rewind
165
+ # Signals spider gazelle to shutdown gracefully
166
+ def shutdown
167
+ @signal_squash.call
168
+ end
113
169
 
114
- if @loops.size == 0
115
- @tcp.close
116
- end
117
- end
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
- 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'
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
- @squash = @spider.async do
132
- squash
133
- end
210
+ @signal_squash = @web.async @squash
134
211
 
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)
212
+ # Link up the loops logger
213
+ logger.progress method(:log)
145
214
 
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)
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
- # Launch the gazelle here
159
- @options[:gazelle_count].times do
160
- Thread.new do
161
- gazelle = Gazelle.new(@app, @options)
162
- gazelle.run
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
- @spider.signal(:INT) do
168
- squash
169
- end
253
+ @web.signal :INT, @squash
170
254
 
171
255
  # Update state only once the event loop is ready
172
- @status = :running
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
- @tcp.close
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
- # 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
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
- begin
200
- @signaller.close
201
- File.unlink(SIGNAL_PIPE)
202
- rescue
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