td-client 0.8.80 → 0.8.81

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6b90ad6943946fa449277adfcb6cf73151d06f5d
4
- data.tar.gz: 3955d6677c5ff35ec1da44ed517d8ab69fca7b47
3
+ metadata.gz: f6639c3d87e9a018b4d8270c64165afc64ddb5c1
4
+ data.tar.gz: f0d2a64b5cb60ec6bc0b43f53f6ea0f8136e236f
5
5
  SHA512:
6
- metadata.gz: 21a28c9c7d85fa5b70eca4f9d841cbea5ee19a240140201046a12a3bfd13e52293a6bd6cbccfa4c4b2526b41689f9c027923f0f2b9260853491e839e2a7f603f
7
- data.tar.gz: ae79f478833ade0e3ab01c30e8eacdbc1ea3b1cbae613e9a68bead92b746b6b5165c1b9106abee517fb865a949813d9ef48eceff44a85bef7cf40bdb524c0109
6
+ metadata.gz: 7fe4f5336064479b42b5f867bd54014530aae360b175e3953aa3d82ee06822361d2c41219b9f64179791c08e918102de712ad323de39ef8f5e06ea2990d17e95
7
+ data.tar.gz: dd8dffeca422ea5d9e57ece36ed2655b4a2b57ef072dcf697dddd6f6c01d8a8a8ba04fd1f946d442982242d1028101830a8c6aefed676350ce168708c8d90324
@@ -235,19 +235,17 @@ private
235
235
 
236
236
  # @param [String] url
237
237
  # @param [Hash] params
238
- # @param [Hash] opt
239
238
  # @yield [response]
240
- def get(url, params=nil, opt={}, &block)
239
+ def get(url, params=nil, &block)
241
240
  guard_no_sslv3 do
242
- do_get(url, params, opt, &block)
241
+ do_get(url, params, &block)
243
242
  end
244
243
  end
245
244
 
246
245
  # @param [String] url
247
246
  # @param [Hash] params
248
- # @param [Hash] opt
249
247
  # @yield [response]
250
- def do_get(url, params=nil, opt={})
248
+ def do_get(url, params=nil, &block)
251
249
  client, header = new_client
252
250
  client.send_timeout = @send_timeout
253
251
  client.receive_timeout = @read_timeout
@@ -270,49 +268,23 @@ private
270
268
  # for both exceptions and 500+ errors retrying is enabled by default.
271
269
  # The total number of retries cumulatively should not exceed 10 minutes / 600 seconds
272
270
  response = nil
273
- etag = nil
274
- current_total_chunk_size = 0
275
- body = String.new unless block_given?
276
271
  begin # this block is to allow retry (redo) in the begin part of the begin-rescue block
277
272
  begin
278
- if etag
279
- header['If-Range'] = etag
280
- header['Range'] = "bytes=#{current_total_chunk_size}-"
281
- else
282
- etag = nil
273
+ if block
283
274
  current_total_chunk_size = 0
284
- body.clear if body
285
- end
286
-
287
- if block_given?
288
275
  response = client.get(target, params, header) {|res, chunk|
289
- current_total_chunk_size += chunk.bytesize if res.status == 200
290
- yield res, chunk, current_total_chunk_size
276
+ current_total_chunk_size += chunk.bytesize
277
+ block.call(res, chunk, current_total_chunk_size)
291
278
  }
279
+
280
+ # XXX ext/openssl raises EOFError in case where underlying connection causes an error,
281
+ # and msgpack-ruby that used in block handles it as an end of stream == no exception.
282
+ # Therefor, check content size.
283
+ validate_content_length!(response, current_total_chunk_size) if @ssl
292
284
  else
293
285
  response = client.get(target, params, header)
294
- if response.status == 200
295
- current_total_chunk_size += response.body.bytesize
296
- body << response.body
297
- end
298
- end
299
286
 
300
- # XXX ext/openssl raises EOFError in case where underlying connection causes an error,
301
- # but httpclient ignores it. Therefor, check content size.
302
- # https://github.com/nahi/httpclient/issues/296
303
- if expected_size = response.header['Content-Range'].first
304
- expected_size = expected_size[/\d+$/]
305
- else
306
- expected_size = response.header['Content-Length'].first
307
- end
308
- if expected_size
309
- expected_size = expected_size.to_i
310
- if expected_size != current_total_chunk_size
311
- if expected_size < current_total_chunk_size
312
- etag = false
313
- end
314
- raise IncompleteError, "#{expected_size} bytes expected, but got #{current_total_chunk_size} bytes"
315
- end
287
+ validate_content_length!(response, response.body.bytesize) if @ssl
316
288
  end
