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
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module SpiderGazelle
|
4
|
+
class Spider
|
5
|
+
class Binding
|
6
|
+
attr_reader :app_id
|
7
|
+
attr_accessor :tcp
|
8
|
+
|
9
|
+
|
10
|
+
def initialize(iterator, app_id, options)
|
11
|
+
if options[:mode] == :no_ipc
|
12
|
+
@delegate = method(:direct_delegate)
|
13
|
+
@gazelle = iterator
|
14
|
+
else
|
15
|
+
@delegate = method(:delegate)
|
16
|
+
@select_gazelle = iterator
|
17
|
+
end
|
18
|
+
|
19
|
+
@options = options
|
20
|
+
|
21
|
+
@logger = Logger.instance
|
22
|
+
@signaller = Signaller.instance
|
23
|
+
@thread = @signaller.thread
|
24
|
+
|
25
|
+
@port = @options[:port]
|
26
|
+
@indicator = app_id.to_s.freeze
|
27
|
+
end
|
28
|
+
|
29
|
+
# Bind the application to the selected port
|
30
|
+
def bind
|
31
|
+
# Bind the socket
|
32
|
+
@tcp = @thread.tcp
|
33
|
+
@tcp.bind @options[:host], @port, @delegate
|
34
|
+
@tcp.listen @options[:backlog]
|
35
|
+
@tcp.enable_simultaneous_accepts
|
36
|
+
|
37
|
+
@logger.info "Listening on tcp://#{@options[:host]}:#{@port}"
|
38
|
+
|
39
|
+
@tcp.catch do |error|
|
40
|
+
begin
|
41
|
+
@logger.print_error(error)
|
42
|
+
rescue
|
43
|
+
ensure
|
44
|
+
@signaller.general_failure
|
45
|
+
end
|
46
|
+
end
|
47
|
+
@tcp
|
48
|
+
end
|
49
|
+
|
50
|
+
# Close the bindings
|
51
|
+
def unbind
|
52
|
+
@tcp.close unless @tcp.nil?
|
53
|
+
@tcp
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
|
60
|
+
DELEGATE_ERR = proc { |error|
|
61
|
+
client.close
|
62
|
+
begin
|
63
|
+
Logger.instance.print_error(error, "delegating socket to gazelle")
|
64
|
+
rescue
|
65
|
+
end
|
66
|
+
}
|
67
|
+
def delegate(client, retries = 0)
|
68
|
+
promise = @select_gazelle.next.write2(client, @indicator)
|
69
|
+
promise.then do
|
70
|
+
client.close
|
71
|
+
end
|
72
|
+
promise.catch DELEGATE_ERR
|
73
|
+
end
|
74
|
+
|
75
|
+
def direct_delegate(client)
|
76
|
+
@gazelle.__send__(:process_connection, client, @indicator)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -1,94 +1,98 @@
|
|
1
|
-
require 'spider-gazelle/const'
|
2
1
|
require 'websocket/driver'
|
3
2
|
require 'forwardable'
|
4
3
|
|
5
4
|
module SpiderGazelle
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
5
|
+
class Websocket < ::Libuv::Q::DeferredPromise
|
6
|
+
attr_reader :env, :url, :loop
|
7
|
+
|
8
|
+
|
9
|
+
RACK_URL_SCHEME = "rack.url_scheme".freeze
|
10
|
+
HTTP_HOST = "HTTP_HOST".freeze
|
11
|
+
REQUEST_URI= "REQUEST_URI".freeze
|
12
|
+
HTTPS = 'https'.freeze
|
13
|
+
|
14
|
+
|
15
|
+
extend Forwardable
|
16
|
+
def_delegators :@driver, :start, :ping, :protocol, :ready_state, :set_header, :state, :close
|
17
|
+
def_delegators :@socket, :write
|
18
|
+
|
19
|
+
def initialize(tcp, env)
|
20
|
+
@socket, @env = tcp, env
|
21
|
+
|
22
|
+
# Initialise the promise
|
23
|
+
super tcp.loop, tcp.loop.defer
|
24
|
+
|
25
|
+
scheme = env[RACK_URL_SCHEME] == HTTPS ? 'wss://' : 'ws://'
|
26
|
+
@url = scheme + env[HTTP_HOST] + env[REQUEST_URI]
|
27
|
+
@driver = ::WebSocket::Driver.rack self
|
28
|
+
|
29
|
+
# Pass data from the socket to the driver
|
30
|
+
@socket.progress &method(:socket_read)
|
31
|
+
@socket.finally &method(:socket_close)
|
32
|
+
|
33
|
+
|
34
|
+
# Driver has indicated that it is closing
|
35
|
+
# We'll close the socket after writing any remaining data
|
36
|
+
@driver.on :close, &method(:on_close)
|
37
|
+
@driver.on :message, &method(:on_message)
|
38
|
+
@driver.on :error, &method(:on_error)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Write some text to the websocket connection
|
42
|
+
#
|
43
|
+
# @param string [String] a string of data to be sent to the far end
|
44
|
+
def text(string)
|
45
|
+
@loop.schedule { @driver.text(string.to_s) }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Write some binary data to the websocket connection
|
49
|
+
#
|
50
|
+
# @param array [Array] an array of bytes to be sent to the far end
|
51
|
+
def binary(array)
|
52
|
+
@loop.schedule { @driver.binary(array.to_a) }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Used to define a callback when data is received from the client
|
56
|
+
#
|
57
|
+
# @param callback [Proc] the callback to be called when data is received
|
58
|
+
def progress(callback = nil, &blk)
|
59
|
+
@progress = callback || blk
|
60
|
+
end
|
61
|
+
|
62
|
+
# Used to define a callback when the websocket connection is established
|
63
|
+
# Data sent before this callback is buffered.
|
64
|
+
#
|
65
|
+
# @param callback [Proc] the callback to be triggered on establishment
|
66
|
+
def on_open(callback = nil, &blk)
|
67
|
+
callback ||= blk
|
68
|
+
@driver.on :open, &callback
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
def socket_read(data, tcp)
|
74
|
+
@driver.parse data
|
75
|
+
end
|
76
|
+
|
77
|
+
def socket_close
|
78
|
+
if @shutdown_called.nil?
|
79
|
+
@defer.reject WebSocket::Driver::CloseEvent.new(1006, 'connection was closed unexpectedly')
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def on_message(event)
|
85
|
+
@progress.call(event.data, self) unless @progress.nil?
|
86
|
+
end
|
87
|
+
|
88
|
+
def on_error(event)
|
89
|
+
@defer.reject event
|
90
|
+
end
|
91
|
+
|
92
|
+
def on_close(event)
|
93
|
+
@shutdown_called = true
|
94
|
+
@socket.shutdown
|
95
|
+
@defer.resolve event
|
96
|
+
end
|
92
97
|
end
|
93
|
-
end
|
94
98
|
end
|
data/spec/http1_spec.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'spider-gazelle/gazelle/http1'
|
2
|
+
|
3
|
+
|
4
|
+
# TODO:: Mock logger
|
5
|
+
|
6
|
+
class MockSocket
|
7
|
+
def initialize
|
8
|
+
@stopped = false
|
9
|
+
@write_cb = proc {}
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :closed, :stopped
|
13
|
+
attr_accessor :storage, :write_cb, :shutdown_cb, :close_cb
|
14
|
+
|
15
|
+
def peername; ['127.0.0.1']; end
|
16
|
+
def finally(method = nil, &block); @on_close = method || block; end
|
17
|
+
def close; @close_cb.call; end
|
18
|
+
def shutdown; @shutdown_cb.call; end
|
19
|
+
def progress(_); end
|
20
|
+
def stop_read; @stopped = true; end
|
21
|
+
|
22
|
+
def write(data); @write_cb.call(data); end
|
23
|
+
end
|
24
|
+
|
25
|
+
class MockLogger
|
26
|
+
def initialize
|
27
|
+
@logged = []
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :logged
|
31
|
+
|
32
|
+
def method_missing(methId, *args)
|
33
|
+
@logged << methId
|
34
|
+
p "methId: #{args}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe ::SpiderGazelle::Gazelle::Http1 do
|
39
|
+
before :each do
|
40
|
+
@shutdown_called = 0
|
41
|
+
@close_called = 0
|
42
|
+
|
43
|
+
@loop = ::Libuv::Loop.default
|
44
|
+
@timeout = @loop.timer do
|
45
|
+
@loop.stop
|
46
|
+
@general_failure << "test timed out"
|
47
|
+
end
|
48
|
+
@timeout.start(1000)
|
49
|
+
|
50
|
+
|
51
|
+
@return ||= proc {|http1|
|
52
|
+
@returned = http1
|
53
|
+
}
|
54
|
+
@logger = MockLogger.new
|
55
|
+
@http1_callbacks ||= ::SpiderGazelle::Gazelle::Http1::Callbacks.new
|
56
|
+
@http1 = ::SpiderGazelle::Gazelle::Http1.new(@return, @http1_callbacks, @loop, @logger)
|
57
|
+
|
58
|
+
@socket = MockSocket.new
|
59
|
+
@socket.shutdown_cb = proc {
|
60
|
+
@shutdown_called += 1
|
61
|
+
@loop.stop
|
62
|
+
}
|
63
|
+
@socket.close_cb = proc {
|
64
|
+
@close_called += 1
|
65
|
+
@loop.stop
|
66
|
+
}
|
67
|
+
|
68
|
+
@app_mode = :thread_pool
|
69
|
+
@port = 80
|
70
|
+
@tls = false
|
71
|
+
end
|
72
|
+
|
73
|
+
after :each do
|
74
|
+
@timeout.close
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should process a single request and close the connection", http1: true do
|
78
|
+
app = lambda do |env|
|
79
|
+
body = 'Hello, World!'
|
80
|
+
[200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
|
81
|
+
end
|
82
|
+
writes = []
|
83
|
+
|
84
|
+
@loop.run {
|
85
|
+
@http1.load(@socket, @port, app, @app_mode, @tls)
|
86
|
+
@http1.parse("GET / HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
87
|
+
|
88
|
+
@socket.write_cb = proc { |data|
|
89
|
+
writes << data
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
expect(@shutdown_called).to be == 1
|
94
|
+
expect(@close_called).to be == 0
|
95
|
+
expect(writes).to eq([
|
96
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\nConnection: close\r\n\r\n",
|
97
|
+
"Hello, World!"
|
98
|
+
])
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should process a single request and keep the connection open", http1: true do
|
102
|
+
app = lambda do |env|
|
103
|
+
body = 'Hello, World!'
|
104
|
+
[200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
|
105
|
+
end
|
106
|
+
writes = []
|
107
|
+
|
108
|
+
@loop.run {
|
109
|
+
@http1.load(@socket, @port, app, @app_mode, @tls)
|
110
|
+
@http1.parse("GET / HTTP/1.1\r\n\r\n")
|
111
|
+
|
112
|
+
@socket.write_cb = proc { |data|
|
113
|
+
writes << data
|
114
|
+
if writes.length == 2
|
115
|
+
@loop.next_tick do
|
116
|
+
@loop.stop
|
117
|
+
end
|
118
|
+
end
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
expect(@shutdown_called).to be == 0
|
123
|
+
expect(@close_called).to be == 0
|
124
|
+
expect(writes).to eq([
|
125
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\n",
|
126
|
+
"Hello, World!"
|
127
|
+
])
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should fill out the environment properly", http1: true do
|
131
|
+
app = lambda do |env|
|
132
|
+
expect(env['REQUEST_URI']).to eq('/?test=ing')
|
133
|
+
expect(env['REQUEST_PATH']).to eq('/')
|
134
|
+
expect(env['QUERY_STRING']).to eq('test=ing')
|
135
|
+
expect(env['SERVER_NAME']).to eq('spider.gazelle.net')
|
136
|
+
expect(env['SERVER_PORT']).to eq(80)
|
137
|
+
expect(env['REMOTE_ADDR']).to eq('127.0.0.1')
|
138
|
+
expect(env['rack.url_scheme']).to eq('http')
|
139
|
+
|
140
|
+
body = 'Hello, World!'
|
141
|
+
[200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
|
142
|
+
end
|
143
|
+
|
144
|
+
@loop.run {
|
145
|
+
@http1.load(@socket, @port, app, @app_mode, @tls)
|
146
|
+
@http1.parse("GET /?test=ing HTTP/1.1\r\nHost: spider.gazelle.net\r\nConnection: Close\r\n\r\n")
|
147
|
+
}
|
148
|
+
|
149
|
+
expect(@shutdown_called).to be == 1
|
150
|
+
expect(@close_called).to be == 0
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should respond with a chunked response", http1: true do
|
154
|
+
app = lambda do |env|
|
155
|
+
body = ['Hello', ',', ' World!']
|
156
|
+
[200, {'Content-Type' => 'text/plain'}, body]
|
157
|
+
end
|
158
|
+
writes = []
|
159
|
+
|
160
|
+
@loop.run {
|
161
|
+
@http1.load(@socket, @port, app, @app_mode, @tls)
|
162
|
+
@http1.parse("GET / HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
163
|
+
|
164
|
+
@socket.write_cb = proc { |data|
|
165
|
+
writes << data
|
166
|
+
}
|
167
|
+
}
|
168
|
+
|
169
|
+
expect(@shutdown_called).to be == 1
|
170
|
+
expect(@close_called).to be == 0
|
171
|
+
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"])
|
172
|
+
end
|
173
|
+
end
|
data/spec/rack_lock_spec.rb
CHANGED
@@ -6,121 +6,121 @@ require 'rack/lock_patch'
|
|
6
6
|
|
7
7
|
|
8
8
|
module Rack
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
class Lock
|
10
|
+
def would_block
|
11
|
+
@count > 0
|
12
|
+
end
|
12
13
|
end
|
13
|
-
end
|
14
14
|
end
|
15
15
|
|
16
16
|
|
17
|
-
|
17
|
+
def lock_app(app)
|
18
18
|
app = Rack::Lock.new(app)
|
19
19
|
return app, Rack::Lint.new(app)
|
20
|
-
|
20
|
+
end
|
21
21
|
|
22
22
|
|
23
23
|
describe Rack::Lock do
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
25
|
+
describe 'Proxy' do
|
26
|
+
|
27
|
+
it 'delegate each' do
|
28
|
+
env = Rack::MockRequest.env_for("/")
|
29
|
+
response = Class.new {
|
30
|
+
attr_accessor :close_called
|
31
|
+
def initialize; @close_called = false; end
|
32
|
+
def each; %w{ hi mom }.each { |x| yield x }; end
|
33
|
+
}.new
|
34
|
+
|
35
|
+
app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] })[1]
|
36
|
+
response = app.call(env)[2]
|
37
|
+
list = []
|
38
|
+
response.each { |x| list << x }
|
39
|
+
expect(list).to eq(%w{ hi mom })
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'delegate to_path' do
|
43
|
+
env = Rack::MockRequest.env_for("/")
|
41
44
|
|
42
|
-
|
43
|
-
|
45
|
+
res = ['Hello World']
|
46
|
+
def res.to_path ; "/tmp/hello.txt" ; end
|
44
47
|
|
45
|
-
|
46
|
-
|
48
|
+
app = Rack::Lock.new(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, res] })
|
49
|
+
body = app.call(env)[2]
|
47
50
|
|
48
|
-
|
49
|
-
|
51
|
+
expect(body.respond_to? :to_path).to eq(true)
|
52
|
+
expect(body.to_path).to eq("/tmp/hello.txt")
|
53
|
+
end
|
50
54
|
|
51
|
-
|
52
|
-
|
55
|
+
it 'not delegate to_path if body does not implement it' do
|
56
|
+
env = Rack::MockRequest.env_for("/")
|
57
|
+
|
58
|
+
res = ['Hello World']
|
59
|
+
|
60
|
+
app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, res] })[1]
|
61
|
+
body = app.call(env)[2]
|
62
|
+
|
63
|
+
expect(body.respond_to? :to_path).to eq(false)
|
64
|
+
end
|
53
65
|
end
|
54
66
|
|
55
|
-
it '
|
56
|
-
|
67
|
+
it 'call super on close' do
|
68
|
+
env = Rack::MockRequest.env_for("/")
|
69
|
+
response = Class.new {
|
70
|
+
attr_accessor :close_called
|
71
|
+
def initialize; @close_called = false; end
|
72
|
+
def close; @close_called = true; end
|
73
|
+
}.new
|
74
|
+
|
75
|
+
app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] })[1]
|
76
|
+
app.call(env)
|
77
|
+
expect(response.close_called).to eq(false)
|
78
|
+
response.close
|
79
|
+
expect(response.close_called).to eq(true)
|
80
|
+
end
|
57
81
|
|
58
|
-
|
82
|
+
it "not unlock until body is closed" do
|
83
|
+
env = Rack::MockRequest.env_for("/")
|
84
|
+
response = Object.new
|
85
|
+
lock, app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] })
|
59
86
|
|
60
|
-
|
61
|
-
|
87
|
+
expect(lock.would_block).to eq(false)
|
88
|
+
response = app.call(env)[2]
|
89
|
+
expect(lock.would_block).to eq(true)
|
90
|
+
response.close
|
91
|
+
expect(lock.would_block).to eq(false)
|
92
|
+
end
|
93
|
+
|
94
|
+
it "return value from app" do
|
95
|
+
env = Rack::MockRequest.env_for("/")
|
96
|
+
body = [200, {"Content-Type" => "text/plain"}, %w{ hi mom }]
|
97
|
+
app = lock_app(lambda { |inner_env| body })[1]
|
98
|
+
|
99
|
+
res = app.call(env)
|
100
|
+
expect(res[0]).to eq(body[0])
|
101
|
+
expect(res[1]).to eq(body[1])
|
102
|
+
expect(res[2].to_enum.to_a).to eq(["hi", "mom"])
|
103
|
+
end
|
104
|
+
|
105
|
+
it "call synchronize on lock" do
|
106
|
+
env = Rack::MockRequest.env_for("/")
|
107
|
+
lock, app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, %w{ a b c }] })
|
108
|
+
expect(lock.would_block).to eq(false)
|
109
|
+
app.call(env)
|
110
|
+
expect(lock.would_block).to eq(true)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "unlock if the app raises" do
|
114
|
+
env = Rack::MockRequest.env_for("/")
|
115
|
+
lock, app = lock_app(lambda { raise Exception })
|
116
|
+
expect { app.call(env) }.to raise_error(Exception)
|
117
|
+
expect(lock.would_block).to eq(false)
|
118
|
+
end
|
62
119
|
|
63
|
-
|
120
|
+
it "unlock if the app throws" do
|
121
|
+
env = Rack::MockRequest.env_for("/")
|
122
|
+
lock, app = lock_app(lambda {|_| throw :bacon })
|
123
|
+
expect { app.call(env) }.to raise_error(ArgumentError)
|
124
|
+
expect(lock.would_block).to eq(false)
|
64
125
|
end
|
65
|
-
end
|
66
|
-
|
67
|
-
it 'call super on close' do
|
68
|
-
env = Rack::MockRequest.env_for("/")
|
69
|
-
response = Class.new {
|
70
|
-
attr_accessor :close_called
|
71
|
-
def initialize; @close_called = false; end
|
72
|
-
def close; @close_called = true; end
|
73
|
-
}.new
|
74
|
-
|
75
|
-
app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] })[1]
|
76
|
-
app.call(env)
|
77
|
-
expect(response.close_called).to eq(false)
|
78
|
-
response.close
|
79
|
-
expect(response.close_called).to eq(true)
|
80
|
-
end
|
81
|
-
|
82
|
-
it "not unlock until body is closed" do
|
83
|
-
env = Rack::MockRequest.env_for("/")
|
84
|
-
response = Object.new
|
85
|
-
lock, app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] })
|
86
|
-
|
87
|
-
expect(lock.would_block).to eq(false)
|
88
|
-
response = app.call(env)[2]
|
89
|
-
expect(lock.would_block).to eq(true)
|
90
|
-
response.close
|
91
|
-
expect(lock.would_block).to eq(false)
|
92
|
-
end
|
93
|
-
|
94
|
-
it "return value from app" do
|
95
|
-
env = Rack::MockRequest.env_for("/")
|
96
|
-
body = [200, {"Content-Type" => "text/plain"}, %w{ hi mom }]
|
97
|
-
app = lock_app(lambda { |inner_env| body })[1]
|
98
|
-
|
99
|
-
res = app.call(env)
|
100
|
-
expect(res[0]).to eq(body[0])
|
101
|
-
expect(res[1]).to eq(body[1])
|
102
|
-
expect(res[2].to_enum.to_a).to eq(["hi", "mom"])
|
103
|
-
end
|
104
|
-
|
105
|
-
it "call synchronize on lock" do
|
106
|
-
env = Rack::MockRequest.env_for("/")
|
107
|
-
lock, app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, %w{ a b c }] })
|
108
|
-
expect(lock.would_block).to eq(false)
|
109
|
-
app.call(env)
|
110
|
-
expect(lock.would_block).to eq(true)
|
111
|
-
end
|
112
|
-
|
113
|
-
it "unlock if the app raises" do
|
114
|
-
env = Rack::MockRequest.env_for("/")
|
115
|
-
lock, app = lock_app(lambda { raise Exception })
|
116
|
-
expect { app.call(env) }.to raise_error(Exception)
|
117
|
-
expect(lock.would_block).to eq(false)
|
118
|
-
end
|
119
|
-
|
120
|
-
it "unlock if the app throws" do
|
121
|
-
env = Rack::MockRequest.env_for("/")
|
122
|
-
lock, app = lock_app(lambda {|_| throw :bacon })
|
123
|
-
expect { app.call(env) }.to raise_error(ArgumentError)
|
124
|
-
expect(lock.would_block).to eq(false)
|
125
|
-
end
|
126
126
|
end
|