spider-gazelle 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,69 @@
1
+ require 'libuv'
2
+ require 'spider-gazelle/logger'
3
+
4
+
5
+ module SpiderGazelle
6
+ class Reactor
7
+ include Singleton
8
+ attr_reader :thread
9
+
10
+
11
+ def initialize
12
+ @thread = ::Libuv::Loop.default
13
+ @logger = Logger.instance
14
+ @running = false
15
+ @shutdown = method(:shutdown)
16
+ @shutdown_called = false
17
+ end
18
+
19
+ def run(&block)
20
+ if @running
21
+ @thread.schedule block
22
+ else
23
+ @running = true
24
+ @thread.run { |logger|
25
+ logger.progress method(:log)
26
+
27
+ # Listen for the signal to shutdown
28
+ @thread.signal :INT, @shutdown
29
+
30
+ block.call
31
+ }
32
+ end
33
+ end
34
+
35
+ def shutdown(_ = nil)
36
+ if @shutdown_called
37
+ @logger.warn "Shutdown called twice! Callstack:\n#{caller.join("\n")}"
38
+ return
39
+ end
40
+
41
+ @thread.schedule do
42
+ return if @shutdown_called
43
+ @shutdown_called = true
44
+
45
+ # Signaller will manage the shutdown of the gazelles
46
+ signaller = Signaller.instance.shutdown
47
+ signaller.finally do
48
+ @thread.stop
49
+ # New line on exit to avoid any ctrl-c characters
50
+ # We check for pipe as we only want the master process to print this
51
+ puts "\nSpider-Gazelle leaps through the veldt\n" unless @logger.pipe
52
+ end
53
+ end
54
+ end
55
+
56
+ # This is an unhandled error on the Libuv Event loop
57
+ def log(level, errorid, error)
58
+ msg = ''
59
+ if error.respond_to?(:backtrace)
60
+ msg << "unhandled exception: #{error.message} (#{level} - #{errorid})"
61
+ backtrace = error.backtrace
62
+ msg << "\n#{backtrace.join("\n")}" if backtrace
63
+ else
64
+ msg << "unhandled exception: #{args}"
65
+ end
66
+ @logger.error msg
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,214 @@
1
+ require 'libuv'
2
+
3
+
4
+ module SpiderGazelle
5
+ class Signaller
6
+ include Singleton
7
+
8
+
9
+ attr_reader :thread, :pipe
10
+ attr_accessor :gazelle
11
+
12
+
13
+ def initialize
14
+ @thread = ::Libuv::Loop.default
15
+ @logger = Logger.instance
16
+
17
+ # This is used to check if an instance of spider-gazelle is already running
18
+ @is_master = false
19
+ @is_client = false
20
+ @is_connected = false
21
+ @client_check = @thread.defer
22
+ @validated = [] # Set requires more processing
23
+ @validating = {}
24
+ end
25
+
26
+ def request(options)
27
+ defer = @thread.defer
28
+
29
+ defer.resolve(true)
30
+
31
+ defer.promise
32
+ end
33
+
34
+ def check
35
+ @thread.next_tick do
36
+ connect_to_sg_master
37
+ end
38
+ @client_check.promise
39
+ end
40
+
41
+ def shutdown
42
+ defer = @thread.defer
43
+
44
+ # Close the SIGNAL_SERVER pipe
45
+ @pipe.close if @is_connected
46
+
47
+ # Request spider or gazelle process to shutdown
48
+ if @gazelle
49
+ @gazelle.shutdown(defer)
50
+ end
51
+
52
+ if defined?(::SpiderGazelle::Spider)
53
+ Spider.instance.shutdown(defer)
54
+ else
55
+ # This must be the master process
56
+ @thread.next_tick do
57
+ defer.resolve(true)
58
+ end
59
+ end
60
+
61
+ defer.promise
62
+ end
63
+
64
+ # ------------------------------
65
+ # Called from the spider process
66
+ # ------------------------------
67
+ def authenticate(password)
68
+ @pipe.write "\x02validate #{password}\x03"
69
+ end
70
+
71
+ def general_failure
72
+ @pipe.write "\x02Signaller general_failure\x03".freeze
73
+ rescue
74
+ ensure
75
+ Reactor.instance.shutdown
76
+ end
77
+
78
+ def self.general_failure(_)
79
+ Reactor.instance.shutdown
80
+ end
81
+
82
+
83
+ protected
84
+
85
+
86
+ def connect_to_sg_master
87
+ @pipe = @thread.pipe :ipc
88
+
89
+ process = method(:process_response)
90
+ @pipe.connect(SIGNAL_SERVER) do |client|
91
+ @is_client = true
92
+ @is_connected = true
93
+
94
+ @logger.verbose "Client connected to SG Master".freeze
95
+
96
+ require 'uv-rays/buffered_tokenizer'
97
+ @parser = ::UV::BufferedTokenizer.new({
98
+ indicator: "\x02",
99
+ delimiter: "\x03"
100
+ })
101
+
102
+ client.progress process
103
+ client.start_read
104
+ @client_check.resolve(true)
105
+ end
106
+
107
+ @pipe.catch do |reason|
108
+ if !@is_client
109
+ @client_check.resolve(false)
110
+ end
111
+ end
112
+
113
+ @pipe.finally do
114
+ if @is_client
115
+ @is_connected = false
116
+ panic!(nil)
117
+ else
118
+ # Assume the role of master
119
+ become_sg_master
120
+ end
121
+ end
122
+ end
123
+
124
+ def become_sg_master
125
+ @is_master = true
126
+ @is_client = false
127
+ @is_connected = true
128
+
129
+ # Load the server request processor here
130
+ require 'spider-gazelle/signaller/signal_parser'
131
+ @pipe = @thread.pipe :ipc
132
+
133
+ begin
134
+ File.unlink SIGNAL_SERVER
135
+ rescue
136
+ end
137
+
138
+ process = method(:process_request)
139
+ @pipe.bind(SIGNAL_SERVER) do |client|
140
+ @logger.verbose { "Client <0x#{client.object_id.to_s(16)}> connection made" }
141
+ @validating[client.object_id] = SignalParser.new
142
+
143
+ client.catch do |error|
144
+ @logger.print_error(error, "Client <0x#{client.object_id.to_s(16)}> connection error")
145
+ end
146
+
147
+ client.finally do
148
+ @validated.delete client
149
+ @validating.delete client.object_id
150
+ @logger.verbose { "Client <0x#{client.object_id.to_s(16)}> disconnected, #{@validated.length} remaining" }
151
+
152
+ # If all the process connections are gone then we want to shutdown
153
+ # This should never happen under normal conditions
154
+ if @validated.length == 0
155
+ Reactor.instance.shutdown
156
+ end
157
+ end
158
+
159
+ client.progress process
160
+ client.start_read
161
+ end
162
+
163
+ # catch server errors
164
+ @pipe.catch method(:panic!)
165
+ @pipe.finally { @is_connected = false }
166
+
167
+ # start listening
168
+ @pipe.listen(INTERNAL_PIPE_BACKLOG)
169
+ end
170
+
171
+ def panic!(reason)
172
+ #@logger.error "Master pipe went missing: #{reason}"
173
+ # Server most likely exited
174
+ # We'll shutdown here
175
+ Reactor.instance.shutdown
176
+ end
177
+
178
+ # The server processes requests here
179
+ def process_request(data, client)
180
+ validated = @validated.include?(client)
181
+ parser = @validating[client.object_id]
182
+
183
+ if validated
184
+ parser.process data
185
+ else
186
+ result = parser.signal(data)
187
+
188
+ case result
189
+ when :validated
190
+ @validated.each do |old|
191
+ old.write "\x02update\x03".freeze
192
+ end
193
+ @validated << client
194
+ if @validated.length > 1
195
+ client.write "\x02wait\x03".freeze
196
+ else
197
+ client.write "\x02ready\x03".freeze
198
+ end
199
+ @logger.verbose { "Client <0x#{client.object_id.to_s(16)}> connection was validated" }
200
+ when :close_connection
201
+ client.close
202
+ @logger.warn "Client <0x#{client.object_id.to_s(16)}> connection was closed due to bad credentials"
203
+ end
204
+ end
205
+ end
206
+
207
+ # The client processes responses here
208
+ def process_response(data, server)
209
+ @parser.extract(data).each do |msg|
210
+ Spider.instance.__send__(msg)
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,66 @@
1
+ require 'uv-rays'
2
+
3
+
4
+ module SpiderGazelle
5
+ class Signaller
6
+ class SignalParser
7
+ def initialize
8
+ @tokenizer = ::UV::BufferedTokenizer.new({
9
+ indicator: "\x02",
10
+ delimiter: "\x03"
11
+ })
12
+ @logger = Logger.instance
13
+ @launchctrl = LaunchControl.instance
14
+ end
15
+
16
+ def process(msg)
17
+ @tokenizer.extract(msg).each do |cmd|
18
+ perform cmd
19
+ end
20
+ end
21
+
22
+ # These are signals that can be sent
23
+ # While the remote client is untrusted
24
+ def signal(msg)
25
+ result = nil
26
+ @tokenizer.extract(msg).each do |request|
27
+ result = check request
28
+ end
29
+ result
30
+ end
31
+
32
+
33
+ protected
34
+
35
+
36
+ def perform(cmd)
37
+ begin
38
+ klass, action, data = cmd.split(' ', 3)
39
+ SpiderGazelle.const_get(klass).__send__(action, data)
40
+ rescue => e
41
+ @logger.print_error(e, 'Error executing command in SignalParser')
42
+ end
43
+ end
44
+
45
+ def check(cmd)
46
+ comp = cmd.split(' ', 2)
47
+ request = comp[0].to_sym
48
+ data = comp[1]
49
+
50
+ case request
51
+ when :validate
52
+ if data == @launchctrl.password
53
+ return :validated
54
+ else
55
+ return :close_connection
56
+ end
57
+ when :reload
58
+ when :Logger
59
+ perform cmd
60
+ end
61
+
62
+ request
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,391 +1,353 @@
1
- require 'spider-gazelle/const'
2
- require 'set'
3
- require 'thread'
4
- require 'logger'
5
- require 'singleton'
6
- require 'fileutils' # mkdir_p
7
- require 'forwardable' # run method
1
+ require 'spider-gazelle/spider/binding' # Holds a reference to a bound port
2
+ require 'securerandom'
3
+
8
4
 