317
289
 
318
290
  status = response.code
@@ -325,9 +297,7 @@ private
325
297
  redo # restart from beginning of do-while loop
326
298
  end
327
299
  rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError, OpenSSL::SSL::SSLError, SocketError, IncompleteError => e
328
- if opt[:resume]
329
- etag = response.header['ETag'].first if etag != false
330
- elsif block_given?
300
+ if block_given?
331
301
  raise e
332
302
  end
333
303
  $stderr.print "#{e.class}: #{e.message}. "
@@ -353,24 +323,29 @@ private
353
323
  puts "DEBUG: body: " + response.body.to_s
354
324
  end
355
325
 
356
- body = inflate_body(response, body) unless block_given?
326
+ body = block ? response.body : inflate_body(response)
357
327
 
358
328
  return [response.code.to_s, body, response]
359
329
  end
360
330
 
361
- def inflate_body(response, body=response.body)
362
- return body if (ce = response.header['Content-Encoding']).empty?
331
+ def validate_content_length!(response, body_size)
332
+ content_length = response.header['Content-Length'].first
333
+ raise IncompleteError if @ssl && content_length && content_length.to_i != body_size
334
+ end
335
+
336
+ def inflate_body(response)
337
+ return response.body if (ce = response.header['Content-Encoding']).empty?
363
338
 
364
339
  if ce.include?('gzip')
365
340
  infl = Zlib::Inflate.new(Zlib::MAX_WBITS + 16)
366
341
  begin
367
- infl.inflate(body)
342
+ infl.inflate(response.body)
368
343
  ensure
369
344
  infl.close
370
345
  end
371
346
  else
372
347
  # NOTE maybe for content-encoding is msgpack.gz ?
373
- Zlib::Inflate.inflate(body)
348
+ Zlib::Inflate.inflate(response.body)
374
349
  end
375
350
  end
376
351
 
@@ -628,7 +603,7 @@ private
628
603
  error['stacktrace'] = js['stacktrace']
629
604
  end
630
605
  rescue JSON::ParserError
631
- error['message'] = res.body
606
+ error['message'] = res.body[0,1000].dump
632
607
  end
633
608
 
634
609
  error
@@ -115,14 +115,13 @@ module Job
115
115
  # @param [String] job_id
116
116
  # @return [Array]
117
117
  def job_result(job_id)
118
- code, body, res = get("/v3/job/result/#{e job_id}", {'format'=>'msgpack'}, {:resume => true})
119
- if code != "200"
120
- raise_error("Get job result failed", res)
121
- end
122
118
  result = []
123
- MessagePack::Unpacker.new.feed_each(body) {|row|
124
- result << row
125
- }
119
+ unpacker = MessagePack::Unpacker.new
120
+ job_result_download(job_id) do |chunk|
121
+ unpacker.feed_each(chunk) do |row|
122
+ result << row
123
+ end
124
+ end
126
125
  return result
127
126
  end
128
127
 
@@ -133,24 +132,17 @@ module Job
133
132
  # @param [IO] io
134
133
  # @param [Proc] block
135
134
  # @return [nil, String]
136
- def job_result_format(job_id, format, io=nil, &block)
135
+ def job_result_format(job_id, format, io=nil)
137
136
  if io
138
- infl = nil
139
- code, body, res = get("/v3/job/result/#{e job_id}", {'format'=>format}, {:resume => true}) {|res, chunk, current_total_chunk_size|
140
- if res.code != 200
141
- raise_error("Get job result failed", res)
142
- end
143
-
144
- infl ||= create_inflalte_or_null_inflate(res)
145
-
146
- io.write infl.inflate(chunk)
147
- block.call(current_total_chunk_size) if block_given?
148
- }
137
+ job_result_download(job_id, format) do |chunk, total|
138
+ io.write chunk
139
+ yield total if block_given?
140
+ end
149
141
  nil
150
142
  else
