td-client 0.8.80 → 0.8.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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