net-http-pipeline 0.1 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
data.tar.gz.sig CHANGED
Binary file
data/.autotest CHANGED
@@ -2,22 +2,7 @@
2
2
 
3
3
  require 'autotest/restart'
4
4
 
5
- # Autotest.add_hook :initialize do |at|
6
- # at.extra_files << "../some/external/dependency.rb"
7
- #
8
- # at.libs << ":../some/external"
9
- #
10
- # at.add_exception 'vendor'
11
- #
12
- # at.add_mapping(/dependency.rb/) do |f, _|
13
- # at.files_matching(/test_.*rb$/)
14
- # end
15
- #
16
- # %w(TestA TestB).each do |klass|
17
- # at.extra_class_map[klass] = "test/test_misc.rb"
18
- # end
19
- # end
5
+ Autotest.add_hook :initialize do |at|
6
+ at.testlib = 'minitest/autorun'
7
+ end
20
8
 
21
- # Autotest.add_hook :run_command do |at|
22
- # system "rake build"
23
- # end
File without changes
@@ -1,3 +1,15 @@
1
+ === 1.0 / 2011-03-29
2
+
3
+ * API change
4
+ * Net::HTTP::Pipeline#pipeline requires an Array of Net::HTTPRequests now.
5
+ * Major enhancement
6
+ * If a sequence of requests contains a non-idempotent request #pipeline now
7
+ waits for a response to the previous request.
8
+ * Minor enhancements
9
+ * Check for HTTP/1.1 and persistent connections before attempting
10
+ pipelining
11
+ * Added Net::HTTP#persistent= to avoid pipelining-capability check.
12
+
1
13
  === 0.1 / 2010-12-07
2
14
 
3
15
  * Minor enhancement
data/README.txt CHANGED
@@ -5,8 +5,9 @@
5
5
 
6
6
  == DESCRIPTION:
7
7
 
8
- An HTTP/1.1 pipelining implementation atop Net::HTTP. This is an experimental
9
- proof of concept.
8
+ An HTTP/1.1 pipelining implementation atop Net::HTTP. A pipelined connection
9
+ sends multiple requests to the HTTP server without waiting for the responses.
10
+ The server will respond in-order.
10
11
 
11
12
  == FEATURES/PROBLEMS:
12
13
 
@@ -21,8 +22,9 @@ proof of concept.
21
22
  Net::HTTP.start 'localhost' do |http|
22
23
  req1 = Net::HTTP::Get.new '/'
23
24
  req2 = Net::HTTP::Get.new '/'
25
+ req3 = Net::HTTP::Get.new '/'
24
26
 
25
- http.pipeline req1, req2 do |res|
27
+ http.pipeline [req1, req2, req3] do |res|
26
28
  puts res.code
27
29
  puts res.body[0..60].inspect
28
30
  puts
data/Rakefile CHANGED
@@ -9,6 +9,9 @@ Hoe.plugins.delete :rubyforge
9
9
 
10
10
  Hoe.spec 'net-http-pipeline' do
11
11
  developer 'Eric Hodel', 'drbrain@segment7.net'
12
+
13
+ rdoc_locations <<
14
+ 'docs.seattlerb.org:/data/www/docs.seattlerb.org/net-http-pipeline/'
12
15
  end
13
16
 
14
17
  # vim: syntax=Ruby
@@ -1,21 +1,28 @@
1
1
  require 'net/http'
2
2
 
3
3
  ##
4
- # An HTTP/1.1 pipelining implementation atop Net::HTTP. Currently this is not
4
+ # An HTTP/1.1 pipelining implementation atop Net::HTTP. This library is
5
5
  # compliant with RFC 2616 8.1.2.2.
6
6
  #
7
- # Pipeline allows pou to create a bunch of requests then pipeline them to an
8
- # HTTP/1.1 server.
7
+ # Pipeline allows you to create a bunch of requests then send them all to an
8
+ # HTTP/1.1 server without waiting for responses. The server will return HTTP
9
+ # responses in-order.
10
+ #
11
+ # Net::HTTP::Pipeline does not assume the server supports pipelining. If you
12
+ # know the server supports pipelining you can set Net::HTTP#pipelining to
13
+ # true.
9
14
  #
10
15
  # = Example
11
16
  #
12
17
  # require 'net/http/pipeline'
13
18
  #
14
19
  # Net::HTTP.start 'localhost' do |http|
15
- # req1 = Net::HTTP::Get.new '/'
16
- # req2 = Net::HTTP::Get.new '/'
20
+ # requests = []
21
+ # requests << Net::HTTP::Get.new('/')
22
+ # requests << Net::HTTP::Get.new('/')
23
+ # requests << Net::HTTP::Get.new('/')
17
24
  #
18
- # http.pipeline req1, req2 do |res|
25
+ # http.pipeline requests do |res|
19
26
  # puts res.code
20
27
  # puts res.body[0..60].inspect
21
28
  # puts
@@ -24,49 +31,225 @@ require 'net/http'
24
31
 