151
- code, body, res = get("/v3/job/result/#{e job_id}", {'format'=>format}, {:resume => true})
152
- if code != "200"
153
- raise_error("Get job result failed", res)
143
+ body = String.new
144
+ job_result_download(job_id, format) do |chunk|
145
+ body << chunk
154
146
  end
155
147
  body
156
148
  end
@@ -163,22 +155,11 @@ module Job
163
155
  # @return [nil]
164
156
  def job_result_each(job_id, &block)
165
157
  upkr = MessagePack::Unpacker.new
166
- infl = nil
167
-
168
- get("/v3/job/result/#{e job_id}", {'format'=>'msgpack'}, {:resume => true}) {|res, chunk, current_total_chunk_size|
169
- if res.code != 200
170
- raise_error("Get job result failed", res)
171
- end
172
-
173
- # default to decompressing the response since format is fixed to 'msgpack'
174
- infl ||= create_inflate(res)
175
-
176
- inflated_fragment = infl.inflate(chunk)
177
- upkr.feed_each(inflated_fragment, &block)
178
- }
158
+ # default to decompressing the response since format is fixed to 'msgpack'
159
+ job_result_download(job_id) do |chunk|
160
+ upkr.feed_each(chunk, &block)
161
+ end
179
162
  nil
180
- ensure
181
- infl.close if infl
182
163
  end
183
164
 
184
165
  # block is optional and must accept 1 argument
@@ -186,50 +167,30 @@ module Job
186
167
  # @param [String] job_id
187
168
  # @param [Proc] block
188
169
  # @return [nil]
189
- def job_result_each_with_compr_size(job_id, &block)
170
+ def job_result_each_with_compr_size(job_id)
190
171
  upkr = MessagePack::Unpacker.new