9
5
  module SpiderGazelle
10
- class Spider
11
- include Const
12
- include Singleton
13
-
14
- STATES = [:reanimating, :running, :squashing, :dead]
15
- # TODO:: implement clustering using processes
16
- MODES = [:thread, :process, :no_ipc]
17
- DEFAULT_OPTIONS = {
18
- Host: "0.0.0.0",
19
- Port: 8080,
20
- Verbose: false,
21
- tls: false,
22
- optimize_for_latency: true,
23
- backlog: 1024
24
- }
25
-
26
- attr_reader :state, :mode, :threads, :logger
27
- def in_mode?(mode)
28
- mode.to_sym == @mode
29
- end
6
+ class Spider
7
+ include Singleton
30
8
 
31
- extend Forwardable
32
- def_delegators :@web, :run
33
9
 
34
- def self.run(app, options = {})
35
- options = DEFAULT_OPTIONS.merge options
10
+ # This allows applications to recieve a callback once the server has
11
+ # completed loading the applications. Port binding is in progress
12
+ def loaded
13
+ @load_complete.promise
14
+ end
36
15
 
37
- ENV['RACK_ENV'] = options[:environment].to_s if options[:environment]
16
+ # Applications can query the availability of various modes to share resources
17
+ def in_mode?(mode)
18
+ !!@loading[mode.to_sym]
19
+ end
38
20
 
