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.
- checksums.yaml +4 -4
- data/bin/sg +1 -64
- data/lib/rack/handler/spider-gazelle.rb +17 -26
- data/lib/rack/lock_patch.rb +27 -27
- data/lib/spider-gazelle.rb +165 -16
- data/lib/spider-gazelle/gazelle.rb +151 -134
- data/lib/spider-gazelle/gazelle/app_store.rb +86 -0
- data/lib/spider-gazelle/gazelle/http1.rb +496 -0
- data/lib/spider-gazelle/gazelle/request.rb +155 -0
- data/lib/spider-gazelle/logger.rb +122 -0
- data/lib/spider-gazelle/options.rb +213 -0
- data/lib/spider-gazelle/reactor.rb +69 -0
- data/lib/spider-gazelle/signaller.rb +214 -0
- data/lib/spider-gazelle/signaller/signal_parser.rb +66 -0
- data/lib/spider-gazelle/spider.rb +305 -343
- data/lib/spider-gazelle/spider/binding.rb +80 -0
- data/lib/spider-gazelle/upgrades/websocket.rb +92 -88
- data/spec/http1_spec.rb +173 -0
- data/spec/rack_lock_spec.rb +97 -97
- data/spider-gazelle.gemspec +6 -6
- metadata +24 -17
- data/lib/spider-gazelle/app_store.rb +0 -64
- data/lib/spider-gazelle/binding.rb +0 -53
- data/lib/spider-gazelle/connection.rb +0 -371
- data/lib/spider-gazelle/const.rb +0 -206
- data/lib/spider-gazelle/request.rb +0 -103
data/spider-gazelle.gemspec
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
$:.push File.expand_path("../lib", __FILE__)
|
3
3
|
|
4
|
-
require 'spider-gazelle
|
5
|
-
version = SpiderGazelle::
|
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',
|
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 '
|
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 = [
|
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:
|
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-
|
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.
|
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.
|
54
|
+
version: 2.0.5
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
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.
|
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.
|
68
|
+
version: 1.2.0
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: rack
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
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:
|
82
|
+
version: 1.0.0
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
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:
|
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/
|
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
|