191
- infl = nil
192
-
193
- get("/v3/job/result/#{e job_id}", {'format'=>'msgpack'}, {:resume => true}) {|res, chunk, current_total_chunk_size|
194
- if res.code != 200
195
- raise_error("Get job result failed", res)
196
- end
197
-
198
- # default to decompressing the response since format is fixed to 'msgpack'
199
- infl ||= create_inflate(res)
200
-
201
- inflated_fragment = infl.inflate(chunk)
202
- upkr.feed_each(inflated_fragment) {|unpacked|
203
- block.call(unpacked, current_total_chunk_size) if block_given?
172
+ # default to decompressing the response since format is fixed to 'msgpack'
173
+ job_result_download(job_id) do |chunk, total|
174
+ upkr.feed_each(chunk) {|unpacked|
175
+ yield unpacked, total if block_given?
204
176
  }
205
- }
177
+ end
206
178
  nil
207
- ensure
208
- infl.close if infl
209
179
  end
210
180
 
211
181
  # @param [String] job_id
212
182
  # @param [String] format
213
183
  # @return [String]
214
- def job_result_raw(job_id, format, io = nil, &block)
215
- body = nil
216
-
217
- get("/v3/job/result/#{e job_id}", {'format'=>format}, {:resume => true}) {|res, chunk, current_total_chunk_size|
218
- unless res.ok?
219
- raise_error("Get job result failed", res)
220
- end
221
-
184
+ def job_result_raw(job_id, format, io = nil)
185
+ body = io ? nil : String.new
186
+ job_result_download(job_id, format, false) do |chunk, total|
222
187
  if io
223
188
  io.write(chunk)
224
- block.call(current_total_chunk_size) if block_given?
189
+ yield total if block_given?
225
190
  else
226
- if body
227
- body += chunk
228
- else
229
- body = chunk
230
- end
191
+ body << chunk
231
192
  end
232
- }
193
+ end
233
194
  body
234
195
  end
235
196
 
@@ -287,6 +248,117 @@ module Job
287
248
 
288
249
  private
289
250
 
251
+ def validate_content_length_with_range(response, current_total_chunk_size)
252
+ if expected_size = response.header['Content-Range'][0]
253
+ expected_size = expected_size[/\d+$/].to_i
254
+ elsif expected_size = response.header['Content-Length'][0]
255
+ expected_size = expected_size.to_i
256
+ end
257
+
258
+ if expected_size.nil?
259
+ elsif current_total_chunk_size < expected_size
260
+ # too small
261
+ # NOTE:
262
+ # ext/openssl raises EOFError in case where underlying connection
263
+ # causes an error, but httpclient ignores it.
264
+ # https://github.com/nahi/httpclient/blob/v3.2.8/lib/httpclient/session.rb#L1003
265
+ raise EOFError, 'httpclient IncompleteError'
266
+ elsif current_total_chunk_size > expected_size
267
+ # too large
268
+ raise_error("Get job result failed", response)
269
+ end
270
+ end
271
+
272
+ def job_result_download(job_id, format='msgpack', autodecode=true)
273
+ client, header = new_client
274
+ client.send_timeout = @send_timeout
275
+ client.receive_timeout = @read_timeout
276
+ header['Accept-Encoding'] = 'deflate, gzip'
277
+
278
+ url = build_endpoint("/v3/job/result/#{e job_id}", @host)
279
+ params = {'format' => format}
280
+
281
+ unless ENV['TD_CLIENT_DEBUG'].nil?
282
+ puts "DEBUG: REST GET call:"
283
+ puts "DEBUG: header: " + header.to_s
284
+ puts "DEBUG: url: " + url.to_s
285
+ puts "DEBUG: params: " + params.to_s
286
+ end
287
+
288
+ # up to 7 retries with exponential (base 2) back-off starting at 'retry_delay'
289
+ retry_delay = @retry_delay
290
+ cumul_retry_delay = 0
291
+ current_total_chunk_size = 0
292
+ infl = nil
293
+ begin # LOOP of Network/Server errors
294
+ response = nil
295
+ client.get(url, params, header) do |res, chunk|
296
+ unless response
297
+ case res.status
298
+ when 200
299
+ if current_total_chunk_size != 0
300
+ # try to resume but the server returns 200
301
+ raise_error("Get job result failed", res)
302
+ end
303
+ when 206 # resuming
304
+ else
305
+ if res.status/100 == 5 && cumul_retry_delay < @max_cumul_retry_delay
306
+ $stderr.puts "Error #{res.status}: #{get_error(res)}. Retrying after #{retry_delay} seconds..."
307
+ sleep retry_delay
308
+ cumul_retry_delay += retry_delay
309
+ retry_delay *= 2
310
+ redo
311
+ end
312
+ raise_error("Get job result failed", res)
313
+ end
314
+ if infl.nil? && autodecode
315
+ case res.header['Content-Encoding'][0].to_s.downcase
316
+ when 'gzip'
317
+ infl = Zlib::Inflate.new(Zlib::MAX_WBITS + 16)
318
+ when 'deflate'
319
+ infl = Zlib::Inflate.new
320
+ end
321
+ end
322
+ end
323
+ response = res
324
+ current_total_chunk_size += chunk.bytesize
325
+ chunk = infl.inflate(chunk) if infl
326
+ yield chunk, current_total_chunk_size
327
+ end
328
+
329
+ # completed?
330
+ validate_content_length_with_range(response, current_total_chunk_size)
331
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError, OpenSSL::SSL::SSLError, SocketError => e
332
+ if response # at least a chunk is downloaded
333
+ if etag = response.header['ETag'][0]
334
+ header['If-Range'] = etag
335
+ header['Range'] = "bytes=#{current_total_chunk_size}-"
336
+ end
337
+ end
338
+
339
+ $stderr.print "#{e.class}: #{e.message}. "
340
+ if cumul_retry_delay < @max_cumul_retry_delay
341
+ $stderr.puts "Retrying after #{retry_delay} seconds..."
342
+ sleep retry_delay
343
+ cumul_retry_delay += retry_delay
344
+ retry_delay *= 2
345
+ retry
346
+ end
347
+ raise
348
+ end
349
+
350
+ unless ENV['TD_CLIENT_DEBUG'].nil?
351
+ puts "DEBUG: REST GET response:"
352
+ puts "DEBUG: header: " + response.header.to_s
353
+ puts "DEBUG: status: " + response.code.to_s
354
+ puts "DEBUG: body: " + response.body.to_s
355
+ end
356
+
357
+ nil
358
+ ensure
359
+ infl.close if infl
360
+ end
361
+
290
362
  class NullInflate
291
363
  def inflate(chunk)
292
364
  chunk
@@ -1,5 +1,5 @@
1
1
  module TreasureData
2
2
  class Client
3
- VERSION = '0.8.80'
3
+ VERSION = '0.8.81'
4
4
  end
5
5
  end
@@ -178,7 +178,7 @@ describe API do
178
178
 
179
179
  let(:api) { API.new(nil, endpoint: endpoint) }
180
180
  let :packed do
181
- s = StringIO.new
181
+ s = StringIO.new(String.new)
182
182
  Zlib::GzipWriter.wrap(s) do |f|
183
183
  f << ['hello', 'world'].to_json
184
184
  end
@@ -195,7 +195,7 @@ describe API do
195
195
  end
196
196
 
197
197
  subject (:get_api_call) {
198
- api.job_result_format(12345, 'json', StringIO.new)
198
+ api.job_result_format(12345, 'json', StringIO.new(String.new))
199
199
  }
200
200
 
201
201
  context 'without ssl' do
@@ -166,7 +166,7 @@ describe 'Job API' do
166
166
 
167
167
  describe 'job_result' do
168
168
  let :packed do
169
- s = StringIO.new
169
+ s = StringIO.new(String.new)
170
170
  pk = MessagePack::Packer.new(s)
171
171
  pk.write('hello')
172
172
  pk.write('world')
@@ -181,6 +181,74 @@ describe 'Job API' do
181
181
  expect(api.job_result(12345)).to eq(['hello', 'world'])
182
182
  end
183
183
 
184
+ it '200->200 cannot resume' do
185
+ sz = packed.bytesize / 3
186
+ stub_api_request(:get, '/v3/job/result/12345').
187
+ with(:query => {'format' => 'msgpack'}).
188
+ to_return(
189
+ :headers => {
190
+ 'Content-Length' => packed.bytesize,
191
+ 'Etag' => '"abcdefghijklmn"',
192
+ },
193
+ :body => packed[0, sz]
194
+ )
195
+ stub_api_request(:get, '/v3/job/result/12345').
196
+ with(
197
+ :headers => {
198
+ 'If-Range' => '"abcdefghijklmn"',
199
+ 'Range' => "bytes=#{sz}-",
200
+ },
201
+ :query => {'format' => 'msgpack'}
202
+ ).
203
+ to_return(
204
+ :status => 200,
205
+ :headers => {
206
+ 'Content-Length' => packed.bytesize - sz,
207
+ 'Content-Range' => "bytes #{sz}-#{packed.bytesize-1}/#{packed.bytesize}",
208
+ 'Etag' => '"abcdefghijklmn"',
209
+ },
210
+ :body => packed
211
+ )
212
+ expect(api).to receive(:sleep).once
213
+ expect($stderr).to receive(:print)
214
+ expect($stderr).to receive(:puts)
215
+ expect{api.job_result(12345)}.to raise_error(TreasureData::APIError)
216
+ end
217
+
218
+ it '200->403 cannot resume' do
219
+ sz = packed.bytesize / 3
220
+ stub_api_request(:get, '/v3/job/result/12345').
221
+ with(:query => {'format' => 'msgpack'}).
222
+ to_return(
223
+ :headers => {
224
+ 'Content-Length' => packed.bytesize,
225
+ 'Etag' => '"abcdefghijklmn"',
226
+ },
227
+ :body => packed[0, sz]
228
+ )
229
+ stub_api_request(:get, '/v3/job/result/12345').
230
+ with(
231
+ :headers => {
232
+ 'If-Range' => '"abcdefghijklmn"',
233
+ 'Range' => "bytes=#{sz}-",
234
+ },
235
+ :query => {'format' => 'msgpack'}
236
+ ).
237
+ to_return(
238
+ :status => 403,
239
+ :headers => {
240
+ 'Content-Length' => packed.bytesize - sz,
241
+ 'Content-Range' => "bytes #{sz}-#{packed.bytesize-1}/#{packed.bytesize}",
242
+ 'Etag' => '"abcdefghijklmn"',
243
+ },
244
+ :body => packed
245
+ )
246
+ expect(api).to receive(:sleep).once
247
+ expect($stderr).to receive(:print)
248
+ expect($stderr).to receive(:puts)
249
+ expect{api.job_result(12345)}.to raise_error(TreasureData::APIError)
250
+ end
251
+
184
252
  it 'can resume' do
185
253
  sz = packed.bytesize / 3
186
254
  stub_api_request(:get, '/v3/job/result/12345').
@@ -201,6 +269,7 @@ describe 'Job API' do
201
269
  :query => {'format' => 'msgpack'}
202
270
  ).