39
- puts "Look out! Here comes Spider-Gazelle #{SPIDER_GAZELLE_VERSION}!"
40
- puts "* Environment: #{ENV['RACK_ENV']} on #{RUBY_ENGINE || 'ruby'} #{RUBY_VERSION}"
21
+ attr_reader :logger, :threads
41
22
 
42
- server = instance
43
- server.run do |logger|
44
- logger.progress server.method(:log)
45
- server.loaded.then do
46
- puts "* Loading: #{app}"
47
23
 
48
- caught = proc { |e| puts("#{e.message}\n#{e.backtrace.join("\n")}") unless e.backtrace.nil? }
49
- server.load(app, options).catch(caught)
50
- .finally { Process.kill('INT', 0) } # Terminate the application if the TCP binding is lost
24
+ def initialize
25
+ @logger = Logger.instance
26
+ @signaller = Signaller.instance
27
+ @thread = @signaller.thread
51
28
 
52
- puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
53
- end
54
- end
55
- end
29
+ # Gazelle pipe connection management
30
+ @gazelles = {
31
+ # process: [],
32
+ # thread: [],
33
+ # no_ipc: gazelle_instance
34
+ }
35
+ @counts = {
36
+ # process: number
37
+ # thread: number
38
+ }
39
+ @loading = {} # mode => load defer
40
+ @bindings = {} # port => binding
41
+ @iterators = {} # mode => gazelle round robin iterator
42
+ @iterator_source = {} # mode => gazelle pipe array (iterator source)
56
43
 
