spider-gazelle 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|