25
32
  module Net::HTTP::Pipeline
26
33
 
27
- VERSION = '0.1'
34
+ ##
35
+ # The version of net-http-pipeline you are using
36
+
37
+ VERSION = '1.0'
28
38
 
29
39
  ##
30
40
  # Pipeline error class
31
41
 
32
42
  class Error < RuntimeError
43
+
44
+ ##
45
+ # Remaining requests that have not been sent to the HTTP server
46
+
47
+ attr_reader :requests
48
+
49
+ ##
50
+ # Retrieved responses up to the error point
51
+
52
+ attr_reader :responses
53
+
54
+ ##
55
+ # Creates a new Error with +message+, a list of +requests+ that have not
56
+ # been sent to the server and a list of +responses+ that have been
57
+ # retrieved from the server.
58
+
59
+ def initialize message, requests, responses
60
+ super message
61
+
62
+ @requests = requests
63
+ @responses = responses
64
+ end
65
+
66
+ end
67
+
68
+ ##
69
+ # Raised when an invalid version is given
70
+
71
+ class VersionError < Error
72
+ ##
73
+ # Creates a new VersionError with a list of +requests+ that have not been
74
+ # sent to the server and a list of +responses+ that have been retrieved
75
+ # from the server.
76
+
77
+ def initialize requests, responses
78
+ super 'HTTP/1.1 or newer required', requests, responses
79
+ end
80
+ end
81
+
82
+ ##
83
+ # Raised when the server appears to not support persistent connections
84
+
85
+ class PersistenceError < Error
86
+ ##
87
+ # Creates a new PersistenceError with a list of +requests+ that have not
88
+ # been sent to the server and a list of +responses+ that have been
89
+ # retrieved from the server.
90
+
91
+ def initialize requests, responses
92
+ super 'persistent connections required', requests, responses
93
+ end
94
+ end
95
+
96
+ ##
97
+ # Raised when the server appears to not support pipelining connections
98
+
99
+ class PipelineError < Error
100
+ ##
101
+ # Creates a new PipelineError with a list of +requests+ that have not been
102
+ # sent to the server and a list of +responses+ that have been retrieved
103
+ # from the server.
104
+
105
+ def initialize requests, responses
106
+ super 'pipeline connections are not supported', requests, responses
107
+ end
108
+ end
109
+
110
+ ##
111
+ # Raised if an error occurs while reading responses.
112
+
113
+ class ResponseError < Error
114
+ ##
115
+ # The original exception
116
+
117
+ attr_accessor :original
118
+
119
+ ##
120
+ # Creates a new ResponseError with an original +exception+, a list of
121
+ # +requests+ that were in-flight and a list of +responses+ that have been
122
+ # retrieved from the server.
123
+
124
+ def initialize exception, requests, responses
125
+ @original = exception
126
+ message = "error handling responses: #{original} (#{original.class})"
127
+ super message, requests, responses
128
+ end
129
+ end
130
+
131
+ ##
132
+ # Pipelining capability accessor.
133
+ #
134
+ # Pipeline assumes servers do not support pipelining by default. The first
135
+ # request is not pipelined while Pipeline ensures that the server is
136
+ # HTTP/1.1 or newer and defaults to persistent connections.
137
+ #
138
+ # If you know the server is HTTP/1.1 and defaults to persistent
139
+ # connections you can set this to true when you create the Net::HTTP object.
140
+
141
+ attr_accessor :pipelining
142
+
143
+ ##
144
+ # Is +req+ idempotent according to RFC 2616?
145
+
146
+ def idempotent? req
147
+ case req
148
+ when Net::HTTP::Delete, Net::HTTP::Get, Net::HTTP::Head,
149
+ Net::HTTP::Options, Net::HTTP::Put, Net::HTTP::Trace then
150
+ true
151
+ end
33
152
  end
34
153
 
35
154
  ##
36
155
  # Pipelines +requests+ to the HTTP server yielding responses if a block is
37
156
  # given. Returns all responses recieved.
38
157
  #
39
- # Raises an exception if the connection is not pipelining-capable or if the
158
+ # The Net::HTTP connection must be started before calling #pipeline.
159
+ #
160
+ # Raises an exception if the connection is not pipeline-capable or if the
40
161
  # HTTP session has not been started.
41
162
 
42
- def pipeline *requests
43
- raise Error, 'pipelining requires HTTP/1.1 or newer' unless
44
- @curr_http_version >= '1.1'
45
- raise Error, 'Net::HTTP not started' unless started?
163
+ def pipeline requests, &block # :yields: response
164
+ responses = []
46
165
 
47
- requests.each do |req|
48
- begin_transport req
49
- req.exec @socket, @curr_http_version, edit_path(req.path)
50
- end
166
+ raise Error.new('Net::HTTP not started', requests, responses) unless
167
+ started?
51
168
 
52
- responses = []
169
+ raise VersionError.new(requests, responses) if '1.1' > @curr_http_version
170
+
171
+ pipeline_check requests, responses, &block
172
+
173
+ retried = responses.length
53
174
 