57
- def initialize
58
- # Threaded mode by default
59
- reanimating!
60
- @bindings = {}
61
- # @bindings = {
62
- # id => [bind1, bind2]
63
- # }
64
-
65
- # Single reactor in development
66
- if ENV['RACK_ENV'].to_sym == :development
67
- @mode = :no_ipc
68
- else
69
- mode = ENV['SG_MODE'] || :thread
70
- @mode = mode.to_sym
71
- end
72
-
73
- @delegate = method(no_ipc? ? :direct_delegate : :delegate)
74
- @squash = method(:squash)
75
-
76
- log_path = ENV['SG_LOG'] || File.expand_path('log/sg.log', Dir.pwd)
77
- dirname = File.dirname(log_path)
78
- FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
79
- @logger = ::Logger.new(log_path.to_s, 10, 4194304)
80
-
81
- # Keep track of the loading process
82
- @waiting_gazelle = 0
83
- @gazelle_count = 0
84
-
85
- # Spider always runs on the default loop
86
- @web = ::Libuv::Loop.default
87
- @gazelles_loaded = @web.defer
88
-
89
- # Start the server
90
- reanimate
91
- end
44
+ @password = SecureRandom.hex
92
45
 
93
- # Modes
94
- def thread?
95
- @mode == :thread
96
- end
97
- def process?
98
- @mode == :process
99
- end
100
- def no_ipc?
101
- @mode == :no_ipc
102
- end
46
+ @running = true
47
+ @loaded = false
48
+ @bound = false
103
49
 
104
- # Statuses
105
- def reanimating?
106
- @status == :reanimating
107
- end
108
- def reanimating!
109
- @status = :reanimating
110
- end
111
- def running?
112
- @status == :running
113
- end
114
- def running!
115
- @status = :running
116
- end
117
- def squashing?
118
- @status == :squashing
119
- end
120
- def squashing!
121
- @status = :squashing
122
- end
123
- def dead?
124
- @status == :dead
125
- end
126
- def dead!
127
- @status = :dead
128
- end
50
+ @load_complete = @thread.defer
51
+ end
129
52
 
130
- # Provides a promise that resolves when we are read to start binding applications
131
- #
132
- # @return [::Libuv::Q::Promise] that indicates when the gazelles are loaded
133
- def loaded
134
- @gazelles_loaded.promise unless @gazelles_loaded.nil?
135
- end
53
+ def run!(options)
54
+ @options = options
55
+ @logger.verbose { "Spider Pid: #{Process.pid} started" }
56
+ if options[0][:isolate]
57
+ ready
58
+ else
59
+ @signaller.authenticate(options[0][:spider])
60
+ end
61
+ end
136
62
 
137
- # A thread safe method for loading and binding rack apps. The app can be pre-loaded or a rackup file
138
- #
139
- # @param app [String, Object] rackup filename or rack app
140
- # @param ports [Hash, Array] binding config or array of binding config
141
- # @return [::Libuv::Q::Promise] resolves once the app is loaded (and bound if SG is running)
142
- def load(app, ports = [])
143
- defer = @web.defer
63
+ # Load gazelles and make the required bindings
64
+ def ready
65
+ start_gazelle_server
66
+ load_promise = load_applications
67
+ load_promise.then do
68
+ # Check a shutdown request didn't occur as we were loading
69
+ if @running
70
+ @logger.verbose "All gazelles running".freeze
71
+
72
+ # This happends on the master thread so we don't need to check
73
+ # for the shutdown events here
74
+ bind_application_ports
75
+ else
76
+ @logger.warn "A shutdown event occured while loading".freeze
77
+ perform_shutdown
78
+ end
79
+ end
144
80
 