203
271
  to_return(
272
+ :status => 206,
204
273
  :headers => {
205
274
  'Content-Length' => packed.bytesize - sz,
206
275
  'Content-Range' => "bytes #{sz}-#{packed.bytesize-1}/#{packed.bytesize}",
@@ -213,11 +282,63 @@ describe 'Job API' do
213
282
  expect($stderr).to receive(:puts)
214
283
  expect(api.job_result(12345)).to eq ['hello', 'world']
215
284
  end
285
+
286
+ it '200->500->206 can resume' do
287
+ sz = packed.bytesize / 3
288
+ stub_api_request(:get, '/v3/job/result/12345').
289
+ with(:query => {'format' => 'msgpack'}).
290
+ to_return(
291
+ :headers => {
292
+ 'Content-Length' => packed.bytesize,
293
+ 'Etag' => '"abcdefghijklmn"',
294
+ },
295
+ :body => packed[0, sz]
296
+ )
297
+ stub_api_request(:get, '/v3/job/result/12345').
298
+ with(
299
+ :headers => {
300
+ 'If-Range' => '"abcdefghijklmn"',
301
+ 'Range' => "bytes=#{sz}-",
302
+ },
303
+ :query => {'format' => 'msgpack'}
304
+ ).
305
+ to_return(
306
+ :status => 500,
307
+ :headers => {
308
+ 'Content-Length' => packed.bytesize - sz,
309
+ 'Content-Range' => "bytes #{sz}-#{packed.bytesize-1}/#{packed.bytesize}",
310
+ 'Etag' => '"abcdefghijklmn"',
311
+ },
312
+ :body => packed
313
+ )
314
+ stub_api_request(:get, '/v3/job/result/12345').
315
+ with(
316
+ :headers => {
317
+ 'If-Range' => '"abcdefghijklmn"',
318
+ 'Range' => "bytes=#{sz}-",
319
+ },
320
+ :query => {'format' => 'msgpack'}
321
+ ).
322
+ to_return(
323
+ :status => 206,
324
+ :headers => {
325
+ 'Content-Length' => packed.bytesize - sz,
326
+ 'Content-Range' => "bytes #{sz}-#{packed.bytesize-1}/#{packed.bytesize}",
327
+ 'Etag' => '"abcdefghijklmn"',
328
+ },
329
+ :body => packed[sz, packed.bytesize - sz]
330
+ )
331
+ expect(api).to receive(:sleep).once
332
+ expect($stderr).to receive(:print)
333
+ expect($stderr).to receive(:puts)
334
+ expect(api.job_result(12345)).to eq ['hello', 'world']
335
+ end
336
+
216
337
  end
217
338
 
218
339
  describe 'job_result_format' do
219
340
  let :packed do
220
- s = StringIO.new
341
+ s = StringIO.new(String.new)
221
342
  Zlib::GzipWriter.wrap(s) do |f|
222
343
  f << ['hello', 'world'].to_json
223
344
  end
@@ -286,6 +407,7 @@ describe 'Job API' do
286
407
  :query => {'format' => 'json'}
287
408
  ).
288
409
  to_return(
410
+ :status => 206,
289
411
  :headers => {
290
412
  'Content-Encoding' => 'gzip',
291
413
  'Content-Length' => packed.bytesize-sz,
@@ -312,7 +434,7 @@ describe 'Job API' do
312
434
 
313
435
  describe 'job_result_each' do
314
436
  let :packed do
315
- s = StringIO.new
437
+ s = StringIO.new(String.new)
316
438
  Zlib::GzipWriter.wrap(s) do |f|
317
439
  pk = MessagePack::Packer.new(f)
318
440
  pk.write('hello')
@@ -357,6 +479,7 @@ describe 'Job API' do
357
479
  :query => {'format' => 'msgpack'}
358
480
  ).
359
481
  to_return(
482
+ :status => 206,
360
483
  :headers => {
361
484
  'Content-Length' => packed.bytesize-sz,
362
485
  'Content-Range' => "bytes #{sz}-#{packed.bytesize-1}/#{packed.bytesize}",
@@ -386,7 +509,7 @@ describe 'Job API' do
386
509
  # pk.flush
387
510
  # end
388
511
  # s.string
389
- "\u001F\x8B\b\u0000#\xA1\x93T\u0000\u0003[\x9A\x91\x9A\x93\x93\xBF\xB4<\xBF('\u0005\u0000e 0\xB3\f\u0000\u0000\u0000"
512
+ "\x1F\x8B\b\x00#\xA1\x93T\x00\x03[\x9A\x91\x9A\x93\x93\xBF\xB4<\xBF('\x05\x00e 0\xB3\f\x00\x00\x00".force_encoding(Encoding::ASCII_8BIT)
390
513
  end
391
514
 
392
515
  it 'yields job result for each row with progress' do
@@ -424,6 +547,7 @@ describe 'Job API' do
424
547
  :query => {'format' => 'msgpack'}
425
548
  ).
426
549
  to_return(
550
+ :status => 206,
427
551
  :headers => {
428
552
  'Content-Length' => packed.bytesize - sz,
429
553
  'Content-Range' => "bytes #{sz}-#{packed.bytesize-1}/#{packed.bytesize}",
@@ -498,6 +622,7 @@ describe 'Job API' do
498
622
  :query => {'format' => 'msgpack.gz'}
499
623
  ).
500
624
  to_return(
625
+ :status => 206,
501
626
  :headers => {
502
627
  'Content-Length' => packed.bytesize - sz,
503
628
  'Content-Range' => "bytes #{sz}-#{packed.bytesize-1}/#{packed.bytesize}",
@@ -508,7 +633,7 @@ describe 'Job API' do
508
633
  expect(api).to receive(:sleep).once
509
634
  expect($stderr).to receive(:print)
510
635
  expect($stderr).to receive(:puts)
511
- sio = StringIO.new(''.force_encoding(Encoding::ASCII_8BIT))
636
+ sio = StringIO.new(String.new)
512
637
  api.job_result_raw(12345, 'msgpack.gz', sio)
513
638
  expect(sio.string).to eq(packed)
514
639
  end
@@ -521,4 +646,185 @@ describe 'Job API' do
521
646
  expect(api.kill(12345)).to eq('status')
522
647
  end
523
648
  end
649
+
650
+ describe 'job_result_download' do
651
+ let (:data){ [[1, 'hello', nil], [2, 'world', true], [3, '!', false]] }
652
+ let :formatted do
653
+ case format
654
+ when 'json'
655
+ data.map{|a| JSON(a) }.join("\n")
656
+ when 'msgpack'
657
+ pk = MessagePack::Packer.new
658
+ data.each{|x| pk.write(x) }
659
+ pk.to_str
660
+ else
661
+ raise
662
+ end
663
+ end
664
+ let :gziped do
665
+ s = StringIO.new(String.new)
666
+ Zlib::GzipWriter.wrap(s) do |f|
667
+ f.write formatted
668
+ end
669
+ s.string
670
+ end
671
+ let :deflated do
672
+ Zlib.deflate(formatted)
673
+ end
674
+ subject do
675
+ str = ''
676
+ api.__send__(:job_result_download, 12345, format){|x| str << x }
677
+ str
678
+ end
679
+ context '200' do
680
+ before do
681
+ sz = packed.bytesize / 3
682
+ stub_api_request(:get, '/v3/job/result/12345').
683
+ with(:query => {'format' => format}).
684
+ to_return(
685
+ :headers => {
686
+ 'Content-Encoding' => content_encoding,
687
+ 'Content-Length' => packed.bytesize,
688
+ 'Etag' => '"abcdefghijklmn"',
689
+ },
690
+ :body => packed
691
+ )
692
+ expect(api).not_to receive(:sleep)
693
+ expect($stderr).not_to receive(:print)
694
+ expect($stderr).not_to receive(:puts)
695
+ end
696
+ context 'Content-Encoding: gzip' do
697
+ let (:content_encoding){ 'gzip' }
698
+ let (:packed){ gziped }
699
+ context 'msgpack' do
700
+ let (:format){ 'msgpack' }
701
+ it { is_expected.to eq formatted }
702
+ end
703
+ context 'json' do
704
+ let (:format){ 'json' }
705
+ it { is_expected.to eq formatted }
706
+ end
707
+ end
708
+ context 'Content-Encoding: deflate' do
709
+ let (:content_encoding){ 'deflate' }
710
+ let (:packed){ deflated }
711
+ context 'msgpack' do
712
+ let (:format){ 'msgpack' }
713
+ it { is_expected.to eq formatted }
714
+ end
715
+ context 'json' do
716
+ let (:format){ 'json' }
717
+ it { is_expected.to eq formatted }
718
+ end
719
+ end
720
+ end
721
+
722
+ context '200 -> 206' do
723
+ before do
724
+ sz = packed.bytesize / 3
725
+ stub_api_request(:get, '/v3/job/result/12345').
726
+ with(:query => {'format' => format}).
727
+ to_return(
728
+ :headers => {
729
+ 'Content-Encoding' => content_encoding,
730
+ 'Content-Length' => packed.bytesize,
731
+ 'Etag' => '"abcdefghijklmn"',
732
+ },
733
+ :body => packed[0, sz]
734
+ )
735
+ stub_api_request(:get, '/v3/job/result/12345').
736
+ with(
737
+ :headers => {
738
+ 'If-Range' => '"abcdefghijklmn"',
739
+ 'Range' => "bytes=#{sz}-",
740
+ },
741
+ :query => {'format' => format}
742
+ ).
743
+ to_return(
744
+ :status => 206,
745
+ :headers => {
746
+ 'Content-Encoding' => content_encoding,
747
+ 'Content-Length' => packed.bytesize - sz,
748
+ 'Content-Range' => "bytes #{sz}-#{packed.bytesize-1}/#{packed.bytesize}",
749
+ 'Etag' => '"abcdefghijklmn"',
750
+ },
751
+ :body => packed[sz, packed.bytesize - sz]
752
+ )
753
+ expect(api).to receive(:sleep).once
754
+ allow($stderr).to receive(:print)
755
+ allow($stderr).to receive(:puts)
756
+ end
757
+ context 'Content-Encoding: gzip' do
758
+ let (:content_encoding){ 'gzip' }
759
+ let (:packed){ gziped }
760
+ context 'msgpack' do
761
+ let (:format){ 'msgpack' }
762
+ it { is_expected.to eq formatted }
763
+ end
764
+ context 'json' do
765
+ let (:format){ 'json' }
766
+ it { is_expected.to eq formatted }
767
+ end
768
+ end
769
+ context 'Content-Encoding: deflate' do
770
+ let (:content_encoding){ 'deflate' }
771
+ let (:packed){ deflated }
772
+ context 'msgpack' do
773
+ let (:format){ 'msgpack' }
774
+ it { is_expected.to eq formatted }
775
+ end
776
+ context 'json' do
777
+ let (:format){ 'json' }
778
+ it { is_expected.to eq formatted }
779
+ end
780
+ end
781
+ end
782
+
783
+ context 'without autodecode' do
784
+ before do
785
+ sz = packed.bytesize / 3
786
+ stub_api_request(:get, '/v3/job/result/12345').
787
+ with(:query => {'format' => format}).
788
+ to_return(
789
+ :headers => {
790
+ 'Content-Length' => packed.bytesize,
791
+ 'Etag' => '"abcdefghijklmn"',
792
+ },
793
+ :body => packed
794
+ )
795
+ expect(api).not_to receive(:sleep)
796
+ expect($stderr).not_to receive(:print)
797
+ expect($stderr).not_to receive(:puts)
798
+ end
799
+ subject do
800
+ str = ''
801
+ api.__send__(:job_result_download, 12345, format, false){|x| str << x }
802
+ str
803
+ end
804
+ context 'Content-Encoding: gzip' do
805
+ let (:content_encoding){ 'gzip' }
806
+ let (:packed){ gziped }
807
+ context 'msgpack' do
808
+ let (:format){ 'msgpack' }
809
+ it { is_expected.to eq packed }
810
+ end
811
+ context 'json' do
812
+ let (:format){ 'json' }
813
+ it { is_expected.to eq packed }
814
+ end
815
+ end
816
+ context 'Content-Encoding: deflate' do
817
+ let (:content_encoding){ 'deflate' }
818
+ let (:packed){ deflated }
819
+ context 'msgpack' do
820
+ let (:format){ 'msgpack' }
821
+ it { is_expected.to eq packed }
822
+ end
823
+ context 'json' do
824
+ let (:format){ 'json' }
825
+ it { is_expected.to eq packed }
826
+ end
827
+ end
828
+ end
829
+ end
524
830
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: td-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.80
4
+ version: 0.8.81
5
5
  platform: ruby
6
6
  authors:
7
7
  - Treasure Data, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-30 00:00:00.000000000 Z
11
+ date: 2016-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack