net-http-pipeline 0.1 → 1.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.
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