145
- ports = [ports] if ports.is_a?(Hash)
146
- ports << {} if ports.empty?
81
+ # Provide applications with a load complete callback
82
+ @load_complete.resolve(load_promise)
83
+ end
147
84
 
148
- @web.schedule do
149
- begin
150
- app_id = AppStore.load app
151
- bindings = @bindings[app_id] ||= []
85
+ # Load gazelles and wait for the bindings to be sent
86
+ def wait
87
+
88
+ end
152
89
 
153
- ports.each { |options| bindings << Binding.new(@web, @delegate, app_id, options) }
90
+ # Pass existing bindings to the master process
91
+ def update
154
92
 
155
- defer.resolve(running? ? start(app, app_id) : true)
156
- rescue Exception => e
157
- defer.reject e
158
93
  end
159
- end
160
94
 
161
- defer.promise
162
- end
95
+ # Load a second application (requires a new port binding)
96
+ def load
163
97
 
164
- # Starts the app specified. It must already be loaded
165
- #
166
- # @param app [String, Object] rackup filename or the rack app instance
167
- # @return [::Libuv::Q::Promise] resolves once the app is bound to the port
168
- def start(app, app_id = nil)
169
- defer = @web.defer
170
- app_id ||= AppStore.lookup app
171
-
172
- if !app_id.nil? && running?
173
- @web.schedule do
174
- bindings = @bindings[app_id] ||= []
175
- starting = []
176
-
177
- bindings.each { |binding| starting << binding.bind }
178
- defer.resolve ::Libuv::Q.all(@web, *starting)
179
98
  end
180
- elsif app_id.nil?
181
- defer.reject 'application not loaded'
182
- else
183
- defer.reject 'server not running'
184
- end
185
99
 
186
- defer.promise
187
- end
100
+ # Shutdown after current requests have completed
101
+ def shutdown(finished)
102
+ @shutdown_defer = finished
188
103
 
189
- # Stops the app specified. If loaded
190
- #
191
- # @param app [String, Object] rackup filename or the rack app instance
192
- # @return [::Libuv::Q::Promise] resolves once the app is no longer bound to the port
193
- def stop(app, app_id = nil)
194
- defer = @web.defer
195
- app_id ||= AppStore.lookup app
196
-
197
- if !app_id.nil?
198
- @web.schedule do
199
- bindings = @bindings[app_id]
200
- closing = []
201
-
202
- bindings.each do |binding|
203
- result = binding.unbind
204
- closing << result unless result.nil?
205
- end unless bindings.nil?
206
-
207
- defer.resolve ::Libuv::Q.all(@web, *closing)
104
+ @logger.verbose { "Spider Pid: #{Process.pid} shutting down" }
105
+
106
+ if @loaded
107
+ perform_shutdown
108
+ else
109
+ @running = false
110
+ end
208
111
  end
209
- else
210
- defer.reject 'application not loaded'
211
- end
212
112
 
213
- defer.promise
214
- end
215
113
 
216
- # Signals spider gazelle to shutdown gracefully
217
- def shutdown
218
- @signal_squash.call
219
- end
114
+ protected
220
115
 
221
- protected
222
116
 
223
- # Called from the binding for sending to gazelles
224
- def delegate(client, tls, port, app_id)
225
- indicator = tls ? USE_TLS : NO_TLS
226
- @select_handler.next.write2(client, "#{indicator} #{port} #{app_id}").finally do
227
- client.close
228
- end
229
- end
117
+ # This starts the server the gazelles will be connecting to
118
+ def start_gazelle_server
119
+ @pipe_file = "#{SPIDER_SERVER}#{Process.pid}"
120
+ @logger.verbose { "Spider server starting on #{@pipe_file}" }
230
121
 
231
- def direct_delegate(client, tls, port, app_id)
232
- indicator = tls ? USE_TLS : NO_TLS
233
- @gazelle.__send__(:new_connection, "#{indicator} #{port} #{app_id}", client)
234
- end
122
+ @pipe = @thread.pipe :ipc
123
+ begin
124
+ File.unlink @pipe_file
125
+ rescue
126
+ end
235
127
 
