hatetepe 0.4.1 → 0.5.0.pre
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.
- data/examples/parallel_requests.rb +32 -0
- data/hatetepe.gemspec +3 -4
- data/lib/hatetepe/builder.rb +1 -0
- data/lib/hatetepe/cli.rb +11 -20
- data/lib/hatetepe/client.rb +210 -181
- data/lib/hatetepe/connection.rb +31 -12
- data/lib/hatetepe/server/keep_alive.rb +15 -53
- data/lib/hatetepe/server/pipeline.rb +14 -18
- data/lib/hatetepe/server/rack_app.rb +39 -0
- data/lib/hatetepe/server.rb +60 -108
- data/lib/hatetepe/version.rb +1 -1
- data/lib/rack/handler/hatetepe.rb +2 -0
- data/spec/integration/cli/start_spec.rb +84 -92
- data/spec/integration/client/keep_alive_spec.rb +5 -56
- data/spec/integration/client/timeout_spec.rb +93 -0
- data/spec/integration/server/keep_alive_spec.rb +8 -80
- data/spec/integration/server/timeout_spec.rb +45 -0
- data/spec/spec_helper.rb +7 -59
- data/spec/unit/client_spec.rb +68 -363
- data/spec/unit/connection_spec.rb +5 -7
- data/spec/unit/server_spec.rb +108 -338
- metadata +58 -43
- data/lib/hatetepe/client/keep_alive.rb +0 -32
- data/lib/hatetepe/client/pipeline.rb +0 -19
- data/lib/hatetepe/server/app.rb +0 -85
- data/lib/hatetepe/server/proxy.rb +0 -48
- data/spec/unit/app_spec.rb +0 -125
- data/spec/unit/client/pipeline_spec.rb +0 -40
- data/spec/unit/proxy_spec.rb +0 -145
@@ -0,0 +1,32 @@
|
|
1
|
+
require "hatetepe"
|
2
|
+
require "awesome_print"
|
3
|
+
|
4
|
+
EM.synchrony do
|
5
|
+
# one server
|
6
|
+
app = proc do |env|
|
7
|
+
[200, {"Content-Type" => "text/plain"}, ["Hello, world!"]]
|
8
|
+
end
|
9
|
+
Hatetepe::Server.start :host => "127.0.0.1", :port => 3123, :app => app
|
10
|
+
|
11
|
+
# two clients
|
12
|
+
clients = 2.times.map { Hatetepe::Client.start :host => "127.0.0.1", :port => 3123 }
|
13
|
+
|
14
|
+
# six requests
|
15
|
+
requests = 6.times.map do |i|
|
16
|
+
# no extra headers, empty body
|
17
|
+
req = Hatetepe::Request.new(:get, "/", {}, [])
|
18
|
+
|
19
|
+
# response status between 100 and 399
|
20
|
+
req.callback {|res| puts "request ##{i} finished with #{res.status} response" }
|
21
|
+
|
22
|
+
# response status between 400 and 599 or connection failure
|
23
|
+
req.errback {|res| puts "request ##{i} finished with #{res ? res.status : 'no'} response" }
|
24
|
+
|
25
|
+
clients[i % clients.length] << req
|
26
|
+
req
|
27
|
+
end
|
28
|
+
|
29
|
+
# Client#stop waits until all of its requests have finished
|
30
|
+
clients.map &:stop
|
31
|
+
EM.stop
|
32
|
+
end
|
data/hatetepe.gemspec
CHANGED
@@ -5,6 +5,7 @@ require "hatetepe/version"
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = "hatetepe"
|
7
7
|
s.version = Hatetepe::VERSION
|
8
|
+
s.date = Date.today.to_s
|
8
9
|
s.platform = Gem::Platform::RUBY
|
9
10
|
s.authors = ["Lars Gierth"]
|
10
11
|
s.email = ["lars.gierth@gmail.com"]
|
@@ -13,14 +14,12 @@ Gem::Specification.new do |s|
|
|
13
14
|
#s.description = %q{TODO: write description}
|
14
15
|
|
15
16
|
s.add_dependency "http_parser.rb", "~> 0.5.3"
|
16
|
-
s.add_dependency "eventmachine"
|
17
|
-
s.add_dependency "em-synchrony",
|
17
|
+
s.add_dependency "eventmachine", "~> 1.0.0.beta.4"
|
18
|
+
s.add_dependency "em-synchrony", "~> 1.0"
|
18
19
|
s.add_dependency "rack"
|
19
|
-
s.add_dependency "async-rack"
|
20
20
|
s.add_dependency "thor"
|
21
21
|
|
22
22
|
s.add_development_dependency "rspec"
|
23
|
-
s.add_development_dependency "fakefs"
|
24
23
|
s.add_development_dependency "yard"
|
25
24
|
s.add_development_dependency "rdiscount"
|
26
25
|
|
data/lib/hatetepe/builder.rb
CHANGED
data/lib/hatetepe/cli.rb
CHANGED
@@ -23,11 +23,12 @@ module Hatetepe
|
|
23
23
|
method_option :env, :aliases => "-e", :type => :string,
|
24
24
|
:banner => "Boot the app in the specified environment (default: development)"
|
25
25
|
method_option :timeout, :aliases => "-t", :type => :numeric,
|
26
|
-
:banner => "Time out connections after the specified admount of seconds (default:
|
26
|
+
:banner => "Time out connections after the specified admount of seconds (default: see Hatetepe::Server::CONFIG_DEFAULTS)"
|
27
27
|
def start
|
28
28
|
require "hatetepe/server"
|
29
|
+
require "rack"
|
29
30
|
|
30
|
-
ENV["RACK_ENV"] =
|
31
|
+
ENV["RACK_ENV"] = options[:env] || ENV["RACK_ENV"] || "development"
|
31
32
|
$stderr << "We're in #{ENV["RACK_ENV"]}\n"
|
32
33
|
$stderr.flush
|
33
34
|
|
@@ -42,30 +43,20 @@ module Hatetepe
|
|
42
43
|
trap("INT") { EM.stop }
|
43
44
|
trap("TERM") { EM.stop }
|
44
45
|
|
45
|
-
host
|
46
|
-
port
|
46
|
+
host = options[:bind] || "127.0.0.1"
|
47
|
+
port = options[:port] || 3000
|
48
|
+
timeout = options[:timeout] || Hatetepe::Server::CONFIG_DEFAULTS[:timeout]
|
47
49
|
|
48
50
|
$stderr << "Binding to #{host}:#{port}\n"
|
49
51
|
$stderr.flush
|
52
|
+
|
50
53
|
Server.start({
|
51
|
-
:app
|
52
|
-
:
|
53
|
-
:
|
54
|
-
:
|
55
|
-
:timeout => (options[:timeout] || 1)
|
54
|
+
app: app,
|
55
|
+
host: host,
|
56
|
+
port: port,
|
57
|
+
timeout: timeout
|
56
58
|
})
|
57
59
|
end
|
58
60
|
end
|
59
|
-
|
60
|
-
protected
|
61
|
-
|
62
|
-
def expand_env(env)
|
63
|
-
env &&= env.dup.downcase
|
64
|
-
case env
|
65
|
-
when /^dev(el(op)?)?$/ then "development"
|
66
|
-
when /^test(ing)?$/ then "testing"
|
67
|
-
else env
|
68
|
-
end
|
69
|
-
end
|
70
61
|
end
|
71
62
|
end
|
data/lib/hatetepe/client.rb
CHANGED
@@ -1,218 +1,247 @@
|
|
1
1
|
require "em-synchrony"
|
2
2
|
require "eventmachine"
|
3
|
-
require "rack"
|
4
|
-
require "uri"
|
5
3
|
|
6
4
|
require "hatetepe/builder"
|
7
5
|
require "hatetepe/connection"
|
8
|
-
require "hatetepe/deferred_status_fix"
|
9
6
|
require "hatetepe/parser"
|
10
7
|
require "hatetepe/request"
|
11
8
|
require "hatetepe/version"
|
12
9
|
|
13
|
-
module Hatetepe
|
14
|
-
|
15
|
-
|
10
|
+
module Hatetepe::Client
|
11
|
+
include Hatetepe::Connection
|
12
|
+
|
13
|
+
# @api private
|
14
|
+
Job = Struct.new(:fiber, :request, :sent, :response)
|
15
|
+
|
16
|
+
# The default configuration.
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
CONFIG_DEFAULTS = {
|
20
|
+
:timeout => 5,
|
21
|
+
:connect_timeout => 5
|
22
|
+
}
|
16
23
|
|
17
|
-
|
18
|
-
|
24
|
+
# The configuration for this Client instance.
|
25
|
+
#
|
26
|
+
# @api public
|
27
|
+
attr_reader :config
|
19
28
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
attr_reader :
|
24
|
-
|
29
|
+
# The pipe of middleware and request transmission/response reception.
|
30
|
+
#
|
31
|
+
# @api private
|
32
|
+
attr_reader :app
|
33
|
+
|
34
|
+
# Initializes a new Client instance.
|
35
|
+
#
|
36
|
+
# @param [Hash] config
|
37
|
+
# Configuration values that overwrite the defaults.
|
38
|
+
#
|
39
|
+
# @api semipublic
|
25
40
|
def initialize(config)
|
26
|
-
@config = config
|
27
|
-
@parser, @builder = Hatetepe::Parser.new, Hatetepe::Builder.new
|
28
|
-
|
29
|
-
@requests = []
|
30
|
-
@pending_transmission, @pending_response = {}, {}
|
31
|
-
|
32
|
-
@app = Rack::Builder.new.tap do |b|
|
33
|
-
b.use KeepAlive
|
34
|
-
b.use Pipeline
|
35
|
-
b.run method(:send_request)
|
36
|
-
end.to_app
|
37
|
-
|
38
|
-
super
|
41
|
+
@config = CONFIG_DEFAULTS.merge(config)
|
39
42
|
end
|
40
|
-
|
43
|
+
|
44
|
+
# Initializes the parser, request queue, and middleware pipe.
|
45
|
+
#
|
46
|
+
# TODO: Use +Rack::Builder+ for building the app pipe.
|
47
|
+
#
|
48
|
+
# @see EM::Connection#post_init
|
49
|
+
#
|
50
|
+
# @api semipublic
|
41
51
|
def post_init
|
42
|
-
parser.
|
43
|
-
|
44
|
-
builder.on_write
|
45
|
-
|
46
|
-
|
47
|
-
|
52
|
+
@builder, @parser = Hatetepe::Builder.new, Hatetepe::Parser.new
|
53
|
+
@builder.on_write << method(:send_data)
|
54
|
+
# @builder.on_write {|data| p "|--> #{data}" }
|
55
|
+
@parser.on_response << method(:receive_response)
|
56
|
+
|
57
|
+
@queue = []
|
58
|
+
|
59
|
+
@app = method(:send_request)
|
60
|
+
|
61
|
+
self.comm_inactivity_timeout = config[:timeout]
|
62
|
+
self.pending_connect_timeout = config[:connect_timeout]
|
48
63
|
end
|
49
|
-
|
64
|
+
|
65
|
+
# Feeds response data into the parser.
|
66
|
+
#
|
67
|
+
# @see EM::Connection#receive_data
|
68
|
+
#
|
69
|
+
# @param [String] data
|
70
|
+
# The received data that's gonna be fed into the parser.
|
71
|
+
#
|
72
|
+
# @api semipublic
|
50
73
|
def receive_data(data)
|
51
|
-
#p "
|
52
|
-
parser << data
|
53
|
-
rescue => e
|
54
|
-
close_connection
|
55
|
-
raise e
|
56
|
-
end
|
57
|
-
|
58
|
-
def send_request(request)
|
59
|
-
id = request.object_id
|
60
|
-
|
61
|
-
request.headers.delete "X-Hatetepe-Single"
|
62
|
-
builder.request request.to_a
|
63
|
-
pending_transmission[id].succeed
|
64
|
-
|
65
|
-
pending_response[id] = EM::DefaultDeferrable.new
|
66
|
-
EM::Synchrony.sync pending_response[id]
|
67
|
-
ensure
|
68
|
-
pending_response.delete id
|
74
|
+
# p "|<-- #{data}"
|
75
|
+
@parser << data
|
69
76
|
end
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
77
|
+
|
78
|
+
# Aborts all outstanding requests.
|
79
|
+
#
|
80
|
+
# @see EM::Connection#unbind
|
81
|
+
#
|
82
|
+
# @api semipublic
|
83
|
+
def unbind(reason)
|
84
|
+
super
|
85
|
+
@queue.each {|job| job.fiber.resume(:kill) }
|
76
86
|
end
|
77
|
-
|
78
|
-
def <<(request)
|
79
|
-
request.headers["Host"] ||= "#{config[:host]}:#{config[:port]}"
|
80
87
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
88
|
+
# Sends a request and waits for the response without blocking.
|
89
|
+
#
|
90
|
+
# Transmission and reception are performed within a separate +Fiber+.
|
91
|
+
# +#succeed+ and +#fail+ will be called on the +request+ passing the
|
92
|
+
# response, depending on whether the response indicates success (100-399)
|
93
|
+
# or failure (400-599).
|
94
|
+
#
|
95
|
+
# The request will +#fail+ with a +nil+ response if the connection was
|
96
|
+
# closed for whatever reason.
|
97
|
+
#
|
98
|
+
# @api public
|
99
|
+
def <<(request)
|
89
100
|
Fiber.new do
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
status = (response && response.success?) ? :succeed : :fail
|
97
|
-
requests.delete(request).send status, response
|
98
|
-
end
|
99
|
-
ensure
|
100
|
-
pending_transmission.delete request.object_id
|
101
|
+
response = @app.call(request)
|
102
|
+
|
103
|
+
if !response || response.failure?
|
104
|
+
request.fail(response)
|
105
|
+
else
|
106
|
+
request.succeed(response)
|
101
107
|
end
|
102
108
|
end.resume
|
103
109
|
end
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
request.
|
110
|
+
|
111
|
+
# Builds a +Request+, sends it, and blocks while waiting for the response.
|
112
|
+
#
|
113
|
+
# @param [Symbol, String] verb
|
114
|
+
# The HTTP method verb, e.g. +:get+ or +"PUT"+.
|
115
|
+
# @param [String, URI] uri
|
116
|
+
# The request URI.
|
117
|
+
# @param [Hash] headers (optional)
|
118
|
+
# The request headers.
|
119
|
+
# @param [#each] body (optional)
|
120
|
+
# A request body object whose +#each+ method yields objects that respond
|
121
|
+
# to +#to_s+.
|
122
|
+
#
|
123
|
+
# @return [Hatetepe::Response, nil]
|
124
|
+
#
|
125
|
+
# @api public
|
126
|
+
def request(verb, uri, headers = {}, body = [])
|
127
|
+
request = Hatetepe::Request.new(verb, uri, headers, body)
|
128
|
+
self << request
|
129
|
+
EM::Synchrony.sync(request)
|
122
130
|
end
|
123
|
-
|
131
|
+
|
132
|
+
# Gracefully stops the client.
|
133
|
+
#
|
134
|
+
# Waits for all requests to finish and then stops the client.
|
135
|
+
#
|
136
|
+
# @api public
|
124
137
|
def stop
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
138
|
+
wait
|
139
|
+
stop!
|
140
|
+
end
|
141
|
+
|
142
|
+
# Immediately stops the client by closing the connection.
|
143
|
+
#
|
144
|
+
# This will lead to EventMachine's event loop calling {#unbind}, which fail
|
145
|
+
# all outstanding requests.
|
146
|
+
#
|
147
|
+
# @see #unbind
|
148
|
+
#
|
149
|
+
# @api public
|
150
|
+
def stop!
|
129
151
|
close_connection
|
130
152
|
end
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
end
|
142
|
-
# fail reponse body if the response has already been started
|
143
|
-
if req.response
|
144
|
-
req.response.body.tap {|b| b.close_write unless b.closed_write? }
|
145
|
-
end
|
146
|
-
# XXX FiberError: dead fiber called because req already succeeded
|
147
|
-
# or failed, see github.com/eventmachine/eventmachine/issues/287
|
148
|
-
req.fail req.response
|
149
|
-
end
|
153
|
+
|
154
|
+
# Blocks until the last request has finished receiving its response.
|
155
|
+
#
|
156
|
+
# Returns immediately if there are no outstanding requests.
|
157
|
+
#
|
158
|
+
# @api public
|
159
|
+
def wait
|
160
|
+
if job = @queue.last
|
161
|
+
EM::Synchrony.sync(job.request)
|
162
|
+
EM::Synchrony.sync(job.response.body) if job.response
|
150
163
|
end
|
151
164
|
end
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
165
|
+
|
166
|
+
# Starts a new Client.
|
167
|
+
#
|
168
|
+
# @param [Hash] config
|
169
|
+
# The +:host+ and +:port+ the Client should connect to.
|
170
|
+
#
|
171
|
+
# @return [Hatetepe::Client]
|
172
|
+
# The new Client instance.
|
173
|
+
#
|
174
|
+
# @api public
|
175
|
+
def self.start(config)
|
176
|
+
EM.connect(config[:host], config[:port], self, config)
|
163
177
|
end
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
if Hash === body
|
168
|
-
query = lambda do |value|
|
169
|
-
case value
|
170
|
-
when Array
|
171
|
-
value.each &query
|
172
|
-
when Hash
|
173
|
-
value.values.each &query
|
174
|
-
when Rack::Multipart::UploadedFile
|
175
|
-
multipart = true
|
176
|
-
end
|
177
|
-
end
|
178
|
-
body.values.each &query
|
179
|
-
urlencoded = !multipart
|
180
|
-
end
|
181
|
-
|
182
|
-
body = if multipart
|
183
|
-
boundary = Rack::Multipart::MULTIPART_BOUNDARY
|
184
|
-
headers["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
|
185
|
-
[Rack::Multipart.build_multipart(body)]
|
186
|
-
elsif urlencoded
|
187
|
-
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
188
|
-
[Rack::Utils.build_nested_query(body)]
|
189
|
-
else
|
190
|
-
body
|
191
|
-
end
|
192
|
-
|
193
|
-
[headers, body]
|
178
|
+
|
179
|
+
# @api public
|
180
|
+
def self.request(verb, uri, headers = {}, body = [])
|
194
181
|
end
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
182
|
+
|
183
|
+
# Feeds the request into the builder and blocks while waiting for the
|
184
|
+
# response to arrive.
|
185
|
+
#
|
186
|
+
# Supports the request bit of HTTP pipelining by waiting until the previous
|
187
|
+
# request has been sent.
|
188
|
+
#
|
189
|
+
# @param [Hatetepe::Request] request
|
190
|
+
# The request that's gonna be sent.
|
191
|
+
#
|
192
|
+
# @return [Hatetepe::Response, nil]
|
193
|
+
# The received response or +nil+ if the connection has been closed before
|
194
|
+
# receiving a response.
|
195
|
+
#
|
196
|
+
# @api private
|
197
|
+
def send_request(request)
|
198
|
+
previous = @queue.last
|
199
|
+
current = Job.new(Fiber.current, request, false)
|
200
|
+
@queue << current
|
201
|
+
|
202
|
+
# wait for the previous request to be sent
|
203
|
+
while previous && !previous.sent
|
204
|
+
return if Fiber.yield == :kill
|
199
205
|
end
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
206
|
+
|
207
|
+
# send the request
|
208
|
+
self.comm_inactivity_timeout = 0
|
209
|
+
@builder.request(request.to_a)
|
210
|
+
current.sent = true
|
211
|
+
self.comm_inactivity_timeout = config[:timeout]
|
212
|
+
|
213
|
+
# wait for the response
|
214
|
+
while !current.response
|
215
|
+
return if Fiber.yield == :kill
|
209
216
|
end
|
217
|
+
|
218
|
+
# clean up and return response
|
219
|
+
@queue.delete(current)
|
220
|
+
current.response
|
210
221
|
end
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
222
|
+
|
223
|
+
# Relates an incoming response to the corresponding request.
|
224
|
+
#
|
225
|
+
# Supports the response bit of HTTP pipelining by relating responses to
|
226
|
+
# requests in the order the requests were sent.
|
227
|
+
#
|
228
|
+
# TODO: raise a more meaningful error.
|
229
|
+
#
|
230
|
+
# @param [Hatetepe::Response] response
|
231
|
+
# The incoming response
|
232
|
+
#
|
233
|
+
# @raise [RuntimeError]
|
234
|
+
# There is no request that's waiting for a response.
|
235
|
+
#
|
236
|
+
# @api private
|
237
|
+
def receive_response(response)
|
238
|
+
query = proc {|j| j.response.nil? }
|
239
|
+
|
240
|
+
if job = @queue.find(&query)
|
241
|
+
job.response = response
|
242
|
+
job.fiber.resume
|
243
|
+
else
|
244
|
+
raise "Received response but didn't expect one: #{response.status}"
|
216
245
|
end
|
217
246
|
end
|
218
247
|
end
|
data/lib/hatetepe/connection.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
require "eventmachine"
|
2
|
-
|
3
1
|
module Hatetepe
|
4
|
-
|
2
|
+
module Connection
|
5
3
|
attr_accessor :processing_enabled
|
6
4
|
alias_method :processing_enabled?, :processing_enabled
|
7
5
|
|
@@ -16,9 +14,17 @@ module Hatetepe
|
|
16
14
|
def sockaddr
|
17
15
|
@sockaddr ||= Socket.unpack_sockaddr_in(get_peername) rescue nil
|
18
16
|
end
|
17
|
+
|
18
|
+
def connection_completed
|
19
|
+
@connected = true
|
20
|
+
end
|
21
|
+
|
22
|
+
def connected?
|
23
|
+
defined?(@connected) && @connected
|
24
|
+
end
|
19
25
|
|
20
26
|
def closed?
|
21
|
-
|
27
|
+
!!defined?(@closed_by)
|
22
28
|
end
|
23
29
|
|
24
30
|
def closed_by_remote?
|
@@ -26,17 +32,30 @@ module Hatetepe
|
|
26
32
|
end
|
27
33
|
|
28
34
|
def closed_by_self?
|
29
|
-
|
35
|
+
@closed_by == :self
|
30
36
|
end
|
31
|
-
|
32
|
-
def
|
33
|
-
@closed_by
|
34
|
-
|
37
|
+
|
38
|
+
def closed_by_timeout?
|
39
|
+
connected? && @closed_by == :timeout
|
40
|
+
end
|
41
|
+
|
42
|
+
def closed_by_connect_timeout?
|
43
|
+
!connected? && @closed_by == :timeout
|
35
44
|
end
|
36
45
|
|
37
|
-
|
38
|
-
|
39
|
-
|
46
|
+
def close_connection(after_writing = false)
|
47
|
+
@closed_by = :self unless closed?
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
def unbind(reason)
|
52
|
+
unless closed?
|
53
|
+
@closed_by = if reason == Errno::ETIMEDOUT
|
54
|
+
:timeout
|
55
|
+
else
|
56
|
+
:remote
|
57
|
+
end
|
58
|
+
end
|
40
59
|
end
|
41
60
|
end
|
42
61
|
end
|