54
- requests.each do |req|
175
+ until requests.empty? do
55
176
  begin
56
- res = Net::HTTPResponse.read_new @socket
57
- end while res.kind_of? Net::HTTPContinue
177
+ in_flight = pipeline_send requests
58
178
 
59
- res.reading_body @socket, req.response_body_permitted? do
60
- responses << res
61
- yield res if block_given?
62
- end
179
+ pipeline_receive in_flight, responses, &block
180
+ rescue Net::HTTP::Pipeline::ResponseError => e
181
+ e.requests.reverse_each do |request|
182
+ requests.unshift request
183
+ end
184
+
185
+ raise if responses.length == retried or not idempotent? requests.first
186
+
187
+ retried = responses.length
188
+
189
+ pipeline_reset requests, responses
63
190
 
64
- pipeline_end_transport res
191
+ retry
192
+ end
65
193
  end
66
194
 
67
195
  responses
68
196
  end
69
197
 
198
+ ##
199
+ # Ensures the connection supports pipelining.
200
+ #
201
+ # If the server has not been tested for pipelining support one of the
202
+ # +requests+ will be consumed and placed in +responses+.
203
+ #
204
+ # A VersionError will be raised if the server is not HTTP/1.1 or newer.
205
+ #
206
+ # A PersistenceError will be raised if the server does not support
207
+ # persistent connections.
208
+ #
209
+ # A PipelineError will be raised if the it was previously determined that
210
+ # the server does not support pipelining.
211
+
212
+ def pipeline_check requests, responses
213
+ if instance_variable_defined? :@pipelining then
214
+ return if @pipelining
215
+ raise PipelineError.new(requests, responses) unless @pipelining
216
+ else
217
+ @pipelining = false
218
+ end
219
+
220
+ req = requests.shift
221
+ retried = false
222
+
223
+ begin
224
+ res = request req
225
+ rescue Timeout::Error, EOFError, Errno::ECONNABORTED, Errno::ECONNRESET,
226
+ Errno::EPIPE, Net::HTTPBadResponse => e
227
+ if retried then
228
+ requests.unshift req
229
+ raise ResponseError.new(e, requests, responses)
230
+ end
231
+
232
+ retried = true
233
+
234
+ pipeline_reset requests, responses
235
+
236
+ retry
237
+ end
238
+
239
+ responses << res
240
+
241
+ yield res if block_given?
242
+
243
+ @pipelining = pipeline_keep_alive? res
244
+
245
+ if '1.1' > @curr_http_version then
246
+ @pipelining = false
247
+ raise VersionError.new(requests, responses)
248
+ elsif not @pipelining then
249
+ raise PersistenceError.new(requests, responses)
250
+ end
251
+ end
252
+
70
253
  ##
71
254
  # Updates the HTTP version and ensures the connection has keep-alive.
72
255
 
@@ -83,6 +266,14 @@ module Net::HTTP::Pipeline
83
266
  end
84
267
  end
85
268
 
269
+ ##
270
+ # Closes the connection and rescues any IOErrors this may cause
271
+
272
+ def pipeline_finish
273
+ finish
274
+ rescue IOError
275
+ end
276
+
86
277
  if Net::HTTPResponse.allocate.respond_to? :connection_close? then
87
278
  ##
88
279
  # Checks for an connection close header
@@ -92,10 +283,86 @@ module Net::HTTP::Pipeline
92
283
  end
93
284
  else
94
285
  def pipeline_keep_alive? res
95
- not /close/i =~ res['connection'].to_s
286
+ not res['connection'].to_s =~ /close/i
96
287
  end
97
288
  end
98
289
 
290
+ ##
291
+ # Receives HTTP responses for +in_flight+ requests and adds them to
292
+ # +responses+
293
+
294
+ def pipeline_receive in_flight, responses
295
+ while req = in_flight.shift do
296
+ begin
297
+ begin
298
+ res = Net::HTTPResponse.read_new @socket
299
+ end while res.kind_of? Net::HTTPContinue
300
+
301
+ res.reading_body @socket, req.response_body_permitted? do
302
+ responses << res
303
+ yield res if block_given?
304
+ end
305
+
306
+ pipeline_end_transport res
307
+ rescue StandardError, Timeout::Error
308
+ in_flight.unshift req
309
+ raise
310
+ end
311
+ end
312
+
313
+ responses
314
+ rescue Timeout::Error, EOFError, Errno::ECONNABORTED, Errno::ECONNRESET,
315
+ Errno::EPIPE, Net::HTTPBadResponse => e
316
+ pipeline_finish
317
+
318
+ raise ResponseError.new(e, in_flight, responses)
319
+ end
320
+
321
+ ##
322
+ # Resets this connection
323
+
324
+ def pipeline_reset requests, responses
325
+ pipeline_finish
326
+
327
+ start
328
+ rescue Errno::ECONNREFUSED
329
+ raise Error.new("connection refused: #{address}:#{port}", requests,
330
+ responses)
331
+ rescue Errno::EHOSTDOWN
332
+ raise Error.new("host down: #{address}:#{port}", requests, responses)
333
+ end
334
+
335
+ ##
336
+ # Sends +requests+ to the HTTP server and removes them from the +requests+
337
+ # list. Returns the requests that have been pipelined and are in-flight.
338
+ #
339
+ # If a non-idempotent request is first in +requests+ it will be sent and no
340
+ # further requests will be pipelined.
341
+ #
342
+ # If a non-idempotent request is encountered after an idempotent request it
343
+ # will not be sent.
344
+
345
+ def pipeline_send requests
346
+ in_flight = []
347
+
348
+ while req = requests.shift do
349
+ idempotent = idempotent? req
350
+
351
+ unless idempotent or in_flight.empty? then
352
+ requests.unshift req
353
+ break
354
+ end
355
+
356
+ begin_transport req
357
+ req.exec @socket, @curr_http_version, edit_path(req.path)
358
+ in_flight << req
359
+
360
+ break unless idempotent
361
+ end
362
+
363
+ in_flight
364
+ end
365
+
99
366
  end