236
- # Triggers the creation of gazelles
237
- def reanimate
238
-
239
- # Manage the set of Gazelle socket listeners
240
- @threads = Set.new
241
-
242
- if thread?
243
- cpus = ::Libuv.cpu_count || 1
244
- cpus.times { @threads << Libuv::Loop.new }
245
- elsif no_ipc?
246
- # TODO:: need to perform process mode as well
247
- @threads << @web
248
- end
249
-
250
- @handlers = Set.new
251
- @select_handler = @handlers.cycle # provides a looping enumerator for our round robin
252
- @accept_handler = method :accept_handler
253
-
254
- # Manage the set of Gazelle signal pipes
255
- @gazella = Set.new
256
- @accept_gazella = method :accept_gazella
257
-
258
- # Create a function for stopping the spider from another thread
259
- @signal_squash = @web.async @squash
260
-
261
- if no_ipc?
262
- @gazelle = Gazelle.new @web, @logger, @mode
263
- @gazelle_count = 1
264
- start_bindings
265
- else
266
- # Bind the pipe for sending sockets to gazelle
267
- begin
268
- File.unlink DELEGATE_PIPE
269
- rescue
270
- end
271
- @delegator = @web.pipe :with_socket_support
272
- @delegator.bind(DELEGATE_PIPE, @accept_handler)
273
- @delegator.listen INTERNAL_PIPE_BACKLOG
274
-
275
- # Bind the pipe for communicating with gazelle
276
- begin
277
- File.unlink SIGNAL_PIPE
278
- rescue
128
+ shutdown = false
129
+ check = method(:check_credentials)
130
+ @pipe.bind(@pipe_file) do |client|
131
+ @logger.verbose { "Gazelle <0x#{client.object_id.to_s(16)}> connection made" }
132
+
133
+ # Shutdown if there is an error with any of the gazelles
134
+ client.catch do |error|
135
+ begin
136
+ @logger.print_error(error, "Gazelle <0x#{client.object_id.to_s(16)}> connection error")
137
+ rescue
138
+ end
139
+ end
140
+
141
+ # Client closed gracefully
142
+ client.finally do
143
+ begin
144
+ @logger.verbose { "Gazelle <0x#{client.object_id.to_s(16)}> disconnected" }
145
+ rescue
146
+ ensure
147
+ @gazelles.delete client
148
+ if !shutdown
149
+ shutdown = true
150
+ @signaller.general_failure
151
+ end
152
+ end
153
+ end
154
+
155
+ client.progress check
156
+ client.start_read
157
+ end
158
+
159
+ # catch server errors
160
+ @pipe.catch do |error|
161
+ @logger.print_error(error)
162
+ @signaller.general_failure
163
+ end
164
+
165
+ # start listening
166
+ @pipe.listen(INTERNAL_PIPE_BACKLOG)
279
167
  end
280
- @signaller = @web.pipe
281
- @signaller.bind(SIGNAL_PIPE, @accept_gazella)
282
- @signaller.listen INTERNAL_PIPE_BACKLOG
283
-
284
- # Launch the gazelle here
285
- @threads.each do |thread|
286
- Thread.new { Gazelle.new(thread, @logger, @mode).run }
287
- @waiting_gazelle += 1
168
+
169
+ def check_credentials(data, gazelle)
170
+ password, mode = data.split(' ', 2)
171
+ mode_sym = mode.to_sym
172
+
173
+ if password == @password && MODES.include?(mode_sym)
174
+ @gazelles[mode_sym] ||= []
175
+ gazelles = @gazelles[mode_sym]
176
+ gazelles << gazelle
177
+ @logger.verbose { "Gazelle <0x#{gazelle.object_id.to_s(16)}> connection was validated" }
178
+
179
+ # All the gazelles have loaded. Lets start processing requests
180
+ if gazelles.length == @counts[mode_sym]
181
+ @logger.verbose { "#{mode.capitalize} gazelles are ready" }
182
+
183
+ @iterator_source[mode_sym] = gazelles
184
+ @iterators[mode_sym] = gazelles.cycle
185
+ @loading[mode_sym].resolve(true)
186
+ end
187
+ else
188
+ @logger.warn "Gazelle <0x#{gazelle.object_id.to_s(16)}> connection closed due to bad credentials"
189
+ gazelle.close
190
+ end
288
191
  end
