spider-gazelle 0.1.6 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +7 -0
- data/lib/rack/handler/spider-gazelle.rb +30 -32
- data/lib/spider-gazelle.rb +4 -6
- data/lib/spider-gazelle/app_store.rb +48 -53
- data/lib/spider-gazelle/binding.rb +48 -54
- data/lib/spider-gazelle/connection.rb +286 -322
- data/lib/spider-gazelle/const.rb +198 -0
- data/lib/spider-gazelle/gazelle.rb +121 -139
- data/lib/spider-gazelle/request.rb +95 -141
- data/lib/spider-gazelle/spider.rb +335 -351
- data/lib/spider-gazelle/upgrades/websocket.rb +88 -98
- data/spider-gazelle.gemspec +8 -3
- metadata +3 -4
- data/lib/spider-gazelle/error.rb +0 -16
- data/lib/spider-gazelle/version.rb +0 -3
@@ -1,151 +1,105 @@
|
|
1
|
+
require 'spider-gazelle/const'
|
1
2
|
require 'stringio'
|
2
3
|
|
3
|
-
|
4
4
|
module SpiderGazelle
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze
|
41
|
-
SERVER = 'SpiderGazelle'.freeze
|
42
|
-
REMOTE_ADDR = 'REMOTE_ADDR'.freeze
|
43
|
-
|
44
|
-
|
45
|
-
#
|
46
|
-
# TODO:: Add HTTP headers to the env and capitalise them and prefix them with HTTP_
|
47
|
-
# convert - signs to underscores
|
48
|
-
# => copy puma with a const file
|
49
|
-
#
|
50
|
-
PROTO_ENV = {
|
51
|
-
'rack.version'.freeze => ::Rack::VERSION, # Should be an array of integers
|
52
|
-
'rack.errors'.freeze => $stderr, # An error stream that supports: puts, write and flush
|
53
|
-
'rack.multithread'.freeze => true, # can the app be simultaneously invoked by another thread?
|
54
|
-
'rack.multiprocess'.freeze => false, # will the app be simultaneously be invoked in a separate process?
|
55
|
-
'rack.run_once'.freeze => false, # this isn't CGI so will always be false
|
56
|
-
|
57
|
-
'SCRIPT_NAME'.freeze => ENV['SCRIPT_NAME'] || EMPTY, # The virtual path of the app base (empty if root)
|
58
|
-
'SERVER_PROTOCOL'.freeze => HTTP_11,
|
59
|
-
|
60
|
-
GATEWAY_INTERFACE => CGI_VER,
|
61
|
-
SERVER_SOFTWARE => SERVER
|
62
|
-
}
|
63
|
-
|
64
|
-
|
65
|
-
attr_accessor :env, :url, :header, :body, :keep_alive, :upgrade, :deferred
|
66
|
-
attr_reader :hijacked, :response
|
67
|
-
|
68
|
-
|
69
|
-
def initialize(connection, app)
|
70
|
-
@app = app
|
71
|
-
@body = ''
|
72
|
-
@header = ''
|
73
|
-
@url = ''
|
74
|
-
@execute = method(:execute)
|
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
|
81
|
-
end
|
82
|
-
|
83
|
-
def execute!
|
84
|
-
@env[CONTENT_LENGTH] = @env.delete(HTTP_CONTENT_LENGTH) || @body.length
|
85
|
-
@env[CONTENT_TYPE] = @env.delete(HTTP_CONTENT_TYPE) || DEFAULT_TYPE
|
86
|
-
@env[REQUEST_URI] = @url.freeze
|
87
|
-
|
88
|
-
# For Rack::Lint on 1.9, ensure that the encoding is always for spec
|
89
|
-
@body.force_encoding('ASCII-8BIT') if @body.respond_to?(:force_encoding)
|
90
|
-
@env[RACK_INPUT] = StringIO.new(@body)
|
91
|
-
|
92
|
-
# Break the request into its components
|
93
|
-
query_start = @url.index('?')
|
94
|
-
if query_start
|
95
|
-
path = @url[0...query_start].freeze
|
96
|
-
@env[PATH_INFO] = path
|
97
|
-
@env[REQUEST_PATH] = path
|
98
|
-
@env[QUERY_STRING] = @url[query_start + 1..-1].freeze
|
99
|
-
else
|
100
|
-
@env[PATH_INFO] = @url
|
101
|
-
@env[REQUEST_PATH] = @url
|
102
|
-
@env[QUERY_STRING] = ''
|
103
|
-
end
|
104
|
-
|
105
|
-
# Grab the host name from the request
|
106
|
-
if host = @env[HTTP_HOST]
|
107
|
-
if colon = host.index(':')
|
108
|
-
@env[SERVER_NAME] = host[0, colon]
|
109
|
-
@env[SERVER_PORT] = host[colon+1, host.bytesize]
|
110
|
-
else
|
111
|
-
@env[SERVER_NAME] = host
|
112
|
-
@env[SERVER_PORT] = PROTO_ENV[SERVER_PORT]
|
113
|
-
end
|
114
|
-
else
|
115
|
-
@env[SERVER_NAME] = LOCALHOST
|
116
|
-
@env[SERVER_PORT] = PROTO_ENV[SERVER_PORT]
|
117
|
-
end
|
118
|
-
|
119
|
-
# Provide hijack options if this is an upgrade request
|
120
|
-
if @upgrade == true
|
121
|
-
@env[RACK_HIJACKABLE] = true
|
122
|
-
@env[RACK_HIJACK] = method(:hijack)
|
123
|
-
end
|
5
|
+
class Request
|
6
|
+
include Const
|
7
|
+
|
8
|
+
# TODO:: Add HTTP headers to the env and capitalise them and prefix them with HTTP_
|
9
|
+
# convert - signs to underscores
|
10
|
+
PROTO_ENV = {
|
11
|
+
RACK_VERSION => ::Rack::VERSION, # Should be an array of integers
|
12
|
+
RACK_ERRORS => $stderr, # An error stream that supports: puts, write and flush
|
13
|
+
RACK_MULTITHREAD => true, # can the app be simultaneously invoked by another thread?
|
14
|
+
RACK_MULTIPROCESS => false, # will the app be simultaneously be invoked in a separate process?
|
15
|
+
RACK_RUN_ONCE => false, # this isn't CGI so will always be false
|
16
|
+
|
17
|
+
SCRIPT_NAME => ENV['SCRIPT_NAME'] || EMPTY, # The virtual path of the app base (empty if root)
|
18
|
+
SERVER_PROTOCOL => HTTP_11,
|
19
|
+
|
20
|
+
GATEWAY_INTERFACE => CGI_VER,
|
21
|
+
SERVER_SOFTWARE => SERVER
|
22
|
+
}
|
23
|
+
|
24
|
+
attr_accessor :env, :url, :header, :body, :keep_alive, :upgrade, :deferred
|
25
|
+
attr_reader :hijacked, :response
|
26
|
+
|
27
|
+
def initialize(connection, app)
|
28
|
+
@app = app
|
29
|
+
@body = ''
|
30
|
+
@header = ''
|
31
|
+
@url = ''
|
32
|
+
@execute = method :execute
|
33
|
+
@env = PROTO_ENV.dup
|
34
|
+
@loop = connection.loop
|
35
|
+
@env[SERVER_PORT] = connection.port
|
36
|
+
@env[REMOTE_ADDR] = connection.remote_ip
|
37
|
+
@env[RACK_URL_SCHEME] = connection.tls ? HTTPS : HTTP
|
38
|
+
@env[ASYNC] = connection.async_callback
|
39
|
+
end
|
124
40
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
41
|
+
def execute!
|
42
|
+
@env[CONTENT_LENGTH] = @env.delete(HTTP_CONTENT_LENGTH) || @body.length
|
43
|
+
@env[CONTENT_TYPE] = @env.delete(HTTP_CONTENT_TYPE) || DEFAULT_TYPE
|
44
|
+
@env[REQUEST_URI] = @url.freeze
|
45
|
+
|
46
|
+
# For Rack::Lint on 1.9, ensure that the encoding is always for spec
|
47
|
+
@body.force_encoding('ASCII-8BIT') if @body.respond_to?(:force_encoding)
|
48
|
+
@env[RACK_INPUT] = StringIO.new @body
|
49
|
+
|
50
|
+
# Break the request into its components
|
51
|
+
query_start = @url.index '?'
|
52
|
+
if query_start
|
53
|
+
path = @url[0...query_start].freeze
|
54
|
+
@env[PATH_INFO] = path
|
55
|
+
@env[REQUEST_PATH] = path
|
56
|
+
@env[QUERY_STRING] = @url[query_start + 1..-1].freeze
|
57
|
+
else
|
58
|
+
@env[PATH_INFO] = @url
|
59
|
+
@env[REQUEST_PATH] = @url
|
60
|
+
@env[QUERY_STRING] = ''
|
61
|
+
end
|
62
|
+
|
63
|
+
# Grab the host name from the request
|
64
|
+
if host = @env[HTTP_HOST]
|
65
|
+
if colon = host.index(':')
|
66
|
+
@env[SERVER_NAME] = host[0, colon]
|
67
|
+
@env[SERVER_PORT] = host[colon+1, host.bytesize]
|
68
|
+
else
|
69
|
+
@env[SERVER_NAME] = host
|
70
|
+
@env[SERVER_PORT] = PROTO_ENV[SERVER_PORT]
|
131
71
|
end
|
72
|
+
else
|
73
|
+
@env[SERVER_NAME] = LOCALHOST
|
74
|
+
@env[SERVER_PORT] = PROTO_ENV[SERVER_PORT]
|
75
|
+
end
|
76
|
+
|
77
|
+
# Provide hijack options if this is an upgrade request
|
78
|
+
if @upgrade == true
|
79
|
+
@env[HIJACK_P] = true
|
80
|
+
@env[HIJACK] = method :hijack
|
81
|
+
end
|
82
|
+
|
83
|
+
# Execute the request
|
84
|
+
@response = catch :async, &@execute
|
85
|
+
@deferred = @loop.defer if (@response.nil? || @response[0] == -1)
|
86
|
+
@response
|
87
|
+
end
|
132
88
|
|
89
|
+
protected
|
133
90
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
body.close if body.respond_to?(:close)
|
143
|
-
result
|
144
|
-
end
|
91
|
+
# Execute the request then close the body
|
92
|
+
# NOTE:: closing the body here might cause issues (see connection.rb)
|
93
|
+
def execute(*args)
|
94
|
+
result = @app.call @env
|
95
|
+
body = result[2]
|
96
|
+
body.close if body.respond_to?(:close)
|
97
|
+
result
|
98
|
+
end
|
145
99
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
end
|
100
|
+
def hijack
|
101
|
+
@hijacked = @loop.defer
|
102
|
+
@env[HIJACK_IO] = @hijacked.promise
|
150
103
|
end
|
104
|
+
end
|
151
105
|
end
|
@@ -1,408 +1,392 @@
|
|
1
|
+
require 'spider-gazelle/const'
|
1
2
|
require 'set'
|
2
3
|
require 'thread'
|
3
4
|
require 'logger'
|
4
5
|
require 'singleton'
|
5
|
-
require 'fileutils'
|
6
|
+
require 'fileutils' # mkdir_p
|
6
7
|
require 'forwardable' # run method
|
7
8
|
|
8
|
-
|
9
9
|
module SpiderGazelle
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
class Spider
|
11
|
+
include Const
|
12
|
+
include Singleton
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
+
}
|
17
25
|
|
18
|
-
|
19
|
-
MODES = [:thread, :process, :no_ipc] # TODO:: implement clustering using processes
|
26
|
+
attr_reader :state, :mode, :threads, :logger
|
20
27
|
|
21
|
-
|
22
|
-
|
23
|
-
:Port => 8080,
|
24
|
-
:Verbose => false,
|
25
|
-
:tls => false,
|
26
|
-
:optimize_for_latency => true,
|
27
|
-
:backlog => 1024
|
28
|
-
}
|
28
|
+
extend Forwardable
|
29
|
+
def_delegators :@web, :run
|
29
30
|
|
30
|
-
|
31
|
-
|
31
|
+
def self.run(app, options = {})
|
32
|
+
options = DEFAULT_OPTIONS.merge options
|
32
33
|
|
33
|
-
|
34
|
+
ENV['RACK_ENV'] = options[:environment].to_s if options[:environment]
|
34
35
|
|
35
|
-
|
36
|
-
|
36
|
+
puts "Look out! Here comes Spider-Gazelle #{SPIDER_GAZELLE_VERSION}!"
|
37
|
+
puts "* Environment: #{ENV['RACK_ENV']} on #{RUBY_ENGINE || 'ruby'} #{RUBY_VERSION}"
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
39
|
+
server = instance
|
40
|
+
server.run do |logger|
|
41
|
+
logger.progress server.method(:log)
|
42
|
+
server.loaded.then do
|
43
|
+
puts "* Loading: #{app}"
|
43
44
|
|
44
|
-
|
45
|
+
# yield server if block_given?
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
# This will execute if the TCP binding is lost
|
50
|
-
# Terminating the application
|
51
|
-
Process.kill 'INT', 0
|
52
|
-
end
|
47
|
+
caught = proc { |e| puts("#{e.message}\n#{e.backtrace.join("\n")}") unless e.backtrace.nil? }
|
48
|
+
server.load(app, options).catch(caught)
|
49
|
+
.finally { Process.kill('INT', 0) } # Terminate the application if the TCP binding is lost
|
53
50
|
|
54
|
-
|
55
|
-
end
|
56
|
-
end
|
51
|
+
puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
|
57
52
|
end
|
53
|
+
end
|
54
|
+
end
|
58
55
|
|
56
|
+
def initialize
|
57
|
+
# Threaded mode by default
|
58
|
+
reanimating!
|
59
|
+
@bindings = {}
|
60
|
+
# @bindings = {
|
61
|
+
# id => [bind1, bind2]
|
62
|
+
# }
|
63
|
+
|
64
|
+
mode = ENV['SG_MODE'] || :thread
|
65
|
+
@mode = mode.to_sym
|
66
|
+
|
67
|
+
@delegate = method(no_ipc? ? :direct_delegate : :delegate)
|
68
|
+
@squash = method(:squash)
|
69
|
+
|
70
|
+
# FIXME The expanded path path seems a bit risky
|
71
|
+
log_path = ENV['SG_LOG'] || File.expand_path('../../../logs/server.log', __FILE__)
|
72
|
+
dirname = File.dirname(log_path)
|
73
|
+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
74
|
+
@logger = ::Logger.new(log_path.to_s, 10, 4194304)
|
75
|
+
|
76
|
+
# Keep track of the loading process
|
77
|
+
@waiting_gazelle = 0
|
78
|
+
@gazelle_count = 0
|
79
|
+
|
80
|
+
# Spider always runs on the default loop
|
81
|
+
@web = ::Libuv::Loop.default
|
82
|
+
@gazelles_loaded = @web.defer
|
83
|
+
|
84
|
+
# Start the server
|
85
|
+
if @web.reactor_running?
|
86
|
+
# Call run so we can be notified of errors
|
87
|
+
@web.run &method(:reanimate)
|
88
|
+
else
|
89
|
+
# Don't block on this thread if default reactor not running
|
90
|
+
Thread.new { @web.run(&method(:reanimate)) }
|
91
|
+
end
|
92
|
+
end
|
59
93
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
# id => [bind1, bind2]
|
71
|
-
}
|
94
|
+
# Modes
|
95
|
+
def thread?
|
96
|
+
@mode == :thread
|
97
|
+
end
|
98
|
+
def process?
|
99
|
+
@mode == :process
|
100
|
+
end
|
101
|
+
def no_ipc?
|
102
|
+
@mode == :no_ipc
|
103
|
+
end
|
72
104
|
|
73
|
-
|
74
|
-
|
105
|
+
# Statuses
|
106
|
+
def reanimating?
|
107
|
+
@status == :reanimating
|
108
|
+
end
|
109
|
+
def reanimating!
|
110
|
+
@status = :reanimating
|
111
|
+
end
|
112
|
+
def running?
|
113
|
+
@status == :running
|
114
|
+
end
|
115
|
+
def running!
|
116
|
+
@status = :running
|
117
|
+
end
|
118
|
+
def squashing?
|
119
|
+
@status == :squashing
|
120
|
+
end
|
121
|
+
def squashing!
|
122
|
+
@status = :squashing
|
123
|
+
end
|
124
|
+
def dead?
|
125
|
+
@status == :dead
|
126
|
+
end
|
127
|
+
def dead!
|
128
|
+
@status = :dead
|
129
|
+
end
|
75
130
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
131
|
+
# Provides a promise that resolves when we are read to start binding applications
|
132
|
+
#
|
133
|
+
# @return [::Libuv::Q::Promise] that indicates when the gazelles are loaded
|
134
|
+
def loaded
|
135
|
+
@gazelles_loaded.promise unless @gazelles_loaded.nil?
|
136
|
+
end
|
82
137
|
|
138
|
+
# A thread safe method for loading and binding rack apps. The app can be pre-loaded or a rackup file
|
139
|
+
#
|
140
|
+
# @param app [String, Object] rackup filename or rack app
|
141
|
+
# @param ports [Hash, Array] binding config or array of binding config
|
142
|
+
# @return [::Libuv::Q::Promise] resolves once the app is loaded (and bound if SG is running)
|
143
|
+
def load(app, ports = [])
|
144
|
+
defer = @web.defer
|
83
145
|
|
84
|
-
|
85
|
-
|
86
|
-
unless File.directory?(dirname)
|
87
|
-
FileUtils.mkdir_p(dirname)
|
88
|
-
end
|
89
|
-
@logger = ::Logger.new(log_path.to_s, 10, 4194304)
|
90
|
-
|
91
|
-
# Keep track of the loading process
|
92
|
-
@waiting_gazelle = 0
|
93
|
-
@gazelle_count = 0
|
94
|
-
|
95
|
-
# Spider always runs on the default loop
|
96
|
-
@web = ::Libuv::Loop.default
|
97
|
-
@gazelles_loaded = @web.defer
|
98
|
-
|
99
|
-
# Start the server
|
100
|
-
if @web.reactor_running?
|
101
|
-
# Call run so we can be notified of errors
|
102
|
-
@web.run &method(:reanimate)
|
103
|
-
else
|
104
|
-
# Don't block on this thread if default reactor not running
|
105
|
-
Thread.new do
|
106
|
-
@web.run &method(:reanimate)
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
146
|
+
ports = [ports] if ports.is_a?(Hash)
|
147
|
+
ports << {} if ports.empty?
|
110
148
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
@gazelles_loaded.promise unless @gazelles_loaded.nil?
|
116
|
-
end
|
149
|
+
@web.schedule do
|
150
|
+
begin
|
151
|
+
app_id = AppStore.load app
|
152
|
+
bindings = @bindings[app_id] ||= []
|
117
153
|
|
118
|
-
|
119
|
-
#
|
120
|
-
# @param app [String, Object] rackup filename or rack app
|
121
|
-
# @param ports [Hash, Array] binding config or array of binding config
|
122
|
-
# @return [::Libuv::Q::Promise] resolves once the app is loaded (and bound if SG is running)
|
123
|
-
def load(app, ports = [])
|
124
|
-
defer = @web.defer
|
125
|
-
|
126
|
-
ports = [ports] if ports.is_a? Hash
|
127
|
-
ports << {} if ports.empty?
|
128
|
-
|
129
|
-
@web.schedule do
|
130
|
-
begin
|
131
|
-
app_id = AppStore.load(app)
|
132
|
-
bindings = @bindings[app_id] ||= []
|
133
|
-
|
134
|
-
ports.each do |options|
|
135
|
-
binding = Binding.new(@web, @delegate, app_id, options)
|
136
|
-
bindings << binding
|
137
|
-
end
|
138
|
-
|
139
|
-
if @status == :running
|
140
|
-
defer.resolve(start(app, app_id))
|
141
|
-
else
|
142
|
-
defer.resolve(true)
|
143
|
-
end
|
144
|
-
rescue Exception => e
|
145
|
-
defer.reject(e)
|
146
|
-
end
|
147
|
-
end
|
154
|
+
ports.each { |options| bindings << Binding.new(@web, @delegate, app_id, options) }
|
148
155
|
|
149
|
-
|
156
|
+
defer.resolve(running? ? start(app, app_id) : true)
|
157
|
+
rescue Exception => e
|
158
|
+
defer.reject e
|
150
159
|
end
|
160
|
+
end
|
151
161
|
|
152
|
-
|
153
|
-
|
154
|
-
# @param app [String, Object] rackup filename or the rack app instance
|
155
|
-
# @return [::Libuv::Q::Promise] resolves once the app is bound to the port
|
156
|
-
def start(app, app_id = nil)
|
157
|
-
defer = @web.defer
|
158
|
-
app_id = app_id || AppStore.lookup(app)
|
159
|
-
|
160
|
-
if app_id != nil && @status == :running
|
161
|
-
@web.schedule do
|
162
|
-
bindings = @bindings[app_id] ||= []
|
163
|
-
starting = []
|
164
|
-
|
165
|
-
bindings.each do |binding|
|
166
|
-
starting << binding.bind
|
167
|
-
end
|
168
|
-
defer.resolve(::Libuv::Q.all(@web, *starting))
|
169
|
-
end
|
170
|
-
elsif app_id.nil?
|
171
|
-
defer.reject('application not loaded')
|
172
|
-
else
|
173
|
-
defer.reject('server not running')
|
174
|
-
end
|
162
|
+
defer.promise
|
163
|
+
end
|
175
164
|
|
176
|
-
|
165
|
+
# Starts the app specified. It must already be loaded
|
166
|
+
#
|
167
|
+
# @param app [String, Object] rackup filename or the rack app instance
|
168
|
+
# @return [::Libuv::Q::Promise] resolves once the app is bound to the port
|
169
|
+
def start(app, app_id = nil)
|
170
|
+
defer = @web.defer
|
171
|
+
app_id ||= AppStore.lookup app
|
172
|
+
|
173
|
+
if !app_id.nil? && running?
|
174
|
+
@web.schedule do
|
175
|
+
bindings = @bindings[app_id] ||= []
|
176
|
+
starting = []
|
177
|
+
|
178
|
+
bindings.each { |binding| starting << binding.bind }
|
179
|
+
defer.resolve ::Libuv::Q.all(@web, *starting)
|
177
180
|
end
|
181
|
+
elsif app_id.nil?
|
182
|
+
defer.reject 'application not loaded'
|
183
|
+
else
|
184
|
+
defer.reject 'server not running'
|
185
|
+
end
|
178
186
|
|
179
|
-
|
180
|
-
|
181
|
-
# @param app [String, Object] rackup filename or the rack app instance
|
182
|
-
# @return [::Libuv::Q::Promise] resolves once the app is no longer bound to the port
|
183
|
-
def stop(app, app_id = nil)
|
184
|
-
defer = @web.defer
|
185
|
-
app_id = app_id || AppStore.lookup(app)
|
186
|
-
|
187
|
-
if !app_id.nil?
|
188
|
-
@web.schedule do
|
189
|
-
bindings = @bindings[app_id]
|
190
|
-
closing = []
|
191
|
-
|
192
|
-
if bindings != nil
|
193
|
-
bindings.each do |binding|
|
194
|
-
result = binding.unbind
|
195
|
-
closing << result unless result.nil?
|
196
|
-
end
|
197
|
-
end
|
198
|
-
defer.resolve(::Libuv::Q.all(@web, *closing))
|
199
|
-
end
|
200
|
-
else
|
201
|
-
defer.reject('application not loaded')
|
202
|
-
end
|
187
|
+
defer.promise
|
188
|
+
end
|
203
189
|
|
204
|
-
|
190
|
+
# Stops the app specified. If loaded
|
191
|
+
#
|
192
|
+
# @param app [String, Object] rackup filename or the rack app instance
|
193
|
+
# @return [::Libuv::Q::Promise] resolves once the app is no longer bound to the port
|
194
|
+
def stop(app, app_id = nil)
|
195
|
+
defer = @web.defer
|
196
|
+
app_id ||= AppStore.lookup app
|
197
|
+
|
198
|
+
if !app_id.nil?
|
199
|
+
@web.schedule do
|
200
|
+
bindings = @bindings[app_id]
|
201
|
+
closing = []
|
202
|
+
|
203
|
+
bindings.each do |binding|
|
204
|
+
result = binding.unbind
|
205
|
+
closing << result unless result.nil?
|
206
|
+
end unless bindings.nil?
|
207
|
+
|
208
|
+
defer.resolve ::Libuv::Q.all(@web, *closing)
|
205
209
|
end
|
210
|
+
else
|
211
|
+
defer.reject 'application not loaded'
|
212
|
+
end
|
206
213
|
|
207
|
-
|
208
|
-
|
209
|
-
@signal_squash.call
|
210
|
-
end
|
214
|
+
defer.promise
|
215
|
+
end
|
211
216
|
|
217
|
+
# Signals spider gazelle to shutdown gracefully
|
218
|
+
def shutdown
|
219
|
+
@signal_squash.call
|
220
|
+
end
|
212
221
|
|
213
|
-
|
222
|
+
protected
|
214
223
|
|
224
|
+
# Called from the binding for sending to gazelles
|
225
|
+
def delegate(client, tls, port, app_id)
|
226
|
+
indicator = tls ? USE_TLS : NO_TLS
|
227
|
+
@select_handler.next.write2(client, "#{indicator} #{port} #{app_id}")
|
228
|
+
end
|
215
229
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
loop.write2(client, "#{indicator} #{port} #{app_id}")
|
221
|
-
end
|
230
|
+
def direct_delegate(client, tls, port, app_id)
|
231
|
+
indicator = tls ? USE_TLS : NO_TLS
|
232
|
+
@gazelle.__send__(:new_connection, "#{indicator} #{port} #{app_id}", client)
|
233
|
+
end
|
222
234
|
|
223
|
-
|
224
|
-
|
225
|
-
|
235
|
+
# Triggers the creation of gazelles
|
236
|
+
def reanimate(logger)
|
237
|
+
# Manage the set of Gazelle socket listeners
|
238
|
+
@threads = Set.new
|
239
|
+
|
240
|
+
if thread?
|
241
|
+
cpus = ::Libuv.cpu_count || 1
|
242
|
+
cpus.times { @threads << Libuv::Loop.new }
|
243
|
+
elsif no_ipc?
|
244
|
+
# TODO:: need to perform process mode as well
|
245
|
+
@threads << @web
|
246
|
+
end
|
247
|
+
|
248
|
+
@handlers = Set.new
|
249
|
+
@select_handler = @handlers.cycle # provides a looping enumerator for our round robin
|
250
|
+
@accept_handler = method :accept_handler
|
251
|
+
|
252
|
+
# Manage the set of Gazelle signal pipes
|
253
|
+
@gazella = Set.new
|
254
|
+
@accept_gazella = method :accept_gazella
|
255
|
+
|
256
|
+
# Create a function for stopping the spider from another thread
|
257
|
+
@signal_squash = @web.async @squash
|
258
|
+
|
259
|
+
# Link up the loops logger
|
260
|
+
logger.progress method(:log)
|
261
|
+
|
262
|
+
if no_ipc?
|
263
|
+
@gazelle = Gazelle.new @web, @logger, @mode
|
264
|
+
@gazelle_count = 1
|
265
|
+
start_bindings
|
266
|
+
else
|
267
|
+
# Bind the pipe for sending sockets to gazelle
|
268
|
+
begin
|
269
|
+
File.unlink DELEGATE_PIPE
|
270
|
+
rescue
|
226
271
|
end
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
cpus.times do
|
236
|
-
@threads << Libuv::Loop.new
|
237
|
-
end
|
238
|
-
elsif @mode == :no_ipc
|
239
|
-
# TODO:: need to perform process mode as well
|
240
|
-
@threads << @web
|
241
|
-
end
|
242
|
-
|
243
|
-
@handlers = Set.new
|
244
|
-
@select_handler = @handlers.cycle # provides a looping enumerator for our round robin
|
245
|
-
@accept_handler = method(:accept_handler)
|
246
|
-
|
247
|
-
# Manage the set of Gazelle signal pipes
|
248
|
-
@gazella = Set.new
|
249
|
-
@accept_gazella = method(:accept_gazella)
|
250
|
-
|
251
|
-
# Create a function for stopping the spider from another thread
|
252
|
-
@signal_squash = @web.async @squash
|
253
|
-
|
254
|
-
# Link up the loops logger
|
255
|
-
logger.progress method(:log)
|
256
|
-
|
257
|
-
if @mode == :no_ipc
|
258
|
-
@gazelle = Gazelle.new(@web, @logger, @mode)
|
259
|
-
@gazelle_count = 1
|
260
|
-
start_bindings
|
261
|
-
else
|
262
|
-
# Bind the pipe for sending sockets to gazelle
|
263
|
-
begin
|
264
|
-
File.unlink(DELEGATE_PIPE)
|
265
|
-
rescue
|
266
|
-
end
|
267
|
-
@delegator = @web.pipe(true)
|
268
|
-
@delegator.bind(DELEGATE_PIPE) do
|
269
|
-
@delegator.accept @accept_handler
|
270
|
-
end
|
271
|
-
@delegator.listen(16)
|
272
|
-
|
273
|
-
# Bind the pipe for communicating with gazelle
|
274
|
-
begin
|
275
|
-
File.unlink(SIGNAL_PIPE)
|
276
|
-
rescue
|
277
|
-
end
|
278
|
-
@signaller = @web.pipe(true)
|
279
|
-
@signaller.bind(SIGNAL_PIPE) do
|
280
|
-
@signaller.accept @accept_gazella
|
281
|
-
end
|
282
|
-
@signaller.listen(16)
|
283
|
-
|
284
|
-
# Launch the gazelle here
|
285
|
-
@threads.each do |thread|
|
286
|
-
Thread.new do
|
287
|
-
gazelle = Gazelle.new(thread, @logger, @mode)
|
288
|
-
gazelle.run
|
289
|
-
end
|
290
|
-
@waiting_gazelle += 1
|
291
|
-
end
|
292
|
-
end
|
293
|
-
|
294
|
-
# Signal gazelle death here
|
295
|
-
@web.signal :INT, @squash
|
296
|
-
|
297
|
-
# Update state only once the event loop is ready
|
298
|
-
@gazelles_loaded.promise
|
272
|
+
@delegator = @web.pipe true
|
273
|
+
@delegator.bind(DELEGATE_PIPE) { @delegator.accept @accept_handler }
|
274
|
+
@delegator.listen INTERNAL_PIPE_BACKLOG
|
275
|
+
|
276
|
+
# Bind the pipe for communicating with gazelle
|
277
|
+
begin
|
278
|
+
File.unlink SIGNAL_PIPE
|
279
|
+
rescue
|
299
280
|
end
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
@bindings.each_key do |key|
|
309
|
-
stop(key)
|
310
|
-
end
|
311
|
-
|
312
|
-
if @mode == :no_ipc
|
313
|
-
@web.stop
|
314
|
-
@status = :dead
|
315
|
-
else
|
316
|
-
# Signal all the gazelle to shutdown
|
317
|
-
promises = []
|
318
|
-
@gazella.each do |gazelle|
|
319
|
-
promises << gazelle.write(KILL_GAZELLE)
|
320
|
-
end
|
321
|
-
|
322
|
-
# Once the signal has been sent we can stop the spider loop
|
323
|
-
@web.finally(*promises).finally do
|
324
|
-
|
325
|
-
# TODO:: need a better system for ensuring these are cleaned up
|
326
|
-
# Especially when we implement live migrations and process clusters
|
327
|
-
begin
|
328
|
-
@delegator.close
|
329
|
-
File.unlink(DELEGATE_PIPE)
|
330
|
-
rescue
|
331
|
-
end
|
332
|
-
begin
|
333
|
-
@signaller.close
|
334
|
-
File.unlink(SIGNAL_PIPE)
|
335
|
-
rescue
|
336
|
-
end
|
337
|
-
|
338
|
-
@web.stop
|
339
|
-
@status = :dead
|
340
|
-
end
|
341
|
-
end
|
342
|
-
end
|
281
|
+
@signaller = @web.pipe true
|
282
|
+
@signaller.bind(SIGNAL_PIPE) { @signaller.accept @accept_gazella }
|
283
|
+
@signaller.listen INTERNAL_PIPE_BACKLOG
|
284
|
+
|
285
|
+
# Launch the gazelle here
|
286
|
+
@threads.each do |thread|
|
287
|
+
Thread.new { Gazelle.new(thread, @logger, @mode).run }
|
288
|
+
@waiting_gazelle += 1
|
343
289
|
end
|
290
|
+
end
|
344
291
|
|
345
|
-
|
346
|
-
|
347
|
-
#puts "gazelle #{@gazella.size} signal port ready"
|
292
|
+
# Signal gazelle death here
|
293
|
+
@web.signal :INT, @squash
|
348
294
|
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
@gazella.delete gazelle
|
353
|
-
@waiting_gazelle -= 1
|
354
|
-
@gazelle_count -= 1
|
355
|
-
end
|
295
|
+
# Update state only once the event loop is ready
|
296
|
+
@gazelles_loaded.promise
|
297
|
+
end
|
356
298
|
|
357
|
-
|
358
|
-
|
359
|
-
|
299
|
+
# Triggers a shutdown of the gazelles.
|
300
|
+
# We ensure the process is running here as signals can be called multiple times
|
301
|
+
def squash(*args)
|
302
|
+
if running?
|
303
|
+
# Update the state and close the socket
|
304
|
+
squashing!
|
305
|
+
@bindings.each { |key, val| stop(key) }
|
306
|
+
|
307
|
+
if no_ipc?
|
308
|
+
@web.stop
|
309
|
+
dead!
|
310
|
+
else
|
311
|
+
# Signal all the gazelle to shutdown
|
312
|
+
promises = @gazella.map { |gazelle| gazelle.write(KILL_GAZELLE) }
|
313
|
+
|
314
|
+
# Once the signal has been sent we can stop the spider loop
|
315
|
+
@web.finally(*promises).finally do
|
316
|
+
# TODO:: need a better system for ensuring these are cleaned up
|
317
|
+
# Especially when we implement live migrations and process clusters
|
318
|
+
begin
|
319
|
+
@delegator.close
|
320
|
+
File.unlink DELEGATE_PIPE
|
321
|
+
rescue
|
360
322
|
end
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
# Start any bindings that are already present
|
367
|
-
@bindings.each_key do |key|
|
368
|
-
start(key)
|
323
|
+
begin
|
324
|
+
@signaller.close
|
325
|
+
File.unlink SIGNAL_PIPE
|
326
|
+
rescue
|
369
327
|
end
|
370
328
|
|
371
|
-
|
372
|
-
|
329
|
+
@web.stop
|
330
|
+
dead!
|
331
|
+
end
|
373
332
|
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
# A new gazelle is ready to accept commands
|
337
|
+
def accept_gazella(gazelle)
|
338
|
+
# add the signal port to the set
|
339
|
+
@gazella.add gazelle
|
340
|
+
gazelle.finally do
|
341
|
+
@gazella.delete gazelle
|
342
|
+
@waiting_gazelle -= 1
|
343
|
+
@gazelle_count -= 1
|
344
|
+
end
|
345
|
+
|
346
|
+
@gazelle_count += 1
|
347
|
+
start_bindings if @waiting_gazelle == @gazelle_count
|
348
|
+
end
|
374
349
|
|
375
|
-
|
376
|
-
|
377
|
-
def accept_handler(handler)
|
378
|
-
#puts "gazelle #{@handlers.size} loop running"
|
350
|
+
def start_bindings
|
351
|
+
running!
|
379
352
|
|
380
|
-
|
381
|
-
|
353
|
+
# Start any bindings that are already present
|
354
|
+
@bindings.each { |key, val| start(key) }
|
382
355
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
@select_handler.rewind
|
356
|
+
# Inform any listeners that we have completed loading
|
357
|
+
@gazelles_loaded.resolve @gazelle_count
|
358
|
+
end
|
387
359
|
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
360
|
+
# A new gazelle loop is ready to accept sockets.
|
361
|
+
# We start the server as soon as the first gazelle is ready
|
362
|
+
def accept_handler(handler)
|
363
|
+
# Add the new gazelle to the set
|
364
|
+
@handlers.add handler
|
365
|
+
# Update the enumerator with the new gazelle
|
366
|
+
@select_handler.rewind
|
367
|
+
|
368
|
+
# If a gazelle dies or shuts down we update the set
|
369
|
+
handler.finally do
|
370
|
+
@handlers.delete handler
|
371
|
+
@select_handler.rewind
|
372
|
+
|
373
|
+
# I assume if we made it here something went quite wrong
|
374
|
+
squash if running? && @handlers.empty?
|
375
|
+
end
|
376
|
+
end
|
394
377
|
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
378
|
+
# TODO FIXME Review this method.
|
379
|
+
def log(*args)
|
380
|
+
msg = ''
|
381
|
+
err = args[-1]
|
382
|
+
if err && err.respond_to?(:backtrace)
|
383
|
+
msg << "unhandled exception: #{err.message} (#{args[0..-2]})"
|
384
|
+
msg << "\n#{err.backtrace.join("\n")}" if err.respond_to?(:backtrace) && err.backtrace
|
385
|
+
else
|
386
|
+
msg << "unhandled exception: #{args}"
|
387
|
+
end
|
388
|
+
@logger.error msg
|
389
|
+
::Libuv::Q.reject @web, msg
|
407
390
|
end
|
391
|
+
end
|
408
392
|
end
|