100
367
 
101
368
  class Net::HTTP
@@ -4,12 +4,27 @@ require 'stringio'
4
4
 
5
5
  class TestNetHttpPipeline < MiniTest::Unit::TestCase
6
6
 
7
+ include Net::HTTP::Pipeline
8
+
9
+ def setup
10
+ @curr_http_version = '1.1'
11
+ @started = true
12
+
13
+ @get1 = Net::HTTP::Get.new '/'
14
+ @get2 = Net::HTTP::Get.new '/'
15
+ @get3 = Net::HTTP::Get.new '/'
16
+ @post = Net::HTTP::Post.new '/'
17
+ end
18
+
7
19
  ##
8
20
  # Net::BufferedIO stub
9
21
 
10
22
  class Buffer
11
23
  attr_accessor :read_io, :write_io
12
- def initialize
24
+ def initialize exception = nil, immediate = false
25
+ @readline = immediate
26
+ @exception = exception
27
+
13
28
  @read_io = StringIO.new
14
29
  @write_io = StringIO.new
15
30
  @closed = false
@@ -34,6 +49,8 @@ class TestNetHttpPipeline < MiniTest::Unit::TestCase
34
49
  end
35
50
 
36
51
  def readline
52
+ raise @exception if @exception and @readline
53
+ @readline = true
37
54
  @read_io.readline.chomp "\r\n"
38
55
  end
39
56
 
@@ -50,15 +67,8 @@ class TestNetHttpPipeline < MiniTest::Unit::TestCase
50
67
  end
51
68
  end
52
69
 
53
- include Net::HTTP::Pipeline
54
-
55
70
  attr_writer :started
56
71
 
57
- def setup
58
- @curr_http_version = '1.1'
59
- @started = true
60
- end
61
-
62
72
  def D(*) end
63
73
 
64
74
  def begin_transport req
@@ -68,14 +78,28 @@ class TestNetHttpPipeline < MiniTest::Unit::TestCase
68
78
  path
69
79
  end
70
80
 
71
- def http_request
72
- http_request = []
73
- http_request << 'GET / HTTP/1.1'
74
- http_request << 'Accept: */*'
75
- http_request << 'User-Agent: Ruby' if RUBY_VERSION > '1.9'
76
- http_request.push nil, nil
81
+ def finish
82
+ @socket.close
83
+ end
84
+
85
+ def http_get
86
+ get = []
87
+ get << 'GET / HTTP/1.1'
88
+ get << 'Accept: */*'
89
+ get << 'User-Agent: Ruby' if RUBY_VERSION > '1.9'
90
+ get.push nil, nil
91
+
92
+ get.join "\r\n"
93
+ end
77
94
 
78
- http_request.join "\r\n"
95
+ def http_post
96
+ get = []
97
+ get << 'POST / HTTP/1.1'
98
+ get << 'Accept: */*'
99
+ get << 'User-Agent: Ruby' if RUBY_VERSION > '1.9'
100
+ get.push nil, nil
101
+
102
+ get.join "\r\n"
79
103
  end
80
104
 
81
105
  def http_response body, *extra_header
@@ -88,6 +112,31 @@ class TestNetHttpPipeline < MiniTest::Unit::TestCase
88
112
  http_response.join("\r\n") << body
89
113
  end
90
114
 
115
+ def http_bad_response
116
+ http_response = []
117
+ http_response << 'HTP/1.1 200 OK'
118
+ http_response << 'Content-Length: 0'
119
+ http_response.push nil, nil # Array chomps on #join
120
+
121
+ http_response.join("\r\n")
122
+ end
123
+
124
+ def request req
125
+ req.exec @socket, @curr_http_version, edit_path(req.path)
126
+
127
+ res = Net::HTTPResponse.read_new @socket
128
+
129
+ res.reading_body @socket, req.response_body_permitted? do
130
+ yield res if block_given?
131
+ end
132
+
133
+ @curr_http_version = res.http_version
134
+
135
+ @socket.close unless pipeline_keep_alive? res
136
+
137
+ res
138
+ end
139
+
91
140
  def response
