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.
@@ -1,8 +1,8 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
- require 'spider-gazelle/const'
5
- version = SpiderGazelle::Const::VERSION
4
+ require 'spider-gazelle'
5
+ version = SpiderGazelle::VERSION
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "spider-gazelle"
@@ -20,11 +20,11 @@ Gem::Specification.new do |s|
20
20
 
21
21
  s.add_dependency 'rake'
22
22
  s.add_dependency 'http-parser' # Ruby FFI bindings for https://github.com/joyent/http-parser
23
- s.add_dependency 'libuv', '>= 2.0.0' # Ruby FFI bindings for https://github.com/libuv/libuv
23
+ s.add_dependency 'libuv', '>= 2.0.5' # Ruby FFI bindings for https://github.com/libuv/libuv
24
+ s.add_dependency 'uv-rays','>= 1.2.0' # Provides buffering tools
24
25
  s.add_dependency 'rack', '>= 1.0.0' # Ruby web server interface
25
26
  s.add_dependency 'websocket-driver' # Websocket parser
26
- s.add_dependency 'thread_safe' # Thread safe hashes
27
- s.add_dependency 'radix' # Converts numbers to the unicode representation
27
+ s.add_dependency 'http-2' # HTTP2 parsing and response management
28
28
 
29
29
  s.add_development_dependency 'rspec' # Testing framework
30
30
  s.add_development_dependency 'yard' # Comment based documentation generation
@@ -34,7 +34,7 @@ Gem::Specification.new do |s|
34
34
  s.extra_rdoc_files = ["README.md"]
35
35
 
36
36
  s.bindir = 'bin'
37
- s.executables = ['sg']
37
+ s.executables = [SpiderGazelle::EXEC_NAME]
38
38
 
39
39
  s.require_paths = ["lib"]
40
40
  end
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: 1.2.0
4
+ version: 2.0.0
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-22 00:00:00.000000000 Z
11
+ date: 2015-08-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -44,44 +44,44 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 2.0.0
47
+ version: 2.0.5
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 2.0.0
54
+ version: 2.0.5
55
55
  - !ruby/object:Gem::Dependency
56
- name: rack
56
+ name: uv-rays
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: 1.0.0
61
+ version: 1.2.0
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: 1.0.0
68
+ version: 1.2.0
69
69
  - !ruby/object:Gem::Dependency
70
- name: websocket-driver
70
+ name: rack
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: '0'
75
+ version: 1.0.0
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: 1.0.0
83
83
  - !ruby/object:Gem::Dependency
84
- name: thread_safe
84
+ name: websocket-driver
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
@@ -95,7 +95,7 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: radix
98
+ name: http-2
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
@@ -155,14 +155,19 @@ files:
155
155
  - lib/rack/handler/spider-gazelle.rb
156
156
  - lib/rack/lock_patch.rb
157
157
  - lib/spider-gazelle.rb
158
- - lib/spider-gazelle/app_store.rb
159
- - lib/spider-gazelle/binding.rb
160
- - lib/spider-gazelle/connection.rb
161
- - lib/spider-gazelle/const.rb
162
158
  - lib/spider-gazelle/gazelle.rb
163
- - lib/spider-gazelle/request.rb
159
+ - lib/spider-gazelle/gazelle/app_store.rb
160
+ - lib/spider-gazelle/gazelle/http1.rb
161
+ - lib/spider-gazelle/gazelle/request.rb
162
+ - lib/spider-gazelle/logger.rb
163
+ - lib/spider-gazelle/options.rb
164
+ - lib/spider-gazelle/reactor.rb
165
+ - lib/spider-gazelle/signaller.rb
166
+ - lib/spider-gazelle/signaller/signal_parser.rb
164
167
  - lib/spider-gazelle/spider.rb
168
+ - lib/spider-gazelle/spider/binding.rb
165
169
  - lib/spider-gazelle/upgrades/websocket.rb
170
+ - spec/http1_spec.rb
166
171
  - spec/rack_lock_spec.rb
