spider-gazelle 2.0.4 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/spider-gazelle.rb +12 -10
- data/lib/spider-gazelle/gazelle.rb +12 -21
- data/lib/spider-gazelle/gazelle/app_store.rb +7 -4
- data/lib/spider-gazelle/gazelle/http1.rb +64 -87
- data/lib/spider-gazelle/gazelle/request.rb +55 -62
- data/lib/spider-gazelle/logger.rb +9 -12
- data/lib/spider-gazelle/options.rb +8 -7
- data/lib/spider-gazelle/reactor.rb +11 -11
- data/lib/spider-gazelle/signaller.rb +10 -6
- data/lib/spider-gazelle/signaller/signal_parser.rb +2 -0
- data/lib/spider-gazelle/spider.rb +23 -12
- data/lib/spider-gazelle/spider/binding.rb +3 -1
- data/lib/spider-gazelle/upgrades/websocket.rb +11 -9
- data/lib/spider-gazelle/version.rb +3 -2
- data/spec/http1_spec.rb +22 -17
- data/spider-gazelle.gemspec +9 -9
- metadata +47 -51
- data/lib/rack/lock_patch.rb +0 -39
- data/spec/rack_lock_spec.rb +0 -126
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 778791bf3b7c52fdfa82112ce9e1bf4b53df9223
|
4
|
+
data.tar.gz: 57b34f904238ae88fa5a2bb45c7110f366eaad1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79445b4d863e03f59656d48f112ce565764cca4ebfb59ca915ad56de43342c54392b7fb71afe77894af4acc31952f12d4b3dcb1fe5505d9caac7f192507f1574
|
7
|
+
data.tar.gz: a20df55c3754354438a217350b2693067e23988722c0bbc0c3401d9bf23974fe702534c563a005743cb2ce34d60d956bf38c5ef085afaebfc7a1ca26686fe528
|
data/lib/spider-gazelle.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'thread'
|
2
4
|
require 'singleton'
|
3
5
|
|
@@ -10,22 +12,22 @@ require 'spider-gazelle/signaller'
|
|
10
12
|
|
11
13
|
|
12
14
|
module SpiderGazelle
|
13
|
-
INTERNAL_PIPE_BACKLOG =
|
15
|
+
INTERNAL_PIPE_BACKLOG = 4096
|
14
16
|
|
15
17
|
# Signaller is used to communicate:
|
16
18
|
# * command line requests
|
17
19
|
# * Startup and shutdown requests
|
18
20
|
# * Live updates (bindings passed by this pipe)
|
19
|
-
SIGNAL_SERVER = '/tmp/sg-signaller.pipe'
|
21
|
+
SIGNAL_SERVER = '/tmp/sg-signaller.pipe'
|
20
22
|
|
21
23
|
# Spider server is used to
|
22
24
|
# * Track gazelles
|
23
25
|
# * Signal shutdown as required
|
24
26
|
# * Pass sockets
|
25
|
-
SPIDER_SERVER = '/tmp/sg-spider.pipe.'
|
27
|
+
SPIDER_SERVER = '/tmp/sg-spider.pipe.'
|
28
|
+
|
26
29
|
|
27
30
|
MODES = [:process, :thread, :no_ipc].freeze
|
28
|
-
APP_MODE = [:thread_pool, :fiber_pool, :libuv, :eventmachine, :celluloid]
|
29
31
|
|
30
32
|
|
31
33
|
class LaunchControl
|
@@ -144,24 +146,24 @@ module SpiderGazelle
|
|
144
146
|
|
145
147
|
if running
|
146
148
|
if master[:spider]
|
147
|
-
logger.verbose "Starting Spider"
|
149
|
+
logger.verbose "Starting Spider"
|
148
150
|
start_spider(signaller, logger, options)
|
149
151
|
elsif master[:gazelle]
|
150
|
-
logger.verbose "Starting Gazelle"
|
152
|
+
logger.verbose "Starting Gazelle"
|
151
153
|
start_gazelle(signaller, logger, options)
|
152
154
|
else
|
153
|
-
logger.verbose "Sending signal to SG Master"
|
155
|
+
logger.verbose "Sending signal to SG Master"
|
154
156
|
signal_master(reactor, signaller, logger, options)
|
155
157
|
end
|
156
158
|
|
157
159
|
elsif master[:debug]
|
158
|
-
logger.verbose "SG is now running in debug mode"
|
160
|
+
logger.verbose "SG is now running in debug mode"
|
159
161
|
else
|
160
|
-
logger.verbose "SG was not running, launching Spider"
|
162
|
+
logger.verbose "SG was not running, launching Spider"
|
161
163
|
launch_spider(@args)
|
162
164
|
end
|
163
165
|
rescue => e
|
164
|
-
logger.verbose "Error performing requested operation"
|
166
|
+
logger.verbose "Error performing requested operation"
|
165
167
|
logger.print_error(e)
|
166
168
|
shutdown
|
167
169
|
end
|
@@ -1,5 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "rack" # Ruby webserver abstraction
|
2
|
-
require "rack/lock_patch" # Serialize execution in development mode
|
3
4
|
require 'spider-gazelle/gazelle/app_store'
|
4
5
|
require 'spider-gazelle/gazelle/http1'
|
5
6
|
|
@@ -10,7 +11,7 @@ require "spider-gazelle/upgrades/websocket"
|
|
10
11
|
|
11
12
|
module SpiderGazelle
|
12
13
|
class Gazelle
|
13
|
-
SPACE = ' '
|
14
|
+
SPACE = ' '
|
14
15
|
|
15
16
|
def initialize(thread, type)
|
16
17
|
raise ArgumentError, "type must be one of #{MODES}" unless MODES.include?(type)
|
@@ -52,23 +53,13 @@ module SpiderGazelle
|
|
52
53
|
socket = @pipe.check_pending
|
53
54
|
return if socket.nil?
|
54
55
|
process_connection(socket, data.to_i)
|
56
|
+
rescue => e
|
57
|
+
@logger.print_error(e)
|
55
58
|
end
|
56
59
|
|
57
|
-
def shutdown
|
58
|
-
# Wait for the requests to finish
|
59
|
-
# TODO::
|
60
|
-
|
60
|
+
def shutdown
|
61
|
+
# Wait for the requests to finish
|
61
62
|
@logger.verbose { "Gazelle: #{@type} Pid: #{Process.pid} shutting down" }
|
62
|
-
|
63
|
-
# Then stop the current thread if we are in threaded mode
|
64
|
-
if @type == :thread
|
65
|
-
# In threaded mode the gazelle has the power
|
66
|
-
@thread.stop
|
67
|
-
else
|
68
|
-
# Both no_ipc and process need to know when the requests
|
69
|
-
# have completed to shutdown
|
70
|
-
finished.resolve(true)
|
71
|
-
end
|
72
63
|
end
|
73
64
|
|
74
65
|
|
@@ -84,8 +75,8 @@ module SpiderGazelle
|
|
84
75
|
authenticate
|
85
76
|
end
|
86
77
|
|
87
|
-
@pipe.catch do |error|
|
88
|
-
@logger.print_error(error)
|
78
|
+
@pipe.catch do |error, backtrace|
|
79
|
+
@logger.print_error(error, String.new, backtrace)
|
89
80
|
end
|
90
81
|
|
91
82
|
@pipe.finally do
|
@@ -93,7 +84,7 @@ module SpiderGazelle
|
|
93
84
|
Reactor.instance.shutdown
|
94
85
|
else
|
95
86
|
# Threaded mode
|
96
|
-
|
87
|
+
connect_to_spider
|
97
88
|
end
|
98
89
|
end
|
99
90
|
end
|
@@ -141,7 +132,7 @@ module SpiderGazelle
|
|
141
132
|
end
|
142
133
|
|
143
134
|
def set_protocol(socket, version)
|
144
|
-
app,
|
135
|
+
app, port, tls = socket.storage
|
145
136
|
|
146
137
|
parser = if version == :h2
|
147
138
|
@http2_cache.pop || new_http2_parser
|
@@ -149,7 +140,7 @@ module SpiderGazelle
|
|
149
140
|
@http1_cache.pop || new_http1_parser
|
150
141
|
end
|
151
142
|
|
152
|
-
parser.load(socket, port, app,
|
143
|
+
parser.load(socket, port, app, tls)
|
153
144
|
socket.progress @on_progress
|
154
145
|
socket.storage = parser
|
155
146
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'thread'
|
2
4
|
|
3
5
|
module SpiderGazelle
|
@@ -15,10 +17,11 @@ module SpiderGazelle
|
|
15
17
|
return if @loaded[rackup]
|
16
18
|
|
17
19
|
app, opts = ::Rack::Builder.parse_file(rackup)
|
20
|
+
app = Rack::Lint.new(app) if options[:lint]
|
18
21
|
tls = configure_tls(options)
|
19
22
|
port = tls ? 443 : 80
|
20
23
|
|
21
|
-
val = [app,
|
24
|
+
val = [app, port.to_s, tls]
|
22
25
|
@apps << val
|
23
26
|
@loaded[rackup] = val
|
24
27
|
}
|
@@ -40,7 +43,7 @@ module SpiderGazelle
|
|
40
43
|
tls = configure_tls(options)
|
41
44
|
port = tls ? 443 : 80
|
42
45
|
|
43
|
-
val = [app,
|
46
|
+
val = [app, port.to_s, tls]
|
44
47
|
@apps << val
|
45
48
|
@loaded[obj_id] = val
|
46
49
|
|
@@ -58,8 +61,8 @@ module SpiderGazelle
|
|
58
61
|
@apps[id.to_i]
|
59
62
|
end
|
60
63
|
|
61
|
-
PROTOCOLS = ['h2'
|
62
|
-
FALLBACK = 'http/1.1'
|
64
|
+
PROTOCOLS = ['h2', 'http/1.1'].freeze
|
65
|
+
FALLBACK = 'http/1.1'
|
63
66
|
def self.configure_tls(opts)
|
64
67
|
return false unless opts[:tls]
|
65
68
|
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
2
3
|
require 'http-parser' # C based, fast, http parser
|
3
4
|
require 'spider-gazelle/gazelle/request'
|
@@ -30,10 +31,10 @@ module SpiderGazelle
|
|
30
31
|
req.header.frozen? ? req.header = header : req.header << header
|
31
32
|
end
|
32
33
|
|
33
|
-
DASH = '-'
|
34
|
-
UNDERSCORE = '_'
|
35
|
-
HTTP_META = 'HTTP_'
|
36
|
-
COMMA = ', '
|
34
|
+
DASH = '-'
|
35
|
+
UNDERSCORE = '_'
|
36
|
+
HTTP_META = 'HTTP_'
|
37
|
+
COMMA = ', '
|
37
38
|
|
38
39
|
def on_header_value(parser, value)
|
39
40
|
req = @connection.parsing
|
@@ -49,7 +50,7 @@ module SpiderGazelle
|
|
49
50
|
req.env[header] << COMMA
|
50
51
|
req.env[header] << value
|
51
52
|
else
|
52
|
-
req.env[header] = value
|
53
|
+
req.env[header] = String.new(value)
|
53
54
|
end
|
54
55
|
end
|
55
56
|
end
|
@@ -78,18 +79,8 @@ module SpiderGazelle
|
|
78
79
|
@thread = thread
|
79
80
|
@logger = logger
|
80
81
|
|
81
|
-
@
|
82
|
-
|
83
|
-
if result.is_a?(Fixnum) && !request.defer.resolved?
|
84
|
-
# TODO:: setup timeout for async response
|
85
|
-
end
|
86
|
-
else
|
87
|
-
# Complete the current request
|
88
|
-
request.defer.resolve(result)
|
89
|
-
end
|
90
|
-
}
|
91
|
-
|
92
|
-
@queue_response = method(:queue_response)
|
82
|
+
@queue_response = method :queue_response
|
83
|
+
@write_chunk = method :write_chunk
|
93
84
|
|
94
85
|
# The parser state for this instance
|
95
86
|
@state = ::HttpParser::Parser.new_instance do |inst|
|
@@ -108,28 +99,13 @@ module SpiderGazelle
|
|
108
99
|
def self.on_progress(data, socket); end
|
109
100
|
DUMMY_PROGRESS = self.method :on_progress
|
110
101
|
|
111
|
-
HTTP = 'http'
|
112
|
-
HTTPS = 'https'
|
102
|
+
HTTP = 'http'
|
103
|
+
HTTPS = 'https'
|
113
104
|
|
114
|
-
def load(socket, port, app,
|
105
|
+
def load(socket, port, app, tls)
|
115
106
|
@socket = socket
|
116
107
|
@port = port
|
117
108
|
@app = app
|
118
|
-
@mode = app_mode
|
119
|
-
|
120
|
-
case @mode
|
121
|
-
when :thread_pool
|
122
|
-
@exec = method :exec_on_thread_pool
|
123
|
-
when :fiber_pool
|
124
|
-
# TODO:: Implement these modes
|
125
|
-
@exec = method :critical_error
|
126
|
-
when :libuv
|
127
|
-
@exec = method :critical_error
|
128
|
-
when :eventmachine
|
129
|
-
@exec = method :critical_error
|
130
|
-
when :celluloid
|
131
|
-
@exec = method :critical_error
|
132
|
-
end
|
133
109
|
|
134
110
|
@remote_ip = socket.peername[0]
|
135
111
|
@scheme = tls ? HTTPS : HTTP
|
@@ -182,15 +158,15 @@ module SpiderGazelle
|
|
182
158
|
# Parser Callbacks
|
183
159
|
# ----------------
|
184
160
|
def start_parsing
|
185
|
-
@parsing = Request.new @thread, @app, @port, @remote_ip, @scheme
|
161
|
+
@parsing = Request.new @thread, @app, @port, @remote_ip, @scheme, @socket
|
186
162
|
end
|
187
163
|
|
188
|
-
REQUEST_METHOD = 'REQUEST_METHOD'
|
164
|
+
REQUEST_METHOD = 'REQUEST_METHOD'
|
189
165
|
def headers_complete
|
190
166
|
@parsing.env[REQUEST_METHOD] = @state.http_method.to_s
|
191
167
|
end
|
192
168
|
|
193
|
-
ASYNC = "async.callback"
|
169
|
+
ASYNC = "async.callback"
|
194
170
|
def finished_parsing
|
195
171
|
request = @parsing
|
196
172
|
@parsing = nil
|
@@ -205,52 +181,54 @@ module SpiderGazelle
|
|
205
181
|
# See: http://polycrystal.org/2012/04/15/asynchronous_responses_in_rack.html
|
206
182
|
# Process a response that was marked as async.
|
207
183
|
request.env[ASYNC] = proc { |data|
|
208
|
-
@thread.schedule { request.defer.resolve(data) }
|
184
|
+
@thread.schedule { request.defer.resolve([request, data]) }
|
209
185
|
}
|
210
186
|
request.upgrade = @state.upgrade?
|
211
187
|
@requests << request
|
212
|
-
|
188
|
+
|
189
|
+
unless @processing
|
190
|
+
::Fiber.new { process_next }.resume
|
191
|
+
end
|
213
192
|
end
|
214
193
|
|
215
194
|
# ------------------
|
216
195
|
# Request Processing
|
217
196
|
# ------------------
|
197
|
+
EMPTY_RESPONSE = [''].freeze
|
218
198
|
def process_next
|
219
199
|
@processing = @requests.shift
|
220
200
|
if @processing
|
221
|
-
@
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
def exec_on_thread_pool
|
231
|
-
request = @processing
|
232
|
-
promise = @thread.work { work(request) }
|
233
|
-
promise.then @work_complete
|
234
|
-
promise.catch WORKER_ERROR
|
235
|
-
end
|
201
|
+
request = @processing
|
202
|
+
begin
|
203
|
+
result = begin
|
204
|
+
request.execute!
|
205
|
+
rescue StandardError => e
|
206
|
+
@logger.print_error e, 'framework error'
|
207
|
+
@processing.keep_alive = false
|
208
|
+
[500, {}, EMPTY_RESPONSE]
|
209
|
+
end
|
236
210
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
211
|
+
if request.is_async && !request.hijacked
|
212
|
+
if result.nil? && !request.defer.resolved?
|
213
|
+
# TODO:: setup timeout for async response
|
214
|
+
end
|
215
|
+
else
|
216
|
+
# Complete the current request
|
217
|
+
request.defer.resolve([request, result])
|
218
|
+
end
|
219
|
+
request.then @queue_response
|
220
|
+
rescue Exception => error
|
221
|
+
Logger.instance.print_error error, 'critical error'
|
222
|
+
Reactor.instance.shutdown
|
223
|
+
end
|
245
224
|
end
|
246
225
|
end
|
247
226
|
|
248
|
-
|
249
227
|
# ----------------
|
250
228
|
# Response Sending
|
251
229
|
# ----------------
|
252
|
-
def queue_response(
|
253
|
-
@responses <<
|
230
|
+
def queue_response(response)
|
231
|
+
@responses << response
|
254
232
|
send_next_response unless @transmitting
|
255
233
|
|
256
234
|
# Processing will be set to nil if the array is empty
|
@@ -258,14 +236,14 @@ module SpiderGazelle
|
|
258
236
|
end
|
259
237
|
|
260
238
|
|
261
|
-
HEAD = 'HEAD'
|
262
|
-
ETAG = 'ETag'
|
263
|
-
HTTP_ETAG = 'HTTP_ETAG'
|
264
|
-
CONTENT_LENGTH2 = 'Content-Length'
|
265
|
-
TRANSFER_ENCODING = 'Transfer-Encoding'
|
266
|
-
CHUNKED = 'chunked'
|
267
|
-
ZERO = '0'
|
268
|
-
NOT_MODIFIED_304 = "HTTP/1.1 304 Not Modified\r\n"
|
239
|
+
HEAD = 'HEAD'
|
240
|
+
ETAG = 'ETag'
|
241
|
+
HTTP_ETAG = 'HTTP_ETAG'
|
242
|
+
CONTENT_LENGTH2 = 'Content-Length'
|
243
|
+
TRANSFER_ENCODING = 'Transfer-Encoding'
|
244
|
+
CHUNKED = 'chunked'
|
245
|
+
ZERO = '0'
|
246
|
+
NOT_MODIFIED_304 = "HTTP/1.1 304 Not Modified\r\n"
|
269
247
|
|
270
248
|
def send_next_response
|
271
249
|
request, result = @responses.shift
|
@@ -361,7 +339,7 @@ module SpiderGazelle
|
|
361
339
|
# Optimize the response
|
362
340
|
begin
|
363
341
|
if body.size < 2
|
364
|
-
headers[CONTENT_LENGTH2] = body.size == 1 ? body[0].bytesize : ZERO
|
342
|
+
headers[CONTENT_LENGTH2] = body.size == 1 ? body[0].bytesize.to_s : ZERO
|
365
343
|
end
|
366
344
|
rescue # just in case
|
367
345
|
end
|
@@ -381,7 +359,7 @@ module SpiderGazelle
|
|
381
359
|
end
|
382
360
|
end
|
383
361
|
|
384
|
-
CLOSE_CHUNKED = "0\r\n\r\n"
|
362
|
+
CLOSE_CHUNKED = "0\r\n\r\n"
|
385
363
|
def write_response(request, status, headers, body)
|
386
364
|
keep_alive = request.keep_alive
|
387
365
|
|
@@ -397,7 +375,6 @@ module SpiderGazelle
|
|
397
375
|
write_headers keep_alive, status, headers
|
398
376
|
|
399
377
|
# Stream the response
|
400
|
-
@write_chunk ||= method :write_chunk
|
401
378
|
body.each &@write_chunk
|
402
379
|
|
403
380
|
@socket.write CLOSE_CHUNKED
|
@@ -407,8 +384,8 @@ module SpiderGazelle
|
|
407
384
|
body.close if body.respond_to?(:close)
|
408
385
|
end
|
409
386
|
|
410
|
-
COLON_SPACE = ': '
|
411
|
-
LINE_END = "\r\n"
|
387
|
+
COLON_SPACE = ': '
|
388
|
+
LINE_END = "\r\n"
|
412
389
|
def add_header(header, key, value)
|
413
390
|
header << key
|
414
391
|
header << COLON_SPACE
|
@@ -416,14 +393,14 @@ module SpiderGazelle
|
|
416
393
|
header << LINE_END
|
417
394
|
end
|
418
395
|
|
419
|
-
CONNECTION = "Connection"
|
420
|
-
NEWLINE = "\n"
|
421
|
-
CLOSE = "close"
|
422
|
-
RACK = "rack"
|
396
|
+
CONNECTION = "Connection"
|
397
|
+
NEWLINE = "\n"
|
398
|
+
CLOSE = "close"
|
399
|
+
RACK = "rack"
|
423
400
|
def write_headers(keep_alive, status, headers)
|
424
401
|
headers[CONNECTION] = CLOSE if keep_alive == false
|
425
402
|
|
426
|
-
header = "HTTP/1.1 #{status} #{fetch_code(status)}\r\n"
|
403
|
+
header = String.new("HTTP/1.1 #{status} #{fetch_code(status)}\r\n")
|
427
404
|
headers.each do |key, value|
|
428
405
|
next if key.start_with? RACK
|
429
406
|
value.to_s.split(NEWLINE).each {|val| add_header(header, key, val)}
|
@@ -439,7 +416,7 @@ module SpiderGazelle
|
|
439
416
|
end
|
440
417
|
|
441
418
|
HTTP_STATUS_CODES = Rack::Utils::HTTP_STATUS_CODES
|
442
|
-
HTTP_STATUS_DEFAULT = proc { 'CUSTOM'
|
419
|
+
HTTP_STATUS_DEFAULT = proc { 'CUSTOM' }
|
443
420
|
def fetch_code(status)
|
444
421
|
HTTP_STATUS_CODES.fetch(status, &HTTP_STATUS_DEFAULT)
|
445
422
|
end
|
@@ -469,7 +446,7 @@ module SpiderGazelle
|
|
469
446
|
end
|
470
447
|
end
|
471
448
|
|
472
|
-
ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
|
449
|
+
ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
|
473
450
|
def send_parsing_error
|
474
451
|
@logger.info "Parsing error!"
|
475
452
|
@socket.stop_read
|
@@ -477,7 +454,7 @@ module SpiderGazelle
|
|
477
454
|
@socket.shutdown
|
478
455
|
end
|
479
456
|
|
480
|
-
ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
|
457
|
+
ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
|
481
458
|
def send_internal_error
|
482
459
|
@logger.info "Internal error"
|
483
460
|
@socket.stop_read
|