92
141
  r = Net::HTTPResponse.allocate
93
142
  def r.http_version() Net::HTTP::HTTPVersion end
@@ -98,70 +147,370 @@ class TestNetHttpPipeline < MiniTest::Unit::TestCase
98
147
  r
99
148
  end
100
149
 
150
+ def start
151
+ end
152
+
101
153
  def started?() @started end
102
154
 
155
+ # tests start
156
+
157
+ def test_idempotent_eh
158
+ http = Net::HTTP.new 'localhost'
159
+
160
+ assert http.idempotent? Net::HTTP::Delete.new '/'
161
+ assert http.idempotent? Net::HTTP::Get.new '/'
162
+ assert http.idempotent? Net::HTTP::Head.new '/'
163
+ assert http.idempotent? Net::HTTP::Options.new '/'
164
+ assert http.idempotent? Net::HTTP::Put.new '/'
165
+ assert http.idempotent? Net::HTTP::Trace.new '/'
166
+
167
+ refute http.idempotent? Net::HTTP::Post.new '/'
168
+ end
169
+
103
170
  def test_pipeline
104
171
  @socket = Buffer.new
105
-
106
172
  @socket.read_io.write http_response('Worked 1!')
107
173
  @socket.read_io.write http_response('Worked 2!')
108
-
109
- req1 = Net::HTTP::Get.new '/'
110
- req2 = Net::HTTP::Get.new '/'
111
-
112
174
  @socket.start
113
175
 
114
- responses = pipeline req1, req2
176
+ requests = [@get1, @get2]
177
+
178
+ responses = pipeline requests
115
179
 
116
180
  @socket.finish
117
181
 
118
- expected = ''
119
- expected << http_request
120
- expected << http_request
182
+ expected = http_get * 2
121
183
 
122
184
  assert_equal expected, @socket.write_io.read
123
185
  refute @socket.closed?
124
186
 
125
187
  assert_equal 'Worked 1!', responses.first.body
126
188
  assert_equal 'Worked 2!', responses.last.body
189
+
190
+ assert_empty requests
127
191
  end
128
192
 
129
- def test_pipeline_connection_close
193
+ def test_pipeline_block
130
194
  @socket = Buffer.new
131
-
132
- @socket.read_io.write http_response('Worked 1!', 'Connection: close')
133
-
134
- req1 = Net::HTTP::Get.new '/'
135
-
195
+ @socket.read_io.write http_response('Worked 1!')
196
+ @socket.read_io.write http_response('Worked 2!')
136
197
  @socket.start
137
198
 
138
- pipeline req1
199
+ requests = [@get1, @get2]
200
+ responses = []
201
+
202
+ pipeline requests do |response| responses << response end
139
203
 
140
204
  @socket.finish
141
205
 
142
- assert @socket.closed?
206
+ assert_equal 'Worked 1!', responses.first.body
207
+ assert_equal 'Worked 2!', responses.last.body
143
208
  end
144
209
 
145
210
  def test_pipeline_http_1_0
146
211
  @curr_http_version = '1.0'
147
212
 
148
- e = assert_raises Net::HTTP::Pipeline::Error do
149
- pipeline
213
+ @socket = Buffer.new
214
+ @socket.read_io.write http_response('Worked 1!', 'Connection: close')
215
+ @socket.start
216
+
217
+ e = assert_raises Net::HTTP::Pipeline::VersionError do
218
+ pipeline [@get1, @get2]
150
219
  end
151
220
 
152
- assert_equal 'pipelining requires HTTP/1.1 or newer', e.message
221
+ assert_equal [@get1, @get2], e.requests
222
+ assert_empty e.responses
223
+ end
224
+
225
+ def test_pipeline_non_idempotent
226
+ @socket = Buffer.new
227
+ @socket.read_io.write http_response('Worked 1!')
228
+ @socket.read_io.write http_response('Worked 2!')
229
+ @socket.read_io.write http_response('Worked 3!')
230
+ @socket.read_io.write http_response('Worked 4!')
231
+ @socket.start
232
+
233
+ responses = pipeline [@get1, @get2, @post, @get3]
234
+
235
+ @socket.finish
236
+
237
+ expected = ''
238
+ expected << http_get * 2
239
+ expected << http_post
240
+ expected << http_get
241
+
242
+ assert_equal expected, @socket.write_io.read
243
+ refute @socket.closed?
244
+
245
+ assert_equal 'Worked 1!', responses.shift.body
246
+ assert_equal 'Worked 2!', responses.shift.body
247
+ assert_equal 'Worked 3!', responses.shift.body
248
+ assert_equal 'Worked 4!', responses.shift.body
249
+
250
+ assert responses.empty?
153
251
  end
154
252
 
155
253
  def test_pipeline_not_started
156
254
  @started = false
157
255
 
158
256
  e = assert_raises Net::HTTP::Pipeline::Error do