289
- end
290
192
 
291
- # Signal gazelle death here
292
- @web.signal :INT, @squash
193
+ def load_applications
194
+ loaded = []
195
+ @logger.info "Environment: #{ENV['RACK_ENV']} on #{RUBY_ENGINE || 'ruby'} #{RUBY_VERSION}"
293
196
 
294
- # Update state only once the event loop is ready
295
- @gazelles_loaded.promise
296
- end
197
+ # Load the different types of gazelles required
198
+ @options.each do |app|
199
+ @logger.info "Loading: #{app[:rackup]}" if app[:rackup]
297
200
 
298
- # Triggers a shutdown of the gazelles.
299
- # We ensure the process is running here as signals can be called multiple times
300
- def squash(*args)
301
- if running?
302
- # Update the state and close the socket
303
- squashing!
304
- @bindings.each { |key, val| stop(key) }
305
-
306
- if no_ipc?
307
- @web.stop
308
- dead!
309
- else
310
- # Signal all the gazelle to shutdown
311
- promises = @gazella.map { |gazelle| gazelle.write(KILL_GAZELLE) }
312
-
313
- # Once the signal has been sent we can stop the spider loop
314
- @web.finally(*promises).finally do
315
- # TODO:: need a better system for ensuring these are cleaned up
316
- # Especially when we implement live migrations and process clusters
317
- begin
318
- @delegator.close
319
- File.unlink DELEGATE_PIPE
320
- rescue
201
+ mode = app[:mode]
202
+ loaded << load_gazelles(mode, app[:count], @options) unless @loading[mode]
321
203
  end
322
- begin
323
- @signaller.close
324
- File.unlink SIGNAL_PIPE
325
- rescue
204
+
205
+ # Return a promise that resolves when all the gazelles are loaded
206
+ # Gazelles will only load the applications that apply to them based on the application type
207
+ @thread.all(*loaded)
208
+ end
209
+
210
+
211
+ def load_gazelles(mode, count, options)
212
+ defer = @thread.defer
213
+ @loading[mode] = defer
214
+
215
+ pass = options[0][:spider]
216
+
217
+ if mode == :no_ipc
218
+ # Provide the password to the gazelle instance
219
+ options[0][:gazelle] = @password
220
+
221
+ # Start the gazelle
222
+ require 'spider-gazelle/gazelle'
223
+ gaz = ::SpiderGazelle::Gazelle.new(@thread, mode).run!(options)
224
+ @gazelles[:no_ipc] = gaz
225
+
226
+ # Setup the round robin
227
+ @iterator_source[mode] = gaz
228
+ @iterators[mode] = gaz
229
+ defer.resolve(true)
230
+ else
231
+ require 'thread'
232
+
233
+ # Provide the password to the gazelle instance
234
+ options[0][:gazelle] = @password
235
+ options[0][:gazelle_ipc] = @pipe_file
236
+
237
+ count = @counts[mode] = count || ::Libuv.cpu_count || 1
238
+ @logger.info "#{mode.to_s.capitalize} count: #{count}"
239
+
240
+ if mode == :thread
241
+ require 'spider-gazelle/gazelle'
242
+ reactor = Reactor.instance
243
+
244
+ @threads = []
245
+ count.times do
246
+ thread = ::Libuv::Loop.new
247
+ @threads << thread
248
+
249
+ Thread.new { load_gazelle_thread(reactor, thread, mode, options) }
250
+ end
251
+ else
252
+ # Remove the spider option
253
+ args = LaunchControl.instance.args - ['-s', pass]
254
+
255
+ # Build the command with the gazelle option
256
+ args = [EXEC_NAME, '-g', @password, '-f', @pipe_file] + args
257
+
258
+ @logger.verbose { "Launching #{count} gazelle processes" }
259
+ count.times do
260
+ Thread.new { launch_gazelle(args) }
261
+ end
262
+ end
326
263
  end
327
264
 
328
- @web.stop
329
- dead!
330
- end
265
+ defer.promise
331
266
  end
332
- end
333
- end
334
267
 
