spider-gazelle 2.0.1 → 2.0.2
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/README.md +3 -2
- data/bin/sg +2 -0
- data/lib/spider-gazelle/gazelle/http1.rb +16 -25
- data/lib/spider-gazelle/gazelle/request.rb +3 -3
- data/lib/spider-gazelle/options.rb +3 -0
- data/lib/spider-gazelle/version.rb +5 -0
- data/lib/spider-gazelle.rb +1 -2
- data/spec/http1_spec.rb +198 -15
- data/spider-gazelle.gemspec +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ce0882fd3ec8b1ae9319cff47eb1ed6504b18c2e
|
4
|
+
data.tar.gz: 9ac4f207d5fb67c773ae7775d42f61b1dfc8a309
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0cc6c0b91aaf2dd1299267df06682ea445f5d2b5811a41f83cdb708f5032805513dff7c7872371f982106154e7d5dcbe7f08d76c4506a5064b3fdc5f044cae19
|
7
|
+
data.tar.gz: cfd13c84de17f19f91832a9e29caef019c329cabf6b29de0255512f512b66ba2a44e0128598153c92e80ce3498687f9b8eddd7cc6c6fc810dda763c62393c3f8
|
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# spider-gazelle
|
2
2
|
|
3
|
-
[<img src="https://codeclimate.com/github/cotag/spider-gazelle.
|
3
|
+
[<img src="https://codeclimate.com/github/cotag/spider-gazelle/badges/gpa.svg" />](https://codeclimate.com/github/cotag/spider-gazelle)
|
4
|
+
[](https://travis-ci.org/cotag/spider-gazelle)
|
4
5
|
|
5
6
|
|
6
7
|
A fast, parallel and concurrent web server for ruby
|
@@ -28,7 +29,7 @@ Look out! Here comes the Spidergazelle!
|
|
28
29
|
|
29
30
|
## Options
|
30
31
|
|
31
|
-
For other command line options look at [the source](/
|
32
|
+
For other command line options look at [the source](/lib/spider-gazelle/options.rb)
|
32
33
|
|
33
34
|
|
34
35
|
## Community support
|
data/bin/sg
CHANGED
@@ -79,10 +79,9 @@ module SpiderGazelle
|
|
79
79
|
@logger = logger
|
80
80
|
|
81
81
|
@work = method(:work)
|
82
|
-
@work_complete = proc { |result|
|
83
|
-
request = @processing
|
82
|
+
@work_complete = proc { |request, result|
|
84
83
|
if request.is_async && !request.hijacked
|
85
|
-
if result.is_a?
|
84
|
+
if result.is_a?(Fixnum) && !request.defer.resolved?
|
86
85
|
# TODO:: setup timeout for async response
|
87
86
|
end
|
88
87
|
else
|
@@ -91,7 +90,6 @@ module SpiderGazelle
|
|
91
90
|
end
|
92
91
|
}
|
93
92
|
|
94
|
-
@async_callback = method(:async_callback)
|
95
93
|
@queue_response = method(:queue_response)
|
96
94
|
|
97
95
|
# The parser state for this instance
|
@@ -182,7 +180,7 @@ module SpiderGazelle
|
|
182
180
|
# Parser Callbacks
|
183
181
|
# ----------------
|
184
182
|
def start_parsing
|
185
|
-
@parsing = Request.new @thread, @app, @port, @remote_ip, @scheme
|
183
|
+
@parsing = Request.new @thread, @app, @port, @remote_ip, @scheme
|
186
184
|
end
|
187
185
|
|
188
186
|
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
@@ -190,6 +188,7 @@ module SpiderGazelle
|
|
190
188
|
@parsing.env[REQUEST_METHOD] = @state.http_method.to_s
|
191
189
|
end
|
192
190
|
|
191
|
+
ASYNC = "async.callback".freeze
|
193
192
|
def finished_parsing
|
194
193
|
request = @parsing
|
195
194
|
@parsing = nil
|
@@ -200,6 +199,12 @@ module SpiderGazelle
|
|
200
199
|
@socket.stop_read
|
201
200
|
end
|
202
201
|
|
202
|
+
# Process the async request in the same way as Mizuno
|
203
|
+
# See: http://polycrystal.org/2012/04/15/asynchronous_responses_in_rack.html
|
204
|
+
# Process a response that was marked as async.
|
205
|
+
request.env[ASYNC] = proc { |data|
|
206
|
+
@thread.schedule { request.defer.resolve(data) }
|
207
|
+
}
|
203
208
|
request.upgrade = @state.upgrade?
|
204
209
|
@requests << request
|
205
210
|
process_next unless @processing
|
@@ -228,28 +233,13 @@ module SpiderGazelle
|
|
228
233
|
|
229
234
|
EMPTY_RESPONSE = [''.freeze].freeze
|
230
235
|
def work
|
236
|
+
request = @processing
|
231
237
|
begin
|
232
|
-
|
238
|
+
[request, request.execute!]
|
233
239
|
rescue StandardError => e
|
234
240
|
@logger.print_error e, 'framework error'
|
235
241
|
@processing.keep_alive = false
|
236
|
-
[500, {}, EMPTY_RESPONSE]
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
# Process the async request in the same way as Mizuno
|
241
|
-
# See: http://polycrystal.org/2012/04/15/asynchronous_responses_in_rack.html
|
242
|
-
def async_callback(data)
|
243
|
-
@thread.schedule { callback(data) }
|
244
|
-
end
|
245
|
-
|
246
|
-
# Process a response that was marked as async. Save the data if the request hasn't responded yet
|
247
|
-
def callback(data)
|
248
|
-
request = @processing
|
249
|
-
if request && request.is_async
|
250
|
-
request.defer.resolve(data)
|
251
|
-
else
|
252
|
-
@logger.warn "Received async callback and there are no pending requests. Data was:\n#{data}"
|
242
|
+
[request, [500, {}, EMPTY_RESPONSE]]
|
253
243
|
end
|
254
244
|
end
|
255
245
|
|
@@ -477,14 +467,15 @@ module SpiderGazelle
|
|
477
467
|
end
|
478
468
|
end
|
479
469
|
|
480
|
-
ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\n\r\n".freeze
|
470
|
+
ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n".freeze
|
481
471
|
def send_parsing_error
|
482
472
|
@logger.info "Parsing error!"
|
473
|
+
@socket.stop_read
|
483
474
|
@socket.write ERROR_400_RESPONSE
|
484
475
|
@socket.shutdown
|
485
476
|
end
|
486
477
|
|
487
|
-
ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze
|
478
|
+
ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\nContent-Length: 0\r\n\r\n".freeze
|
488
479
|
def send_internal_error
|
489
480
|
@logger.info "Internal error"
|
490
481
|
@socket.stop_read
|
@@ -42,9 +42,8 @@ module SpiderGazelle
|
|
42
42
|
SERVER_PORT = "SERVER_PORT".freeze
|
43
43
|
REMOTE_ADDR = "REMOTE_ADDR".freeze
|
44
44
|
RACK_URL_SCHEME = "rack.url_scheme".freeze
|
45
|
-
ASYNC = "async.callback".freeze
|
46
45
|
|
47
|
-
def initialize(thread, app, port, remote_ip, scheme
|
46
|
+
def initialize(thread, app, port, remote_ip, scheme)
|
48
47
|
super(thread, thread.defer)
|
49
48
|
|
50
49
|
@app = app
|
@@ -55,7 +54,6 @@ module SpiderGazelle
|
|
55
54
|
@env[SERVER_PORT] = port
|
56
55
|
@env[REMOTE_ADDR] = remote_ip
|
57
56
|
@env[RACK_URL_SCHEME] = scheme
|
58
|
-
@env[ASYNC] = async_callback
|
59
57
|
end
|
60
58
|
|
61
59
|
|
@@ -85,6 +83,8 @@ module SpiderGazelle
|
|
85
83
|
USE_HTTP2 = 'h2c'.freeze
|
86
84
|
|
87
85
|
|
86
|
+
|
87
|
+
|
88
88
|
def execute!
|
89
89
|
@env[CONTENT_LENGTH] = @env.delete(HTTP_CONTENT_LENGTH) || @body.length
|
90
90
|
@env[CONTENT_TYPE] = @env.delete(HTTP_CONTENT_TYPE) || DEFAULT_TYPE
|
@@ -160,6 +160,9 @@ module SpiderGazelle
|
|
160
160
|
|
161
161
|
# isolation and process mode don't mix
|
162
162
|
options[:isolate] = false if options[:mode] == :process
|
163
|
+
|
164
|
+
# Force no_ipc mode on Windows (sockets over pipes are not working in threaded mode)
|
165
|
+
options[:mode] = :no_ipc if ::FFI::Platform.windows? && options[:mode] == :thread
|
163
166
|
end
|
164
167
|
|
165
168
|
options
|
data/lib/spider-gazelle.rb
CHANGED
@@ -2,6 +2,7 @@ require 'thread'
|
|
2
2
|
require 'singleton'
|
3
3
|
|
4
4
|
|
5
|
+
require 'spider-gazelle/version'
|
5
6
|
require 'spider-gazelle/options'
|
6
7
|
require 'spider-gazelle/logger'
|
7
8
|
require 'spider-gazelle/reactor'
|
@@ -9,8 +10,6 @@ require 'spider-gazelle/signaller'
|
|
9
10
|
|
10
11
|
|
11
12
|
module SpiderGazelle
|
12
|
-
VERSION = '2.0.1'.freeze
|
13
|
-
EXEC_NAME = 'sg'.freeze
|
14
13
|
INTERNAL_PIPE_BACKLOG = 128
|
15
14
|
|
16
15
|
# Signaller is used to communicate:
|
data/spec/http1_spec.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'spider-gazelle'
|
1
2
|
require 'spider-gazelle/gazelle/http1'
|
2
3
|
|
3
4
|
|
@@ -100,6 +101,53 @@ describe ::SpiderGazelle::Gazelle::Http1 do
|
|
100
101
|
])
|
101
102
|
end
|
102
103
|
|
104
|
+
it "should fill out the environment properly", http1: true do
|
105
|
+
app = lambda do |env|
|
106
|
+
expect(env['REQUEST_URI']).to eq('/?test=ing')
|
107
|
+
expect(env['REQUEST_PATH']).to eq('/')
|
108
|
+
expect(env['QUERY_STRING']).to eq('test=ing')
|
109
|
+
expect(env['SERVER_NAME']).to eq('spider.gazelle.net')
|
110
|
+
expect(env['SERVER_PORT']).to eq(3000)
|
111
|
+
expect(env['REMOTE_ADDR']).to eq('127.0.0.1')
|
112
|
+
expect(env['rack.url_scheme']).to eq('http')
|
113
|
+
|
114
|
+
body = 'Hello, World!'
|
115
|
+
[200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
|
116
|
+
end
|
117
|
+
|
118
|
+
@loop.run {
|
119
|
+
@http1.load(@socket, @port, app, @app_mode, @tls)
|
120
|
+
@http1.parse("GET /?test=ing HTTP/1.1\r\nHost: spider.gazelle.net:3000\r\nConnection: Close\r\n\r\n")
|
121
|
+
}
|
122
|
+
|
123
|
+
expect(@shutdown_called).to be == 1
|
124
|
+
expect(@close_called).to be == 0
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should respond with a chunked response", http1: true do
|
128
|
+
app = lambda do |env|
|
129
|
+
body = ['Hello', ',', ' World!']
|
130
|
+
[200, {'Content-Type' => 'text/plain'}, body]
|
131
|
+
end
|
132
|
+
writes = []
|
133
|
+
|
134
|
+
@loop.run {
|
135
|
+
@http1.load(@socket, @port, app, @app_mode, @tls)
|
136
|
+
@http1.parse("GET / HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
137
|
+
|
138
|
+
@socket.write_cb = proc { |data|
|
139
|
+
writes << data
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
expect(@shutdown_called).to be == 1
|
144
|
+
expect(@close_called).to be == 0
|
145
|
+
expect(writes).to eq([
|
146
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n",
|
147
|
+
"5\r\nHello\r\n", "1\r\n,\r\n", "7\r\n World!\r\n", "0\r\n\r\n"
|
148
|
+
])
|
149
|
+
end
|
150
|
+
|
103
151
|
it "should process a single request and keep the connection open", http1: true do
|
104
152
|
app = lambda do |env|
|
105
153
|
body = 'Hello, World!'
|
@@ -129,33 +177,162 @@ describe ::SpiderGazelle::Gazelle::Http1 do
|
|
129
177
|
])
|
130
178
|
end
|
131
179
|
|
132
|
-
it "should
|
180
|
+
it "should process pipelined requests in order", http1: true do
|
181
|
+
current = 0
|
182
|
+
order = []
|
133
183
|
app = lambda do |env|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
184
|
+
case env['REQUEST_PATH']
|
185
|
+
when '/1'
|
186
|
+
order << 1
|
187
|
+
current = 1
|
188
|
+
body = 'Hello, World!'
|
189
|
+
[200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
|
190
|
+
when '/2'
|
191
|
+
order << 2
|
192
|
+
current = 2
|
193
|
+
body = ['Hello,', ' World!']
|
194
|
+
[200, {'Content-Type' => 'text/plain'}, body]
|
195
|
+
when '/3'
|
196
|
+
order << 3
|
197
|
+
current = 3
|
198
|
+
body = 'Happiness'
|
199
|
+
[200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
|
200
|
+
when '/4'
|
201
|
+
order << 4
|
202
|
+
current = 4
|
203
|
+
body = 'is a warm gun'
|
204
|
+
[200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
|
205
|
+
end
|
206
|
+
end
|
141
207
|
|
142
|
-
|
143
|
-
|
208
|
+
writes = []
|
209
|
+
@loop.run {
|
210
|
+
@http1.load(@socket, @port, app, @app_mode, @tls)
|
211
|
+
@http1.parse("GET /1 HTTP/1.1\r\n\r\nGET /2 HTTP/1.1\r\n\r\nGET /3 HTTP/1.1\r\n\r\n")
|
212
|
+
@http1.parse("GET /4 HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
213
|
+
|
214
|
+
@socket.write_cb = proc { |data|
|
215
|
+
order << current
|
216
|
+
writes << data
|
217
|
+
}
|
218
|
+
}
|
219
|
+
|
220
|
+
expect(@shutdown_called).to be == 1
|
221
|
+
expect(@close_called).to be == 0
|
222
|
+
expect(order).to eq([1,1,1, 2,2,2,2,2, 3,3,3, 4,4,4])
|
223
|
+
expect(writes).to eq([
|
224
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\n",
|
225
|
+
"Hello, World!",
|
226
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n",
|
227
|
+
"6\r\nHello,\r\n", "7\r\n World!\r\n", "0\r\n\r\n",
|
228
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 9\r\n\r\n",
|
229
|
+
"Happiness",
|
230
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\nConnection: close\r\n\r\n",
|
231
|
+
"is a warm gun"
|
232
|
+
])
|
233
|
+
end
|
234
|
+
|
235
|
+
it "should process a single async request and close the connection", http1: true do
|
236
|
+
app = lambda do |env|
|
237
|
+
expect(env['SERVER_PORT']).to eq(80)
|
238
|
+
|
239
|
+
Thread.new do
|
240
|
+
body = 'Hello, World!'
|
241
|
+
env['async.callback'].call [200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
|
242
|
+
end
|
243
|
+
|
244
|
+
throw :async
|
144
245
|
end
|
246
|
+
writes = []
|
145
247
|
|
146
248
|
@loop.run {
|
147
249
|
@http1.load(@socket, @port, app, @app_mode, @tls)
|
148
|
-
@http1.parse("GET
|
250
|
+
@http1.parse("GET / HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
251
|
+
|
252
|
+
@socket.write_cb = proc { |data|
|
253
|
+
writes << data
|
254
|
+
}
|
149
255
|
}
|
150
256
|
|
151
257
|
expect(@shutdown_called).to be == 1
|
152
258
|
expect(@close_called).to be == 0
|
259
|
+
expect(writes).to eq([
|
260
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\nConnection: close\r\n\r\n",
|
261
|
+
"Hello, World!"
|
262
|
+
])
|
153
263
|
end
|
154
264
|
|
155
|
-
it "should
|
265
|
+
it "should process pipelined async requests in order", http1: true do
|
266
|
+
current = 0
|
267
|
+
order = []
|
156
268
|
app = lambda do |env|
|
157
|
-
|
158
|
-
|
269
|
+
Thread.new do
|
270
|
+
env['async.callback'].call case env['REQUEST_PATH']
|
271
|
+
when '/1'
|
272
|
+
order << 1
|
273
|
+
current = 1
|
274
|
+
body = 'Hello, World!'
|
275
|
+
[200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
|
276
|
+
when '/2'
|
277
|
+
order << 2
|
278
|
+
current = 2
|
279
|
+
body = ['Hello,', ' World!']
|
280
|
+
[200, {'Content-Type' => 'text/plain'}, body]
|
281
|
+
when '/3'
|
282
|
+
order << 3
|
283
|
+
current = 3
|
284
|
+
body = 'Happiness'
|
285
|
+
[200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
|
286
|
+
when '/4'
|
287
|
+
order << 4
|
288
|
+
current = 4
|
289
|
+
body = 'is a warm gun'
|
290
|
+
[200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
throw :async
|
295
|
+
end
|
296
|
+
|
297
|
+
writes = []
|
298
|
+
@loop.run {
|
299
|
+
@http1.load(@socket, @port, app, @app_mode, @tls)
|
300
|
+
@http1.parse("GET /1 HTTP/1.1\r\n\r\nGET /2 HTTP/1.1\r\n\r\nGET /3 HTTP/1.1\r\n\r\n")
|
301
|
+
@http1.parse("GET /4 HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
302
|
+
|
303
|
+
@socket.write_cb = proc { |data|
|
304
|
+
order << current
|
305
|
+
writes << data
|
306
|
+
}
|
307
|
+
}
|
308
|
+
|
309
|
+
expect(@shutdown_called).to be == 1
|
310
|
+
expect(@close_called).to be == 0
|
311
|
+
expect(order).to eq([1,1,1, 2,2,2,2,2, 3,3,3, 4,4,4])
|
312
|
+
expect(writes).to eq([
|
313
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\n",
|
314
|
+
"Hello, World!",
|
315
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n",
|
316
|
+
"6\r\nHello,\r\n", "7\r\n World!\r\n", "0\r\n\r\n",
|
317
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 9\r\n\r\n",
|
318
|
+
"Happiness",
|
319
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\nConnection: close\r\n\r\n",
|
320
|
+
"is a warm gun"
|
321
|
+
])
|
322
|
+
end
|
323
|
+
|
324
|
+
it "should process a single async request and not suffer from race conditions", http1: true do
|
325
|
+
app = lambda do |env|
|
326
|
+
expect(env['SERVER_PORT']).to eq(80)
|
327
|
+
|
328
|
+
Thread.new do
|
329
|
+
body = 'Hello, World!'
|
330
|
+
env['async.callback'].call [200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
|
331
|
+
end
|
332
|
+
|
333
|
+
sleep 0.5
|
334
|
+
|
335
|
+
throw :async
|
159
336
|
end
|
160
337
|
writes = []
|
161
338
|
|
@@ -170,6 +347,12 @@ describe ::SpiderGazelle::Gazelle::Http1 do
|
|
170
347
|
|
171
348
|
expect(@shutdown_called).to be == 1
|
172
349
|
expect(@close_called).to be == 0
|
173
|
-
expect(writes).to eq([
|
350
|
+
expect(writes).to eq([
|
351
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\nConnection: close\r\n\r\n",
|
352
|
+
"Hello, World!"
|
353
|
+
])
|
354
|
+
|
355
|
+
# Allow the worker thread to complete so we exit cleanly
|
356
|
+
sleep 1
|
174
357
|
end
|
175
358
|
end
|
data/spider-gazelle.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spider-gazelle
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen von Takach
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-09-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -167,6 +167,7 @@ files:
|
|
167
167
|
- lib/spider-gazelle/spider.rb
|
168
168
|
- lib/spider-gazelle/spider/binding.rb
|
169
169
|
- lib/spider-gazelle/upgrades/websocket.rb
|
170
|
+
- lib/spider-gazelle/version.rb
|
170
171
|
- spec/http1_spec.rb
|
171
172
|
- spec/rack_lock_spec.rb
|
172
173
|
- spider-gazelle.gemspec
|