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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d572d56238befc1bb320a35caa722a5b5862de98
4
- data.tar.gz: 543e26733659f208fd6f2cbb8c634a2082b3a01b
3
+ metadata.gz: ce0882fd3ec8b1ae9319cff47eb1ed6504b18c2e
4
+ data.tar.gz: 9ac4f207d5fb67c773ae7775d42f61b1dfc8a309
5
5
  SHA512:
6
- metadata.gz: 053e8b2128d068779d21d5ffd8f312265895b61d7da0ccceb8dea262c2fb6026dbcd3bf4f41bdc4f7809a0d7a6bc3baf4a56be58a42a31d05e33cad4b7af1eb6
7
- data.tar.gz: 8c8a6ee3c20692c3173b2a0240818223cf4cd9c967f007db3fde7e6186f273234c81e7419906b787325c5e39ff0b6e6ae3e858e0625ad9f13de7ca8b3256638f
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.png" />](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
+ [![Build Status](https://travis-ci.org/cotag/spider-gazelle.svg?branch=master)](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](/bin/sg)
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
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'spider-gazelle/version'
4
+ puts "Launching spider-gazelle v#{::SpiderGazelle::VERSION}"
3
5
  require 'spider-gazelle'
4
6
  SpiderGazelle::LaunchControl.instance.exec(ARGV)
@@ -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? Fixnum
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, @async_callback
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
- @processing.execute!
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, async_callback)
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
@@ -0,0 +1,5 @@
1
+
2
+ module SpiderGazelle
3
+ VERSION = '2.0.2'.freeze
4
+ EXEC_NAME = 'sg'.freeze
5
+ end
@@ -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 fill out the environment properly", http1: true do
180
+ it "should process pipelined requests in order", http1: true do
181
+ current = 0
182
+ order = []
133
183
  app = lambda do |env|
134
- expect(env['REQUEST_URI']).to eq('/?test=ing')
135
- expect(env['REQUEST_PATH']).to eq('/')
136
- expect(env['QUERY_STRING']).to eq('test=ing')
137
- expect(env['SERVER_NAME']).to eq('spider.gazelle.net')
138
- expect(env['SERVER_PORT']).to eq(3000)
139
- expect(env['REMOTE_ADDR']).to eq('127.0.0.1')
140
- expect(env['rack.url_scheme']).to eq('http')
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
- body = 'Hello, World!'
143
- [200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
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 /?test=ing HTTP/1.1\r\nHost: spider.gazelle.net:3000\r\nConnection: Close\r\n\r\n")
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 respond with a chunked response", http1: true do
265
+ it "should process pipelined async requests in order", http1: true do
266
+ current = 0
267
+ order = []
156
268
  app = lambda do |env|
157
- body = ['Hello', ',', ' World!']
158
- [200, {'Content-Type' => 'text/plain'}, body]
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(["HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n", "5\r\nHello\r\n", "1\r\n,\r\n", "7\r\n World!\r\n", "0\r\n\r\n"])
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
@@ -1,7 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
- require 'spider-gazelle'
4
+ require 'spider-gazelle/version'
5
5
  version = SpiderGazelle::VERSION
6
6
 
7
7
  Gem::Specification.new do |s|
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.1
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-08-23 00:00:00.000000000 Z
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