spider-gazelle 0.1.0 → 0.1.1
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 +46 -17
- data/lib/spider-gazelle.rb +5 -0
- data/lib/spider-gazelle/app_store.rb +67 -0
- data/lib/spider-gazelle/binding.rb +72 -0
- data/lib/spider-gazelle/connection.rb +255 -25
- data/lib/spider-gazelle/gazelle.rb +52 -30
- data/lib/spider-gazelle/request.rb +40 -40
- data/lib/spider-gazelle/spider.rb +292 -139
- data/lib/spider-gazelle/upgrades/websocket.rb +83 -32
- data/lib/spider-gazelle/version.rb +1 -1
- data/spider-gazelle.gemspec +2 -0
- metadata +32 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6835ff8fc79a090958589cc6c64474d27446b98
|
4
|
+
data.tar.gz: be7c36e4a3a219ee14ee0c35d4099f534b2fa9ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 52c7dabc77dbe369030abff53cf86b1c120601de0fc88a03f5e84ce888c7513c99bbe7c2281597d525cb80eac92137e3ebb78e18fd2b0298f2a0b1f0ee47454d
|
7
|
+
data.tar.gz: 4b97ce2d2131da24f716d438c0d82c1e76ed7c94c3b5acdf9263c9175a0b7c8152737e7113ead316116c4fcfae7752ddb2b9548aef6ece7a6336ce22bc06d4d9
|
data/bin/sg
CHANGED
@@ -6,23 +6,25 @@ require 'optparse'
|
|
6
6
|
|
7
7
|
|
8
8
|
options = {
|
9
|
-
|
10
|
-
|
9
|
+
host: "0.0.0.0",
|
10
|
+
port: 3000,
|
11
|
+
tls: false,
|
11
12
|
environment: ENV['RACK_ENV'] || 'development',
|
12
|
-
rackup: "#{Dir.pwd}/config.ru"
|
13
|
+
rackup: "#{Dir.pwd}/config.ru",
|
14
|
+
quiet: false
|
13
15
|
}
|
14
16
|
|
15
17
|
parser = OptionParser.new do |opts|
|
16
18
|
opts.on "-p", "--port PORT", Integer, "Define what port TCP port to bind to (default: 3000)" do |arg|
|
17
|
-
options[:
|
19
|
+
options[:port] = arg
|
18
20
|
end
|
19
21
|
|
20
22
|
opts.on "-a", "--address HOST", "bind to HOST address (default: 0.0.0.0)" do |arg|
|
21
|
-
options[:
|
23
|
+
options[:host] = arg
|
22
24
|
end
|
23
25
|
|
24
|
-
opts.on "-q", "--quiet", "
|
25
|
-
options[:
|
26
|
+
opts.on "-q", "--quiet", "quiet down the output" do
|
27
|
+
options[:quiet] = true
|
26
28
|
end
|
27
29
|
|
28
30
|
opts.on "-e", "--environment ENVIRONMENT", "The environment to run the Rack app on (default: development)" do |arg|
|
@@ -32,6 +34,14 @@ parser = OptionParser.new do |opts|
|
|
32
34
|
opts.on "-r", "--rackup FILE", "Load Rack config from this file (default: config.ru)" do |arg|
|
33
35
|
options[:rackup] = arg
|
34
36
|
end
|
37
|
+
|
38
|
+
opts.on "-l", "--logfile FILE", "Location of the servers log file (default: logs/server.log)" do |arg|
|
39
|
+
options[:rackup] = arg
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on "-m", "--mode MODE", "Either thread, process or no_ipc (default: thread)" do |arg|
|
43
|
+
ENV['SG_MODE'] = arg
|
44
|
+
end
|
35
45
|
end
|
36
46
|
|
37
47
|
parser.banner = "sg <options> <rackup file>"
|
@@ -50,15 +60,34 @@ unless File.exists?(options[:rackup])
|
|
50
60
|
abort "No rackup found at #{options[:rackup]}"
|
51
61
|
end
|
52
62
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
63
|
+
ENV['RACK_ENV'] = options[:environment].to_s
|
64
|
+
# Force process mode on Windows (pipes + sockets not working at the moment)
|
65
|
+
ENV['SG_MODE'] = 'no_ipc' if ::FFI::Platform.windows?
|
66
|
+
|
67
|
+
::Libuv::Loop.default.run do |logger|
|
68
|
+
logger.progress do |level, errorid, error|
|
69
|
+
begin
|
70
|
+
puts "Log called: #{level}: #{errorid}\n#{error.message}\n#{error.backtrace.join("\n")}\n"
|
71
|
+
rescue Exception
|
72
|
+
p 'error in gazelle logger'
|
73
|
+
end
|
74
|
+
end
|
59
75
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
76
|
+
puts "Look out! Here comes Spider-Gazelle #{::SpiderGazelle::VERSION}!"
|
77
|
+
puts "* Environment: #{ENV['RACK_ENV']} on #{RUBY_ENGINE || 'ruby'} #{RUBY_VERSION}"
|
78
|
+
server = ::SpiderGazelle::Spider.instance
|
79
|
+
server.loaded.then do
|
80
|
+
puts "* Loading: #{options[:rackup]}"
|
81
|
+
server.load(options[:rackup], options).catch(proc {|e|
|
82
|
+
puts "#{e.message}\n#{e.backtrace.join("\n") unless e.backtrace.nil?}\n"
|
83
|
+
}).finally do
|
84
|
+
# This will execute if the TCP binding is lost
|
85
|
+
p 'server failed to load - shutting down'
|
86
|
+
server.shutdown
|
87
|
+
end
|
88
|
+
|
89
|
+
puts "* Listening on tcp://#{options[:host]}:#{options[:port]}"
|
90
|
+
end
|
64
91
|
end
|
92
|
+
|
93
|
+
puts "\nSpider-Gazelle leaps through the veldt"
|
data/lib/spider-gazelle.rb
CHANGED
@@ -6,8 +6,13 @@ require "spider-gazelle/version"
|
|
6
6
|
require "spider-gazelle/request" # Holds request information and handles request processing
|
7
7
|
require "spider-gazelle/connection" # Holds connection information and handles request pipelining
|
8
8
|
require "spider-gazelle/gazelle" # Processes data received from connections
|
9
|
+
|
10
|
+
require "spider-gazelle/app_store" # Holds references to the loaded rack applications
|
11
|
+
require "spider-gazelle/binding" # Holds a reference to a bound port and associated rack application
|
9
12
|
require "spider-gazelle/spider" # Accepts connections and offloads them to gazelles
|
10
13
|
|
14
|
+
require "spider-gazelle/upgrades/websocket" # Websocket implementation
|
15
|
+
|
11
16
|
|
12
17
|
module SpiderGazelle
|
13
18
|
# Delegate pipe used for passing sockets to the gazelles
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'radix'
|
3
|
+
|
4
|
+
|
5
|
+
module SpiderGazelle
|
6
|
+
module AppStore
|
7
|
+
# Basic compression using UTF (more efficient for ID's stored as strings)
|
8
|
+
B65 = ::Radix::Base.new(::Radix::BASE::B62 + ['-', '_', '~'])
|
9
|
+
B10 = ::Radix::Base.new(10)
|
10
|
+
|
11
|
+
@mutex = Mutex.new
|
12
|
+
@count = 0
|
13
|
+
@apps = ThreadSafe::Cache.new
|
14
|
+
@loaded = ThreadSafe::Cache.new
|
15
|
+
|
16
|
+
# Load an app and assign it an ID
|
17
|
+
def self.load(rackup)
|
18
|
+
id = @loaded[rackup.to_sym]
|
19
|
+
|
20
|
+
if id.nil?
|
21
|
+
app, options = ::Rack::Builder.parse_file rackup
|
22
|
+
|
23
|
+
count = 0
|
24
|
+
@mutex.synchronize {
|
25
|
+
count = @count += 1
|
26
|
+
}
|
27
|
+
id = Radix.convert(count, B10, B65).to_sym
|
28
|
+
@apps[id] = app
|
29
|
+
@loaded[rackup.to_sym] = id
|
30
|
+
end
|
31
|
+
|
32
|
+
id
|
33
|
+
end
|
34
|
+
|
35
|
+
# Manually load an app
|
36
|
+
def self.add(app)
|
37
|
+
id = @loaded[app.__id__]
|
38
|
+
|
39
|
+
if id.nil?
|
40
|
+
count = 0
|
41
|
+
@mutex.synchronize {
|
42
|
+
count = @count += 1
|
43
|
+
}
|
44
|
+
id = Radix.convert(count, B10, B65).to_sym
|
45
|
+
@apps[id] = app
|
46
|
+
@loaded[app.__id__] = id
|
47
|
+
end
|
48
|
+
|
49
|
+
id
|
50
|
+
end
|
51
|
+
|
52
|
+
# Lookup an application
|
53
|
+
def self.lookup(app)
|
54
|
+
if app.is_a?(String) || app.is_a?(Symbol)
|
55
|
+
@apps[@loaded[app.to_sym]]
|
56
|
+
else
|
57
|
+
@apps[@loaded[app.__id__]]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get an app using the id directly
|
62
|
+
def self.get(id)
|
63
|
+
id = id.to_sym if id.is_a?(String)
|
64
|
+
@apps[id]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
|
5
|
+
module SpiderGazelle
|
6
|
+
class Binding
|
7
|
+
DEFAULT_OPTIONS = {
|
8
|
+
:host => '0.0.0.0',
|
9
|
+
:port => 3000,
|
10
|
+
:tls => false,
|
11
|
+
:optimize_for_latency => true,
|
12
|
+
:backlog => 1024
|
13
|
+
}
|
14
|
+
|
15
|
+
|
16
|
+
attr_reader :app_id
|
17
|
+
|
18
|
+
|
19
|
+
def initialize(loop, delegate, app_id, options = {})
|
20
|
+
@app_id = app_id
|
21
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
22
|
+
@loop = loop
|
23
|
+
@delegate = delegate
|
24
|
+
@port = @options[:port]
|
25
|
+
@tls = @options[:tls]
|
26
|
+
@optimize = @options[:optimize_for_latency]
|
27
|
+
|
28
|
+
# Connection management functions
|
29
|
+
@new_connection = method(:new_connection)
|
30
|
+
@accept_connection = method(:accept_connection)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Bind the application to the selected port
|
34
|
+
def bind
|
35
|
+
# Bind the socket
|
36
|
+
@tcp = @loop.tcp
|
37
|
+
@tcp.bind(@options[:host], @port, @new_connection)
|
38
|
+
@tcp.listen(@options[:backlog])
|
39
|
+
|
40
|
+
# Delegate errors
|
41
|
+
@tcp.catch do |e|
|
42
|
+
@loop.log :error, 'application bind failed', e
|
43
|
+
end
|
44
|
+
@tcp
|
45
|
+
end
|
46
|
+
|
47
|
+
# Close the bindings
|
48
|
+
def unbind
|
49
|
+
# close unless we've never been bound
|
50
|
+
@tcp.close unless @tcp.nil?
|
51
|
+
@tcp
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
|
58
|
+
# There is a new connection pending
|
59
|
+
# We accept it
|
60
|
+
def new_connection(server)
|
61
|
+
server.accept @accept_connection
|
62
|
+
end
|
63
|
+
|
64
|
+
# Once the connection is accepted we disable Nagles Algorithm
|
65
|
+
# This improves performance as we are using vectored or scatter/gather IO
|
66
|
+
# Then the spider delegates to the gazelle loops
|
67
|
+
def accept_connection(client)
|
68
|
+
client.enable_nodelay if @optimize == true
|
69
|
+
@delegate.call(client, @tls, @port, @app_id)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -3,18 +3,35 @@ require 'stringio'
|
|
3
3
|
|
4
4
|
module SpiderGazelle
|
5
5
|
class Connection
|
6
|
+
Hijack = Struct.new(:socket, :env)
|
6
7
|
|
7
8
|
|
8
|
-
|
9
|
+
RACK = 'rack'.freeze # used for filtering headers
|
10
|
+
CLOSE = 'close'.freeze
|
11
|
+
CONNECTION = 'Connection'.freeze
|
12
|
+
CONTENT_LENGTH = 'Content-Length'.freeze
|
13
|
+
TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
|
14
|
+
CHUNKED = 'chunked'.freeze
|
15
|
+
COLON_SPACE = ': '.freeze
|
16
|
+
EOF = "0\r\n\r\n".freeze
|
17
|
+
CRLF = "\r\n".freeze
|
9
18
|
|
19
|
+
HTTP_11_400 = "HTTP/1.1 400 Bad Request\r\n\r\n".freeze
|
10
20
|
|
21
|
+
|
22
|
+
def self.on_progress(data, socket); end
|
23
|
+
DUMMY_PROGRESS = self.method(:on_progress)
|
24
|
+
|
25
|
+
|
26
|
+
# For Gazelle
|
11
27
|
attr_reader :state, :parsing
|
12
|
-
|
28
|
+
# For Request
|
29
|
+
attr_reader :tls, :port, :loop, :socket, :async_callback
|
13
30
|
|
14
31
|
|
15
|
-
def initialize(loop, socket,
|
32
|
+
def initialize(gazelle, loop, socket, port, state, app, queue)
|
16
33
|
# A single parser instance per-connection (supports pipelining)
|
17
|
-
@state =
|
34
|
+
@state = state
|
18
35
|
@pending = []
|
19
36
|
|
20
37
|
# Work callback for thread pool processing
|
@@ -27,24 +44,38 @@ module SpiderGazelle
|
|
27
44
|
|
28
45
|
# Used to chain promises (ensures requests are processed in order)
|
29
46
|
@process_next = method(:process_next)
|
47
|
+
@write_chunk = method(:write_chunk)
|
30
48
|
@current_worker = queue # keep track of work queue head to prevent unintentional GC
|
31
49
|
@queue_worker = queue # start queue with an existing resolved promise (::Libuv::Q::ResolvedPromise.new(@loop, true))
|
32
50
|
|
33
51
|
# Socket for writing the response
|
34
52
|
@socket = socket
|
53
|
+
@app = app
|
54
|
+
@port = port
|
55
|
+
@tls = @socket.tls?
|
35
56
|
@loop = loop
|
57
|
+
@gazelle = gazelle
|
58
|
+
@async_callback = method(:deferred_callback)
|
59
|
+
|
60
|
+
# Remove connection if the socket closes
|
61
|
+
socket.finally &method(:unlink)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Lazy eval the IP
|
65
|
+
def remote_ip
|
66
|
+
@remote_ip ||= @socket.peername[0]
|
36
67
|
end
|
37
68
|
|
38
69
|
# Creates a new request state object
|
39
|
-
def start_parsing
|
40
|
-
@parsing =
|
70
|
+
def start_parsing
|
71
|
+
@parsing = Request.new(self, @app)
|
41
72
|
end
|
42
73
|
|
43
74
|
# Chains the work in a promise queue
|
44
75
|
def finished_parsing
|
45
76
|
if !@state.keep_alive?
|
46
77
|
@parsing.keep_alive = false
|
47
|
-
@socket.stop_read # we don't want to do any more work
|
78
|
+
@socket.stop_read # we don't want to do any more work than we need to
|
48
79
|
end
|
49
80
|
@parsing.upgrade = @state.upgrade?
|
50
81
|
@pending.push @parsing
|
@@ -53,52 +84,251 @@ module SpiderGazelle
|
|
53
84
|
|
54
85
|
# The parser encountered an error
|
55
86
|
def parsing_error
|
56
|
-
#
|
57
|
-
|
87
|
+
# Grab the error
|
88
|
+
send_error(@state.error)
|
58
89
|
|
59
90
|
# We no longer care for any further requests from this client
|
60
91
|
# however we will finish processing any valid pipelined requests before shutting down
|
61
92
|
@socket.stop_read
|
62
93
|
@queue_worker = @queue_worker.then do
|
63
94
|
# TODO:: send response (400 bad request)
|
95
|
+
@socket.write HTTP_11_400
|
64
96
|
@socket.shutdown
|
65
97
|
end
|
66
98
|
end
|
67
99
|
|
100
|
+
# Schedule send
|
101
|
+
def response(data)
|
102
|
+
@loop.schedule
|
103
|
+
end
|
104
|
+
|
68
105
|
|
69
106
|
protected
|
70
107
|
|
71
108
|
|
109
|
+
# --------------
|
110
|
+
# State handlers:
|
111
|
+
# --------------
|
112
|
+
|
113
|
+
|
114
|
+
# Called when an error occurs at any point while responding
|
115
|
+
def send_error(reason)
|
116
|
+
# Close the socket as this is fatal (file read error, gazelle error etc)
|
117
|
+
@socket.close
|
118
|
+
|
119
|
+
# Log the error in a worker thread
|
120
|
+
@loop.work do
|
121
|
+
msg = "connection error: #{reason.message}\n#{reason.backtrace.join("\n") if reason.backtrace}\n"
|
122
|
+
puts msg
|
123
|
+
@gazelle.logger.error msg
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# We use promise chaining to move the requests forward
|
128
|
+
# This provides an elegant way to handle persistent and pipelined connections
|
129
|
+
def process_next(result)
|
130
|
+
@request = @pending.shift
|
131
|
+
@current_worker = @loop.work @work
|
132
|
+
@current_worker.then @send_response, @send_error # resolves the promise with a promise
|
133
|
+
end
|
134
|
+
|
135
|
+
# returns the response as the result of the work
|
136
|
+
# We support the unofficial rack async api (multi-call version for chunked responses)
|
137
|
+
def work
|
138
|
+
@request.execute!
|
139
|
+
end
|
140
|
+
|
141
|
+
# Unlinks the connection from the rack app
|
142
|
+
# This occurs when requested and when the socket closes
|
143
|
+
def unlink
|
144
|
+
if not @gazelle.nil?
|
145
|
+
@socket.progress &DUMMY_PROGRESS # unlink the progress callback (prevent funny business)
|
146
|
+
@gazelle.discard(self)
|
147
|
+
@gazelle = nil
|
148
|
+
@state = nil
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
# ----------------------
|
154
|
+
# Core response handlers:
|
155
|
+
# ----------------------
|
156
|
+
|
157
|
+
|
72
158
|
def send_response(result)
|
73
159
|
# As we have come back from another thread the socket may have closed
|
74
160
|
# This check is an optimisation, the call to write and shutdown would fail safely
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
161
|
+
|
162
|
+
if @request.hijacked
|
163
|
+
unlink # unlink the management of the socket
|
164
|
+
|
165
|
+
# Pass the hijack response to the captor using the promise
|
166
|
+
# This forwards the socket and environment as well as moving
|
167
|
+
# continued execution onto the event loop.
|
168
|
+
@request.hijacked.resolve(Hijack.new(@socket, @request.env))
|
169
|
+
|
170
|
+
elsif !@socket.closed
|
171
|
+
if @request.deferred
|
172
|
+
# Wait for the response using this promise
|
173
|
+
promise = @request.deferred.promise
|
174
|
+
|
175
|
+
# Process any responses that might have made it here first
|
176
|
+
if @deferred_responses
|
177
|
+
@deferred_responses.each &method(:respond_with)
|
178
|
+
@deferred_responses = nil
|
179
|
+
end
|
180
|
+
|
181
|
+
return promise
|
182
|
+
|
183
|
+
# NOTE:: Somehow getting to here with a nil request... needs investigation
|
184
|
+
elsif not result.nil?
|
185
|
+
# clear any cached responses just in case
|
186
|
+
# could be set by error in the rack application
|
187
|
+
@deferred_responses = nil if @deferred_responses
|
188
|
+
|
189
|
+
status, headers, body = result
|
190
|
+
|
191
|
+
# If a file, stream the body in a non-blocking fashion
|
192
|
+
if body.respond_to? :to_path
|
193
|
+
headers[CONNECTION] = CLOSE if @request.keep_alive == false
|
194
|
+
|
195
|
+
if headers[CONTENT_LENGTH]
|
196
|
+
type = :raw
|
197
|
+
else
|
198
|
+
type = :http
|
199
|
+
headers[TRANSFER_ENCODING] = CHUNKED
|
200
|
+
end
|
201
|
+
|
202
|
+
write_headers(status, headers)
|
203
|
+
|
204
|
+
file = @loop.file(body.to_path, File::RDONLY)
|
205
|
+
file.progress do # File is open and available for reading
|
206
|
+
file.send_file(@socket, type).finally do
|
207
|
+
file.close
|
208
|
+
if @request.keep_alive == false
|
209
|
+
@socket.shutdown
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
return file
|
215
|
+
else
|
216
|
+
write_response(status, headers, body)
|
217
|
+
end
|
79
218
|
end
|
80
219
|
end
|
220
|
+
|
81
221
|
# continue processing (don't wait for write to complete)
|
82
222
|
# if the write fails it will close the socket
|
83
223
|
nil
|
84
224
|
end
|
85
225
|
|
86
|
-
def
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
226
|
+
def write_response(status, headers, body)
|
227
|
+
headers[CONNECTION] = CLOSE if @request.keep_alive == false
|
228
|
+
|
229
|
+
if headers[CONTENT_LENGTH]
|
230
|
+
headers[CONTENT_LENGTH] = headers[CONTENT_LENGTH].to_s
|
231
|
+
write_headers(status, headers)
|
232
|
+
|
233
|
+
# Stream the response (pass directly into @socket.write)
|
234
|
+
body.each &@socket.method(:write)
|
235
|
+
|
236
|
+
if @request.deferred
|
237
|
+
@request.deferred.resolve(true)
|
238
|
+
@request.deferred = nil # prevent data being sent after completed
|
239
|
+
end
|
240
|
+
|
241
|
+
@socket.shutdown if @request.keep_alive == false
|
242
|
+
else
|
243
|
+
headers[TRANSFER_ENCODING] = CHUNKED
|
244
|
+
write_headers(status, headers)
|
245
|
+
|
246
|
+
# Stream the response
|
247
|
+
body.each &@write_chunk
|
248
|
+
|
249
|
+
if @request.deferred.nil?
|
250
|
+
@socket.write EOF
|
251
|
+
@socket.shutdown if @request.keep_alive == false
|
252
|
+
else
|
253
|
+
@async_state = :chunked
|
254
|
+
end
|
255
|
+
end
|
92
256
|
end
|
93
257
|
|
94
|
-
def
|
95
|
-
|
96
|
-
|
97
|
-
|
258
|
+
def write_headers(status, headers)
|
259
|
+
header = "HTTP/1.1 #{status}\r\n"
|
260
|
+
headers.each do |key, value|
|
261
|
+
next if key.start_with? RACK
|
262
|
+
|
263
|
+
header << key
|
264
|
+
header << COLON_SPACE
|
265
|
+
header << value
|
266
|
+
header << CRLF
|
267
|
+
end
|
268
|
+
header << CRLF
|
269
|
+
@socket.write header
|
98
270
|
end
|
99
271
|
|
100
|
-
def
|
101
|
-
|
272
|
+
def write_chunk(part)
|
273
|
+
chunk = part.bytesize.to_s(16) << CRLF << part << CRLF
|
274
|
+
@socket.write chunk
|
275
|
+
end
|
276
|
+
|
277
|
+
|
278
|
+
# ------------------------
|
279
|
+
# Async response functions:
|
280
|
+
# ------------------------
|
281
|
+
|
282
|
+
|
283
|
+
# Callback from a response that was marked async
|
284
|
+
def deferred_callback(data)
|
285
|
+
# We call close here, like on a regular response
|
286
|
+
body = data[2]
|
287
|
+
body.close if body.respond_to?(:close)
|
288
|
+
@loop.next_tick do
|
289
|
+
callback(data)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Process a response that was marked as async
|
294
|
+
# Save the data if the request hasn't responded yet
|
295
|
+
def callback(data)
|
296
|
+
begin
|
297
|
+
if @request.deferred && @deferred_responses.nil?
|
298
|
+
respond_with(data)
|
299
|
+
else
|
300
|
+
@deferred_responses ||= []
|
301
|
+
@deferred_responses << data
|
302
|
+
end
|
303
|
+
rescue Exception => e
|
304
|
+
# This provides the same level of protection that
|
305
|
+
# the regular responses provide
|
306
|
+
send_error(e)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# Process the async request in the same way as Mizuno
|
311
|
+
# See: http://polycrystal.org/2012/04/15/asynchronous_responses_in_rack.html
|
312
|
+
def respond_with(data)
|
313
|
+
status, headers, body = data
|
314
|
+
|
315
|
+
if @async_state.nil?
|
316
|
+
# Respond with the headers here
|
317
|
+
write_response(status, headers, body)
|
318
|
+
elsif body.empty?
|
319
|
+
@socket.write EOF
|
320
|
+
@socket.shutdown if @request.keep_alive == false
|
321
|
+
|
322
|
+
# Complete the request here
|
323
|
+
deferred = @request.deferred
|
324
|
+
@request.deferred = nil # prevent data being sent after completed
|
325
|
+
@async_state = nil
|
326
|
+
deferred.resolve(true)
|
327
|
+
else
|
328
|
+
# Send the chunks provided
|
329
|
+
body.each &@write_chunk
|
330
|
+
end
|
331
|
+
nil
|
102
332
|
end
|
103
333
|
end
|
104
334
|
end
|