159
- pipeline
257
+ pipeline []
160
258
  end
161
259
 
162
260
  assert_equal 'Net::HTTP not started', e.message
163
261
  end
164
262
 
263
+ def test_pipeline_retry
264
+ self.pipelining = true
265
+ @error_socket = Buffer.new Errno::ECONNRESET
266
+ @error_socket.read_io.write http_response('Worked 1!')
267
+ @error_socket.start
268
+
269
+ @good_socket = Buffer.new
270
+ @good_socket.read_io.write http_response('Worked 2!')
271
+ @good_socket.read_io.write http_response('Worked 3!')
272
+ @good_socket.start
273
+
274
+ @socket = @error_socket
275
+
276
+ def start
277
+ @socket = @good_socket
278
+ end
279
+
280
+ requests = [@get1, @get2, @get3]
281
+
282
+ responses = pipeline requests
283
+
284
+ @error_socket.finish
285
+ @good_socket.finish
286
+
287
+ assert_equal http_get * 3, @error_socket.write_io.read
288
+ assert @error_socket.closed?
289
+
290
+ assert_equal http_get * 2, @good_socket.write_io.read
291
+ refute @good_socket.closed?
292
+
293
+ assert_equal 'Worked 1!', responses.shift.body
294
+ assert_equal 'Worked 2!', responses.shift.body
295
+ assert_equal 'Worked 3!', responses.shift.body
296
+ assert_empty responses
297
+
298
+ assert_empty requests
299
+ end
300
+
301
+ def test_pipeline_retry_fail_post
302
+ self.pipelining = true
303
+
304
+ @socket = Buffer.new Errno::ECONNRESET, true
305
+ @socket.start
306
+
307
+ requests = [@post]
308
+
309
+ e = assert_raises Net::HTTP::Pipeline::ResponseError do
310
+ pipeline requests
311
+ end
312
+
313
+ @socket.finish
314
+
315
+ assert_equal http_post, @socket.write_io.read
316
+
317
+ assert_empty e.responses
318
+
319
+ assert_equal [@post], e.requests
320
+ end
321
+
322
+ def test_pipeline_retry_fail_different
323
+ self.pipelining = true
324
+ @error_socket = Buffer.new Errno::ECONNRESET
325
+ @error_socket.read_io.write http_response('Worked 1!')
326
+ @error_socket.start
327
+
328
+ @error_socket2 = Buffer.new Errno::ECONNRESET
329
+ @error_socket2.read_io.write http_response('Worked 2!')
330
+ @error_socket2.start
331
+
332
+ @good_socket = Buffer.new
333
+ @good_socket.read_io.write http_response('Worked 3!')
334
+ @good_socket.start
335
+
336
+ @socket = @error_socket
337
+
338
+ @sockets = [@error_socket2, @good_socket]
339
+
340
+ def start
341
+ @socket = @sockets.shift
342
+ end
343
+
344
+ requests = [@get1, @get2, @get3]
345
+
346
+ responses = pipeline requests
347
+
348
+ @error_socket.finish
349
+ @error_socket2.finish
350
+ @good_socket.finish
351
+
352
+ assert_equal http_get * 3, @error_socket.write_io.read
353
+ assert @error_socket.closed?
354
+
355
+ assert_equal http_get * 2, @error_socket2.write_io.read
356
+ assert @error_socket2.closed?
357
+
358
+ assert_equal http_get, @good_socket.write_io.read
359
+ refute @good_socket.closed?
360
+
361
+ assert_equal 'Worked 1!', responses.shift.body
362
+ assert_equal 'Worked 2!', responses.shift.body
363
+ assert_equal 'Worked 3!', responses.shift.body
364
+ assert_empty responses
365
+
366
+ assert_empty requests
367
+ end
368
+
369
+ def test_pipeline_retry_fail_same
370
+ self.pipelining = true
371
+
372
+ @error_socket = Buffer.new Errno::ECONNRESET
373
+ @error_socket.read_io.write http_response('Worked 1!')
374
+ @error_socket.start
375
+
376
+ @error_socket2 = Buffer.new Errno::ECONNRESET, true
377
+ @error_socket2.start
378
+
379
+ @socket = @error_socket
380
+
381
+ def start
382
+ @socket = @error_socket2
383
+ end
384
+
385
+ requests = [@get1, @get2, @get3]
386
+
387
+ e = assert_raises Net::HTTP::Pipeline::ResponseError do
388
+ pipeline requests
389
+ end
390
+
391
+ @error_socket.finish
392
+ @error_socket2.finish
393
+
394
+ assert_equal http_get * 3, @error_socket.write_io.read
395
+ assert @error_socket.closed?
396
+
397
+ assert_equal http_get * 2, @error_socket2.write_io.read
398
+ assert @error_socket2.closed?
399
+
400
+ responses = e.responses
401
+ assert_equal 'Worked 1!', responses.shift.body
402
+ assert_empty responses
403
+
404
+ assert_equal [@get2, @get3], e.requests
405
+ end
406
+
407
+ # end #pipeline tests
408
+
409
+ def test_pipeline_check
410
+ @socket = Buffer.new
411
+ @socket.read_io.write http_response('Worked 1!')
412
+ @socket.start
413
+
414
+ requests = [@get1, @get2]
415
+ responses = []
416
+
417
+ pipeline_check requests, responses
418
+
419
+ assert_equal [@get2], requests
420
+ assert_equal 1, responses.length
421
+ assert_equal 'Worked 1!', responses.first.body
422
+ assert pipelining
423
+ end
424
+
425
+ def test_pipeline_check_again
426
+ self.pipelining = false
427
+
428
+ @socket = Buffer.new
429
+ @socket.start
430
+
431
+ e = assert_raises Net::HTTP::Pipeline::PipelineError do
432
+ pipeline_check [@get1, @get2], []
433
+ end
434
+
435
+ assert_equal [@get1, @get2], e.requests
436
+ assert_empty e.responses
437
+ refute pipelining
438
+ end
439
+
440
+ def test_pipeline_check_bad_response
441
+ @socket = Buffer.new
442
+ @socket.read_io.write http_bad_response
443
+ @socket.start
444
+
445
+ @socket2 = Buffer.new
446
+ @socket2.read_io.write http_response('Worked 1!')
447
+ @socket2.start
448
+
449
+ def start
450
+ @socket = @socket2
451
+ end
452
+
453
+ requests = [@get1, @get2]
454
+ responses = []
455
+
456
+ pipeline_check requests, responses
457
+
458
+ assert_equal [@get2], requests
459
+ assert_equal 1, responses.length
460
+ assert_equal 'Worked 1!', responses.first.body
461
+ assert pipelining
462
+ end
463
+
464
+ def test_pipeline_check_http_1_0
465
+ @socket = Buffer.new
466
+ @socket.read_io.write <<-HTTP_1_0
467
+ HTTP/1.0 200 OK\r
468
+ Content-Length: 9\r
469
+ \r
470
+ Worked 1!
471
+ HTTP_1_0
472
+ @socket.start
473
+
474
+ e = assert_raises Net::HTTP::Pipeline::VersionError do
475
+ pipeline_check [@get1, @get2], []
476
+ end
477
+
478
+ assert_equal [@get2], e.requests
479
+ assert_equal 1, e.responses.length
480
+ assert_equal 'Worked 1!', e.responses.first.body
481
+ refute pipelining
482
+ end
483
+
484
+ def test_pipeline_check_non_persistent
485
+ @socket = Buffer.new
486
+ @socket.read_io.write http_response('Worked 1!', 'Connection: close')
487
+ @socket.start
488
+
489
+ e = assert_raises Net::HTTP::Pipeline::PersistenceError do
490
+ pipeline_check [@get1, @get2], []
491
+ end
492
+
493
+ assert_equal [@get2], e.requests
494
+ assert_equal 1, e.responses.length
495
+ assert_equal 'Worked 1!', e.responses.first.body
496
+ refute pipelining
497
+ end
498
+
499
+ def test_pipeline_check_pipelining
500
+ self.pipelining = true
501
+ @socket = Buffer.new
502
+ @socket.start
503
+
504
+ requests = [@get1, @get2]
505
+ responses = []
506
+
507
+ pipeline_check requests, responses
508
+
509
+ assert_equal [@get1, @get2], requests
510
+ assert_empty responses
511
+ assert pipelining
512
+ end
513
+
165
514
  def test_pipeline_end_transport
