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.
@@ -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