167
172
  - spider-gazelle.gemspec
168
173
  homepage: https://github.com/cotag/spider-gazelle
@@ -190,4 +195,6 @@ signing_key:
190
195
  specification_version: 4
191
196
  summary: A fast, parallel and concurrent web server for ruby
192
197
  test_files:
198
+ - spec/http1_spec.rb
193
199
  - spec/rack_lock_spec.rb
200
+ has_rdoc:
@@ -1,64 +0,0 @@
1
- require 'thread'
2
- require 'radix/base'
3
-
4
- module SpiderGazelle
5
- module AppStore
6
- # Basic compression using UTF (more efficient for ID's stored as strings)
7
- B65 = ::Radix::Base.new(::Radix::BASE::B62 + ['-', '_', '~'])
8
- B10 = ::Radix::Base.new(10)
9
-
10
- @mutex = Mutex.new
11
- @apps = ThreadSafe::Cache.new
12
- @loaded = ThreadSafe::Cache.new
13
- @count = 0
14
-
15
- # Load an app and assign it an ID
16
- def self.load(app, options={})
17
- is_rack_app = !app.is_a?(String)
18
- app_key = is_rack_app ? app.class.name.to_sym : app.to_sym
19
- id = @loaded[app_key]
20
-
21
- if id.nil?
22
- app, options = ::Rack::Builder.parse_file(app) unless is_rack_app
23
-
24
- count = 0
25
- @mutex.synchronize { count = @count += 1 }
26
- id = Radix.convert(count, B10, B65).to_sym
27
- @apps[id] = app
28
- @loaded[app_key] = id
29
- end
30
-
31
- id
32
- end
33
-
34
- # Manually load an app
35
- def self.add(app)
36
- id = @loaded[app.__id__]
37
-
38
- if id.nil?
39
- count = 0
40
- @mutex.synchronize { count = @count += 1 }
41
- id = Radix.convert(count, B10, B65).to_sym
42
- @apps[id] = app
43
- @loaded[app.__id__] = id
44
- end
45
-
46
- id
47
- end
48
-
49
- # Lookup an application
50
- def self.lookup(app)
51
- if app.is_a?(String) || app.is_a?(Symbol)
52
- @apps[@loaded[app.to_sym]]
53
- else
54
- @apps[@loaded[app.__id__]]
55
- end
56
- end
57
-
58
- # Get an app using the id directly
59
- def self.get(id)
60
- id = id.to_sym if id.is_a?(String)
61
- @apps[id]
62
- end
63
- end
64
- end
@@ -1,53 +0,0 @@
1
- require 'spider-gazelle/const'
2
- require 'set'
3
- require 'thread'
4
-
5
- module SpiderGazelle
6
- class Binding
7
- include Const
8
-
9
- attr_reader :app_id
10
-
11
- def initialize(loop, delegate, app_id, options = {})
12
- @app_id = app_id
13
- @options = options
14
- @loop = loop
15
- @delegate = delegate
16
- @tls = @options[:tls] || false
17
- @port = @options[:Port] || (@tls ? PORT_443 : PORT_80)
18
- @optimize = @options[:optimize_for_latency] || true
19
-
20
- # Connection management functions
21
- @accept_connection = method :accept_connection
22
- end
23
-
24
- # Bind the application to the selected port
25
- def bind
26
- # Bind the socket
27
- @tcp = @loop.tcp
28
- @tcp.bind @options[:Host], @port, @accept_connection
29
- @tcp.listen @options[:backlog]
30
-
31
- # Delegate errors
32
- @tcp.catch { |e| @loop.log(:error, 'application bind failed', e) }
33
- @tcp
34
- end
35
-
36
- # Close the bindings
37
- def unbind
38
- # close unless we've never been bound
39
- @tcp.close unless @tcp.nil?
40
- @tcp
41
- end
42
-
43
- protected
44
-
45
- # Once the connection is accepted we disable Nagles Algorithm
46
- # This improves performance as we are using vectored or scatter/gather IO
47
- # Then the spider delegates to the gazelle loops
48
- def accept_connection(client)
49
- client.enable_nodelay if @optimize == true
50
- @delegate.call client, @tls, @port, @app_id
51
- end
52
- end
53
- end
@@ -1,371 +0,0 @@
1
- require 'spider-gazelle/const'
2
- require 'digest/md5'
3
- require 'stringio'
4
-
5
- module SpiderGazelle
6
- class Connection
7
- include Const
8
-
9
- Hijack = Struct.new :socket, :env
10
-
11
- def self.on_progress(data, socket); end
12
- DUMMY_PROGRESS = self.method :on_progress
13
-
14
- # For Gazelle
15
- attr_reader :state, :parsing
16
- # For Request
17
- attr_reader :tls, :port, :loop, :socket, :async_callback
18
-
19
- def initialize(gazelle, loop, socket, port, state, app, queue)
20
- # A single parser instance per-connection (supports pipelining)
21
- @state = state
22
- @pending = []
23
-
24
- # Work callback for thread pool processing
25
- @request = nil
26
- @work = method :work
27
-
28
- # Called after the work on the thread pool is complete
29
- @send_response = method :send_response
30
- @send_error = method :send_error
31
-
32
- # Used to chain promises (ensures requests are processed in order)
33
- @process_next = method :process_next
34
- # Keep track of work queue head to prevent unintentional GC
35
- @current_worker = queue
36
- # Start queue with an existing resolved promise (::Libuv::Q::ResolvedPromise.new(@loop, true))
37
- @queue_worker = queue
38
-
39
- # Socket for writing the response
40
- @socket = socket
41
- @app = app
42
- @port = port
43
- @tls = @socket.tls?
44
- @loop = loop
45
- @gazelle = gazelle
46
- @async_callback = method :deferred_callback
47
-
48
- # Remove connection if the socket closes
49
- socket.finally &method(:unlink)
50
- end
51
-
52
- # Lazy eval the IP
53
- def remote_ip
54
- @remote_ip ||= @socket.peername[0]
55
- end
56
-
57
- # Creates a new request state object
58
- def start_parsing
59
- @parsing = Request.new self, @app
60
- end
61
-
62
- # Chains the work in a promise queue
63
- def finished_parsing
64
- if !@state.keep_alive?
65
- @parsing.keep_alive = false
66
- # We don't want to do any more work than we need to
67
- @socket.stop_read
68
- end
69
-
70
- @parsing.upgrade = @state.upgrade?
71
- @pending.push @parsing
72
- @queue_worker = @queue_worker.then @process_next
73
- end
74
-
75
- # The parser encountered an error
76
- def parsing_error
77
- # Grab the error
78
- send_error @state.error
79
-
80
- # We no longer care for any further requests from this client
81
- # however we will finish processing any valid pipelined requests before shutting down
82
- @socket.stop_read
83
- @queue_worker = @queue_worker.then do
84
- @socket.write ERROR_400_RESPONSE
85
- @socket.shutdown
86
- end
87
- end
88
-
89
- # Schedule send
90
- def response(data)
91
- @loop.schedule
92
- end
93
-
94
- protected
95
-
96
- ##
97
- # State handlers
98
-
99
- # Called when an error occurs at any point while responding
100
- def send_error(reason)
101
- # Close the socket as this is fatal (file read error, gazelle error etc)
102
- @socket.close
103
-
104
- # Log the error in a worker thread
105
- @loop.work do
106
- msg = "connection error: #{reason.message}\n#{reason.backtrace.join("\n") if reason.backtrace}\n"
107
- @gazelle.logger.error msg
108
- end
109
- end
110
-
111
- # We use promise chaining to move the requests forward
112
- # This provides an elegant way to handle persistent and pipelined connections
113
- def process_next(result)
114
- @request = @pending.shift
115
- @current_worker = @loop.work @work
116
- # Resolves the promise with a promise
117
- @current_worker.then @send_response, @send_error
118
- end
119
-
120
- # Returns the response as the result of the work
121
- # We support the unofficial rack async api (multi-call version for chunked responses)
122
- def work
123
- begin
124
- @request.execute!
125
- rescue => e
126
- @gazelle.logger.error "framework error: #{e.message}\n#{e.backtrace.join("\n") if e.backtrace}\n"
127
- @request.keep_alive = false
128
- [500, {}, EMPTY_RESPONSE]
129
- end
130
- end
131
-
132
- # Unlinks the connection from the rack app
133
- # This occurs when requested and when the socket closes
134
- def unlink
135
- if @gazelle
136
- # Unlink the progress callback (prevent funny business)
137
- @socket.progress &DUMMY_PROGRESS
138
- @gazelle.discard self
139
- @gazelle = nil
140
- @state = nil
141
- end
142
- end
143
-
144
- ##
145
- # Core response handlers
146
-
147
- def send_response(result)
148
- # As we have come back from another thread the socket may have closed
149
- # This check is an optimisation, the call to write and shutdown would fail safely
150
-
151
- if @request.hijacked
152
- # Unlink the management of the socket
153
- unlink
154
-
155
- # Pass the hijack response to the captor using the promise. This forwards the socket and
156
- # environment as well as moving continued execution onto the event loop.
157
- @request.hijacked.resolve Hijack.new(@socket, @request.env)
158
- elsif @socket.closed
159
- unless result.nil? || @request.deferred
160
- body = result[2]
161
- body.close if body.respond_to?(:close)
162
- end
163
- else
164
- if @request.deferred
165
- # Wait for the response using this promise
166
- promise = @request.deferred.promise
167
-
168
- # Process any responses that might have made it here first
169
- if @deferred_responses
170
- @deferred_responses.each &method(:respond_with)
171
- @deferred_responses = nil
172
- end
173
-
174
- return promise
175
- elsif result
176
- # clear any cached responses just in case
177
- # could be set by error in the rack application
178
- @deferred_responses = nil if @deferred_responses
179
-
180
- status, headers, body = result
181
-
182
- send_body = @request.env[REQUEST_METHOD] != HEAD
183
-
184
- # If a file, stream the body in a non-blocking fashion
185
- if body.respond_to? :to_path
186
- file = @loop.file body.to_path, File::RDONLY
187
-
188
- # Send the body in parallel without blocking the next request in dev
189
- # Also if this is a head request we still want the body closed
190
- body.close if body.respond_to?(:close)
191
-
192
- file.progress do
193
- statprom = file.stat
194
- statprom.then do |stats|
195
- headers[ETAG] = ::Digest::MD5.hexdigest "#{stats[:st_mtim][:tv_sec]}#{body.to_path}"
196
-
197
- if headers[CONTENT_LENGTH2]
198
- type = :raw
199
- else
200
- type = :http
201
- headers[TRANSFER_ENCODING] = CHUNKED
202
- end
203
-
204
- write_headers status, headers
205
-
206
- if send_body
207
- # File is open and available for reading
208
- file.send_file(@socket, type).finally do
209
- file.close
210
- @socket.shutdown if @request.keep_alive == false
211
- end
212
- else
213
- file.close
214
- @socket.shutdown if @request.keep_alive == false
215
- end
216
- end
217
-
218
- # Ensure the file is closed if there is an error
219
- statprom.catch do |reason|
220
- file.close
221
- @loop.work do
222
- msg = "connection error: #{reason.message}\n#{reason.backtrace.join("\n") if reason.backtrace}\n"
223
- @gazelle.logger.error msg
224
- end
225
-
226
- send_response [500, {}, EMPTY_RESPONSE]
227
- end
228
- end
229
-
230
- return file
231
- else
232
- # Optimize the response
233
- begin
234
- if body.size < 2
235
- headers[CONTENT_LENGTH2] = body.size == 1 ? body[0].bytesize : ZERO
236
- end
237
- rescue # just in case
238
- end
239
-
240
- if send_body
241
- write_response status, headers, body
242
- else
243
- body.close if body.respond_to?(:close)
244
- write_headers status, headers
245
- @socket.shutdown if @request.keep_alive == false
246
- end
247
- end
248
- end
249
- end
250
-
251
- # continue processing (don't wait for write to complete)
252
- # if the write fails it will close the socket
253
- nil
254
- end
255
-
256
- def write_response(status, headers, body)
257
- if headers[CONTENT_LENGTH2]
258
- headers[CONTENT_LENGTH2] = headers[CONTENT_LENGTH2].to_s
259
- write_headers status, headers
260
-
261
- # Stream the response (pass directly into @socket.write)
262
- body.each &@socket.method(:write)
263
-
264
- if @request.deferred
265
- @request.deferred.resolve true
266
- # Prevent data being sent after completed
267
- @request.deferred = nil
268
- end
269
-
270
- @socket.shutdown if @request.keep_alive == false
271
- else
272
- headers[TRANSFER_ENCODING] = CHUNKED
273
- write_headers status, headers
274
-
275
- # Stream the response
276
- @write_chunk ||= method :write_chunk
277
- body.each &@write_chunk
278
-
279
- if @request.deferred.nil?
280
- @socket.write CLOSE_CHUNKED
281
- @socket.shutdown if @request.keep_alive == false
282
- else
283
- @async_state = :chunked
284
- end
285
- end
286
-
287
- body.close if body.respond_to?(:close)
288
- end
289
-
290
- def add_header(header, key, value)
291
- header << key
292
- header << COLON_SPACE
293
- header << value
294
- header << LINE_END
295
- end
296
-
297
- def write_headers(status, headers)
298
- headers[CONNECTION] = CLOSE if @request.keep_alive == false
299
-
300
- header = "HTTP/1.1 #{status} #{fetch_code(status)}\r\n"
301
- headers.each do |key, value|
302
- next if key.start_with? RACK
303
- value.to_s.split(NEWLINE).each {|val| add_header(header, key, val)}
304
- end
305
- header << LINE_END
306
- @socket.write header
307
- end
308
-
309
- def write_chunk(part)
310
- chunk = part.bytesize.to_s(HEX_SIZE_CHUNKED_RESPONSE) << LINE_END << part << LINE_END
311
- @socket.write chunk
312
- end
313
-
314
- def fetch_code(status)
315
- HTTP_STATUS_CODES.fetch(status, &HTTP_STATUS_DEFAULT)
316
- end
317
-
318
- ##
319
- # Async response functions
320
-
321
- # Callback from a response that was marked async
322
- def deferred_callback(data)
323
- @loop.next_tick { callback(data) }
324
- end
325
-
326
- # Process a response that was marked as async. Save the data if the request hasn't responded yet
327
- def callback(data)
328
- begin
329
- if @request.deferred && @deferred_responses.nil?
330
- respond_with data
331
- else
332
- @deferred_responses ||= []
333
- @deferred_responses << data
334
- end
335
- rescue Exception => e
336
- # This provides the same level of protection that the regular responses provide
337
- send_error e
338
- end
339
- end
340
-
341
- # Process the async request in the same way as Mizuno
342
- # See: http://polycrystal.org/2012/04/15/asynchronous_responses_in_rack.html
343
- def respond_with(data)
344
- status, headers, body = data
345
-
346
- if @async_state.nil?
347
- # Respond with the headers here
348
- write_response status, headers, body
349
- elsif body.empty?
350
- body.close if body.respond_to?(:close)
351
-
352
- @socket.write CLOSE_CHUNK
353
- @socket.shutdown if @request.keep_alive == false
354
-
355
- # Complete the request here
356
- deferred = @request.deferred
357
- # Prevent data being sent after completed
358
- @request.deferred = nil
359
- @async_state = nil
360
- deferred.resolve true
361
- else
362
- # Send the chunks provided
363
- @write_chunk ||= method :write_chunk
364
- body.each &@write_chunk
365
- body.close if body.respond_to?(:close)
366
- end
367
-
368
- nil
369
- end
370
- end
371
- end