166
515
  @curr_http_version = nil
167
516
 
@@ -199,5 +548,109 @@ class TestNetHttpPipeline < MiniTest::Unit::TestCase
199
548
  refute pipeline_keep_alive? res
200
549
  end
201
550
 
551
+ def test_pipeline_receive
552
+ @socket = Buffer.new
553
+ @socket.read_io.write http_response('Worked 1!')
554
+ @socket.read_io.write http_response('Worked 2!')
555
+ @socket.start
556
+
557
+ in_flight = [@get1, @get2]
558
+ responses = []
559
+
560
+ r = pipeline_receive in_flight, responses
561
+
562
+ @socket.finish
563
+
564
+ refute @socket.closed?
565
+
566
+ assert_equal 'Worked 1!', responses.first.body
567
+ assert_equal 'Worked 2!', responses.last.body
568
+
569
+ assert_same r, responses
570
+ end
571
+
572
+ def test_pipeline_receive_bad_response
573
+ @socket = Buffer.new Errno::ECONNRESET
574
+ @socket.read_io.write http_response('Worked 1!')
575
+ @socket.start
576
+
577
+ in_flight = [@get1, @get2]
578
+ responses = []
579
+
580
+ e = assert_raises Net::HTTP::Pipeline::ResponseError do
581
+ pipeline_receive in_flight, responses
582
+ end
583
+
584
+ @socket.finish
585
+
586
+ assert @socket.closed?
587
+
588
+ assert_equal [@get2], e.requests
589
+ assert_equal 1, e.responses.length
590
+ assert_equal 'Worked 1!', e.responses.first.body
591
+
592
+ assert_kind_of Errno::ECONNRESET, e.original
593
+ end
594
+
595
+ def test_pipeline_receive_timeout
596
+ @socket = Buffer.new Timeout::Error
597
+ @socket.read_io.write http_response('Worked 1!')
598
+ @socket.start
599
+
600
+ in_flight = [@get1, @get2]
601
+ responses = []
602
+
603
+ e = assert_raises Net::HTTP::Pipeline::ResponseError do
604
+ pipeline_receive in_flight, responses
605
+ end
606
+
607
+ @socket.finish
608
+
609
+ assert @socket.closed?
610
+
611
+ assert_equal [@get2], e.requests
612
+ assert_equal 1, e.responses.length
613
+ assert_equal 'Worked 1!', e.responses.first.body
614
+
615
+ assert_kind_of Timeout::Error, e.original
616
+ end
617
+
618
+ def test_pipeline_send
619
+ @socket = Buffer.new
620
+ @socket.start
621
+
622
+ requests = [@get1, @get2, @post, @get3]
623
+
624
+ in_flight = pipeline_send requests
625
+
626
+ @socket.finish
627
+
628
+ assert_equal [@get1, @get2], in_flight
629
+ assert_equal [@post, @get3], requests
630
+
631
+ expected = ''
632
+ expected << http_get * 2
633
+
634
+ assert_equal expected, @socket.write_io.read
635
+ end
636
+
637
+ def test_pipeline_send_non_idempotent
638
+ @socket = Buffer.new
639
+ @socket.start
640
+
641
+ requests = [@post, @get3]
642
+
643
+ in_flight = pipeline_send requests
644
+
645
+ @socket.finish
646
+
647
+ assert_equal [@post], in_flight
648
+ assert_equal [@get3], requests
649
+
650
+ expected = http_post
651
+
652
+ assert_equal expected, @socket.write_io.read
653
+ end
654
+
202
655
  end
203
656
 
metadata CHANGED
@@ -1,12 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-http-pipeline
3
3
  version: !ruby/object:Gem::Version
4
- hash: 9
5
- prerelease: false
4
+ hash: 15
5
+ prerelease:
6
6
  segments:
7
- - 0
8
7
  - 1
9
- version: "0.1"
8
+ - 0
9
+ version: "1.0"
10
10
  platform: ruby
11
11
  authors:
12
12
  - Eric Hodel
@@ -35,7 +35,7 @@ cert_chain:
35
35
  x52qPcexcYZR7w==
36
36
  -----END CERTIFICATE-----
37
37
 
38
- date: 2010-12-07 00:00:00 -08:00
38
+ date: 2011-03-29 00:00:00 -07:00
39
39
  default_executable:
40
40
  dependencies:
41
41
  - !ruby/object:Gem::Dependency
@@ -46,12 +46,12 @@ dependencies:
46
46
  requirements:
47
47
  - - ">="
48
48
  - !ruby/object:Gem::Version
49
- hash: 3
49
+ hash: 11
50
50
  segments:
51
- - 1
52
- - 5
51
+ - 2
53
52
  - 0
54
- version: 1.5.0
53
+ - 2
54
+ version: 2.0.2
55
55
  type: :development
56
56
  version_requirements: *id001
57
57
  - !ruby/object:Gem::Dependency
@@ -62,17 +62,18 @@ dependencies:
62
62
  requirements:
63
63
  - - ">="
64
64
  - !ruby/object:Gem::Version
65
- hash: 19
65
+ hash: 41
66
66
  segments:
67
67
  - 2
68
- - 7
69
- - 0
70
- version: 2.7.0
68
+ - 9
69
+ - 1
70
+ version: 2.9.1
71
71
  type: :development
72
72
  version_requirements: *id002
73
73
  description: |-
74
- An HTTP/1.1 pipelining implementation atop Net::HTTP. This is an experimental
75
- proof of concept.
74
+ An HTTP/1.1 pipelining implementation atop Net::HTTP. A pipelined connection
75
+ sends multiple requests to the HTTP server without waiting for the responses.
76
+ The server will respond in-order.
76
77
  email:
77
78
  - drbrain@segment7.net
78
79
  executables: []
@@ -92,6 +93,7 @@ files:
92
93
  - lib/net/http/pipeline.rb
93
94
  - sample/two_get.rb
94
95
  - test/test_net_http_pipeline.rb
96
+ - .gemtest
95
97
  has_rdoc: true
96
98
  homepage: http://seattlerb.rubyforge.org/net-http-pipeline
97
99
  licenses: []
@@ -123,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
125
  requirements: []
124
126
 
125
127
  rubyforge_project: net-http-pipeline
126
- rubygems_version: 1.3.7
128
+ rubygems_version: 1.6.2
127
129
  signing_key:
128
130
  specification_version: 3
129
131
  summary: An HTTP/1.1 pipelining implementation atop Net::HTTP
metadata.gz.sig CHANGED
Binary file