335
- # A new gazelle is ready to accept commands
336
- def accept_gazella(gazelle)
337
- # add the signal port to the set
338
- @gazella.add gazelle
339
- gazelle.finally do
340
- @gazella.delete gazelle
341
- @waiting_gazelle -= 1
342
- @gazelle_count -= 1
343
- end
344
-
345
- @gazelle_count += 1
346
- start_bindings if @waiting_gazelle == @gazelle_count
347
- end
268
+ def load_gazelle_thread(reactor, thread, mode, options)
269
+ thread.run do |logger|
270
+ # Log any unhandled errors
271
+ logger.progress reactor.method(:log)
348
272
 
349
- def start_bindings
350
- running!
273
+ # Start the gazelle
274
+ ::SpiderGazelle::Gazelle.new(thread, :thread).run!(options)
275
+ end
276
+ end
351
277
 
352
- # Start any bindings that are already present
353
- @bindings.each { |key, val| start(key) }
278
+ def launch_gazelle(cmd)
279
+ # Wait for the process to close
280
+ result = system(*cmd)
281
+
282
+ # Kill the spider if a process exits unexpectedly
283
+ if @running
284
+ @thread.schedule do
285
+ if result
286
+ @logger.verbose "Gazelle process exited with exit status 0".freeze
287
+ else
288
+ @logger.error "Gazelle process exited unexpectedly".freeze
289
+ end
290
+
291
+ @signaller.general_failure
292
+ end
293
+ end
294
+ end
354
295
 
355
- # Inform any listeners that we have completed loading
356
- @gazelles_loaded.resolve @gazelle_count
357
- end
296
+ def bind_application_ports
297
+ @bound = true
298
+ @loaded = true
358
299
 
359
- # A new gazelle loop is ready to accept sockets.
360
- # We start the server as soon as the first gazelle is ready
361
- def accept_handler(handler)
362
- # Add the new gazelle to the set
363
- @handlers.add handler
364
- # Update the enumerator with the new gazelle
365
- @select_handler.rewind
366
-
367
- # If a gazelle dies or shuts down we update the set
368
- handler.finally do
369
- @handlers.delete handler
370
- @select_handler.rewind
371
-
372
- # I assume if we made it here something went quite wrong
373
- squash if running? && @handlers.empty?
374
- end
375
- end
300
+ @options.each_index do |id|
301
+ options = @options[id]
302
+ iterator = @iterators[options[:mode]]
376
303
 
377
- # TODO FIXME Review this method.
378
- def log(*args)
379
- msg = ''
380
- err = args[-1]
381
- if err && err.respond_to?(:backtrace)
382
- msg << "unhandled exception: #{err.message} (#{args[0..-2]})"
383
- msg << "\n#{err.backtrace.join("\n")}" if err.backtrace
384
- else
385
- msg << "unhandled exception: #{args}"
386
- end
387
- @logger.error msg
388
- ::Libuv::Q.reject @web, msg
304
+ binding = @bindings[options[:port]] = Binding.new(iterator, id.to_s, options)
305
+ binding.bind
306
+ end
307
+ end
308
+
309
+
310
+ # -------------------
311
+ # Shutdown Management
312
+ # -------------------
313
+ def perform_shutdown
314
+ if @bound
315
+ # Unbind any ports we are bound to
316
+ ports = []
317
+ @bindings.each do |port, binding|
318
+ ports << binding.unbind
319
+ end
320
+
321
+ # Shutdown once the ports are all closed
322
+ @thread.finally(*ports).then do
323
+ shutdown_gazelles
324
+ end
325
+ else
326
+ shutdown_gazelles
327
+ end
328
+ end
329
+
330
+ def shutdown_gazelles
331
+ @bound = false
332
+ promises = []
333
+
334
+ @iterator_source.each do |mode, gazelles|
335
+ if mode == :no_ipc
336
+ # itr is a gazelle in no_ipc mode
337
+ defer = @thread.defer
338
+ gazelles.shutdown(defer)
339
+ promises << defer.promise
340
+
341
+ else
342
+ # End communication with the gazelle threads / processes
343
+ gazelles.each do |gazelle|
344
+ promises << gazelle.close
345
+ end
346
+ end
347
+ end
348
+
349
+ # Finish shutdown after all signals have been sent
350
+ @shutdown_defer.resolve(@thread.finally(*promises))
351
+ end
389
352
  end
390
- end
391
353
  end