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.
- checksums.yaml +4 -4
- data/bin/sg +1 -64
- data/lib/rack/handler/spider-gazelle.rb +17 -26
- data/lib/rack/lock_patch.rb +27 -27
- data/lib/spider-gazelle.rb +165 -16
- data/lib/spider-gazelle/gazelle.rb +151 -134
- data/lib/spider-gazelle/gazelle/app_store.rb +86 -0
- data/lib/spider-gazelle/gazelle/http1.rb +496 -0
- data/lib/spider-gazelle/gazelle/request.rb +155 -0
- data/lib/spider-gazelle/logger.rb +122 -0
- data/lib/spider-gazelle/options.rb +213 -0
- data/lib/spider-gazelle/reactor.rb +69 -0
- data/lib/spider-gazelle/signaller.rb +214 -0
- data/lib/spider-gazelle/signaller/signal_parser.rb +66 -0
- data/lib/spider-gazelle/spider.rb +305 -343
- data/lib/spider-gazelle/spider/binding.rb +80 -0
- data/lib/spider-gazelle/upgrades/websocket.rb +92 -88
- data/spec/http1_spec.rb +173 -0
- data/spec/rack_lock_spec.rb +97 -97
- data/spider-gazelle.gemspec +6 -6
- metadata +24 -17
- data/lib/spider-gazelle/app_store.rb +0 -64
- data/lib/spider-gazelle/binding.rb +0 -53
- data/lib/spider-gazelle/connection.rb +0 -371
- data/lib/spider-gazelle/const.rb +0 -206
- data/lib/spider-gazelle/request.rb +0 -103
@@ -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/
|
2
|
-
require '
|
3
|
-
|
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
|
-
|
11
|
-
|
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
|
-
|
35
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
24
|
+
def initialize
|
25
|
+
@logger = Logger.instance
|
26
|
+
@signaller = Signaller.instance
|
27
|
+
@thread = @signaller.thread
|
51
28
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
105
|
-
|
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
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
-
|
146
|
-
|
81
|
+
# Provide applications with a load complete callback
|
82
|
+
@load_complete.resolve(load_promise)
|
83
|
+
end
|
147
84
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
85
|
+
# Load gazelles and wait for the bindings to be sent
|
86
|
+
def wait
|
87
|
+
|
88
|
+
end
|
152
89
|
|
153
|
-
|
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
|
-
|
162
|
-
|
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
|
-
|
187
|
-
|
100
|
+
# Shutdown after current requests have completed
|
101
|
+
def shutdown(finished)
|
102
|
+
@shutdown_defer = finished
|
188
103
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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
|
-
|
217
|
-
def shutdown
|
218
|
-
@signal_squash.call
|
219
|
-
end
|
114
|
+
protected
|
220
115
|
|
221
|
-
protected
|
222
116
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
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
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
122
|
+
@pipe = @thread.pipe :ipc
|
123
|
+
begin
|
124
|
+
File.unlink @pipe_file
|
125
|
+
rescue
|
126
|
+
end
|
235
127
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
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
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
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
|
-
|
292
|
-
|
193
|
+
def load_applications
|
194
|
+
loaded = []
|
195
|
+
@logger.info "Environment: #{ENV['RACK_ENV']} on #{RUBY_ENGINE || 'ruby'} #{RUBY_VERSION}"
|
293
196
|
|
294
|
-
|
295
|
-
|
296
|
-
|
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
|
-
|
299
|
-
|
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
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
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
|
-
|
329
|
-
dead!
|
330
|
-
end
|
265
|
+
defer.promise
|
331
266
|
end
|
332
|
-
end
|
333
|
-
end
|
334
267
|
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
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
|
-
|
350
|
-
|
273
|
+
# Start the gazelle
|
274
|
+
::SpiderGazelle::Gazelle.new(thread, :thread).run!(options)
|
275
|
+
end
|
276
|
+
end
|
351
277
|
|
352
|
-
|
353
|
-
|
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
|
-
|
356
|
-
|
357
|
-
|
296
|
+
def bind_application_ports
|
297
|
+
@bound = true
|
298
|
+
@loaded = true
|
358
299
|
|
359
|
-
|
360
|
-
|
361
|
-
|
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
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
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
|