spider-gazelle 1.2.0 → 2.0.0

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