td-client 0.9.0dev2 → 1.0.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/td/client.rb +16 -8
  3. data/lib/td/client/api.rb +66 -47
  4. data/lib/td/client/api/bulk_import.rb +1 -2
  5. data/lib/td/client/api/bulk_load.rb +3 -3
  6. data/lib/td/client/api/export.rb +12 -0
  7. data/lib/td/client/api/import.rb +3 -2
  8. data/lib/td/client/api/job.rb +146 -71
  9. data/lib/td/client/api/schedule.rb +1 -1
  10. data/lib/td/client/api_error.rb +5 -0
  11. data/lib/td/client/model.rb +92 -28
  12. data/lib/td/client/version.rb +1 -1
  13. data/spec/spec_helper.rb +5 -5
  14. data/spec/td/client/account_api_spec.rb +5 -5
  15. data/spec/td/client/api_error_spec.rb +77 -0
  16. data/spec/td/client/api_spec.rb +76 -52
  17. data/spec/td/client/api_ssl_connection_spec.rb +1 -1
  18. data/spec/td/client/bulk_import_spec.rb +28 -29
  19. data/spec/td/client/bulk_load_spec.rb +60 -35
  20. data/spec/td/client/db_api_spec.rb +1 -1
  21. data/spec/td/client/export_api_spec.rb +11 -1
  22. data/spec/td/client/import_api_spec.rb +85 -10
  23. data/spec/td/client/job_api_spec.rb +568 -61
  24. data/spec/td/client/model_job_spec.rb +27 -10
  25. data/spec/td/client/model_schedule_spec.rb +2 -2
  26. data/spec/td/client/model_schema_spec.rb +134 -0
  27. data/spec/td/client/partial_delete_api_spec.rb +1 -1
  28. data/spec/td/client/result_api_spec.rb +3 -3
  29. data/spec/td/client/sched_api_spec.rb +12 -4
  30. data/spec/td/client/server_status_api_spec.rb +2 -2
  31. data/spec/td/client/spec_resources.rb +1 -0
  32. data/spec/td/client/table_api_spec.rb +14 -14
  33. data/spec/td/client/user_api_spec.rb +12 -12
  34. data/spec/td/client_sched_spec.rb +31 -6
  35. data/spec/td/client_spec.rb +1 -0
  36. metadata +42 -81
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a408ad7fd0f4eec142b6cc52fcdfbf3bc14980fd
4
- data.tar.gz: 17891d7c92097c10818735ec7be9db47c2731b88
3
+ metadata.gz: 140238f578d4cee66cc4ef09c155375719ad1c9e
4
+ data.tar.gz: cc7df46122c3186fc80c99ca1911aff0bcf8c994
5
5
  SHA512:
6
- metadata.gz: 58874a549e403387c112f21b14d236218eff0a446057f705cd398eef118603a82bee3a75f85a86ed990c383bdd97b2f7994d44f3df80d271f57806dd6602ef20
7
- data.tar.gz: 498c15e6a4d8ce9f7edcd799107aa0c9ec4cd9a4aee435fc0621ab906ef57acf075f5bf01b841f43064883d180571a4edd01effed53ceec84577d71548f660fa
6
+ metadata.gz: bdd1ba7cfb27afbc22a07845c06cf420afa1d9d40411137573ec2406ceeb923838a65be2e3ce7dbdc5120126a3386aa6234dca79426e9341d57e3a82ebe559d3
7
+ data.tar.gz: 8045b7325a7492b35db7962ed36d3bb6895d3307990c1e43d7523e0832b9a33b7d2ac4d792a0b8284755342726c231ea994f83d0829a72a19a3b7a891789a738
@@ -182,10 +182,10 @@ class Client
182
182
  results = @api.list_jobs(from, to, status, conditions)
183
183
  results.map {|job_id, type, status, query, start_at, end_at, cpu_time,
184
184
  result_size, result_url, priority, retry_limit, org, db,
185
- duration|
185
+ duration, num_records|
186
186
  Job.new(self, job_id, type, query, status, nil, nil, start_at, end_at, cpu_time,
187
187
  result_size, nil, result_url, nil, priority, retry_limit, org, db,
188
- duration)
188
+ duration, num_records)
189
189
  }
190
190
  end
191
191
 
@@ -194,9 +194,9 @@ class Client
194
194
  def job(job_id)
195
195
  job_id = job_id.to_s
196
196
  type, query, status, url, debug, start_at, end_at, cpu_time,
197
- result_size, result_url, hive_result_schema, priority, retry_limit, org, db = @api.show_job(job_id)
197
+ result_size, result_url, hive_result_schema, priority, retry_limit, org, db, duration, num_records = @api.show_job(job_id)
198
198
  Job.new(self, job_id, type, query, status, url, debug, start_at, end_at, cpu_time,
199
- result_size, nil, result_url, hive_result_schema, priority, retry_limit, org, db)
199
+ result_size, nil, result_url, hive_result_schema, priority, retry_limit, org, db, duration, num_records)
200
200
  end
201
201
 
202
202
  # @param [String] job_id
@@ -254,6 +254,14 @@ class Client
254
254
  Job.new(self, job_id, :export, nil)
255
255
  end
256
256
 
257
+ # @param [String] target_job_id
258
+ # @param [Hash] opts
259
+ # @return [Job]
260
+ def result_export(target_job_id, opts={})
261
+ job_id = @api.result_export(target_job_id, opts)
262
+ Job.new(self, job_id, :result_export, nil)
263
+ end
264
+
257
265
  # @param [String] db_name
258
266
  # @param [String] table_name
259
267
  # @param [Fixnum] to
@@ -356,7 +364,7 @@ class Client
356
364
  raise ArgumentError, "'cron' option is required" unless opts[:cron] || opts['cron']
357
365
  raise ArgumentError, "'query' option is required" unless opts[:query] || opts['query']
358
366
  start = @api.create_schedule(name, opts)
359
- return Time.parse(start)
367
+ return start && Time.parse(start)
360
368
  end
361
369
 
362
370
  # @param [String] name
@@ -574,9 +582,9 @@ class Client
574
582
  @api.bulk_load_show(name)
575
583
  end
576
584
 
577
- # name: String, job: BulkLoad -> BulkLoad
578
- def bulk_load_update(name, job)
579
- @api.bulk_load_update(name, job)
585
+ # name: String, settings: Hash -> BulkLoad
586
+ def bulk_load_update(name, settings)
587
+ @api.bulk_load_update(name, settings)
580
588
  end
581
589
 
582
590
  # name: String -> BulkLoad
@@ -36,16 +36,19 @@ class API
36
36
  include API::Table
37
37
  include API::User
38
38
 
39
- DEFAULT_ENDPOINT = 'api.treasure-data.com'
40
- DEFAULT_IMPORT_ENDPOINT = 'api-import.treasure-data.com'
39
+ DEFAULT_ENDPOINT = 'api.treasuredata.com'
40
+ DEFAULT_IMPORT_ENDPOINT = 'api-import.treasuredata.com'
41
41
 
42
- NEW_DEFAULT_ENDPOINT = 'api.treasuredata.com'
43
- NEW_DEFAULT_IMPORT_ENDPOINT = 'api-import.treasuredata.com'
42
+ # Deprecated. Use DEFAULT_ENDPOINT and DEFAULT_IMPORT_ENDPOINT instead
43
+ NEW_DEFAULT_ENDPOINT = DEFAULT_ENDPOINT
44
+ NEW_DEFAULT_IMPORT_ENDPOINT = DEFAULT_IMPORT_ENDPOINT
45
+ OLD_ENDPOINT = 'api.treasure-data.com'
44
46
 
45
47
  class IncompleteError < APIError; end
46
48
 
47
49
  # @param [String] apikey
48
50
  # @param [Hash] opts
51
+ # for backward compatibility
49
52
  def initialize(apikey, opts={})
50
53
  require 'json'
51
54
  require 'time'
@@ -92,12 +95,18 @@ class API
92
95
  # generic URI
93
96
  @host, @port = endpoint.split(':', 2)
94
97
  @port = @port.to_i
95
- if opts[:ssl]
96
- @port = 443 if @port == 0
97
- @ssl = true
98
- else
98
+ if opts[:ssl] === false || @host == TreasureData::API::OLD_ENDPOINT
99
+ # for backward compatibility, old endpoint specified without ssl option, use http
100
+ #
101
+ # opts[:ssl] would be nil if user doesn't specify ssl options,
102
+ # but connecting to https is the new default behavior (since 0.9)
103
+ # so check ssl option by `if opts[:ssl] === false` instead of `if opts[:ssl]`
104
+ # that means if user desire to use http, give `:ssl => false` for initializer such as API.new("APIKEY", :ssl => false)
99
105
  @port = 80 if @port == 0
100
106
  @ssl = false
107
+ else
108
+ @port = 443 if @port == 0
109
+ @ssl = true
101
110
  end
102
111
  @base_path = ''
103
112
  end
@@ -112,12 +121,15 @@ class API
112
121
  # @!attribute [r] apikey
113
122
  attr_reader :apikey
114
123
 
124
+ MSGPACK_INT64_MAX = 2 ** 64 - 1
125
+ MSGPACK_INT64_MIN = -1 * (2 ** 63) # it's just same with -2**63, but for readability
126
+
115
127
  # @param [Hash] record
116
128
  # @param [IO] out
117
129
  def self.normalized_msgpack(record, out = nil)
118
130
  record.keys.each { |k|
119
131
  v = record[k]
120
- if v.kind_of?(Bignum)
132
+ if v.kind_of?(Integer) && (v > MSGPACK_INT64_MAX || v < MSGPACK_INT64_MIN)
121
133
  record[k] = v.to_s
122
134
  end
123
135
  }
@@ -140,15 +152,27 @@ class API
140
152
  end
141
153
 
142
154
  name = name.to_s
143
- if name.empty?
144
- raise ParameterValidationError,
155
+ if max_len
156
+ if name.length < min_len || name.length > max_len
157
+ raise ParameterValidationError,
158
+ "#{target.capitalize} name must be between #{min_len} and #{max_len} characters long. Got #{name.length} " +
159
+ (name.length == 1 ? "character" : "characters") + "."
160
+ end
161
+ else
162
+ if min_len == 1
163
+ if name.empty?
164
+ raise ParameterValidationError,
145
165
  "Empty #{target} name is not allowed"
146
- end
147
- if name.length < min_len || name.length > max_len
148
- raise ParameterValidationError,
149
- "#{target.capitalize} name must be between #{min_len} and #{max_len} characters long. Got #{name.length} " +
166
+ end
167
+ else
168
+ if name.length < min_len
169
+ raise ParameterValidationError,
170
+ "#{target.capitalize} name must be longer than #{min_len} characters. Got #{name.length} " +
150
171
  (name.length == 1 ? "character" : "characters") + "."
172
+ end
173
+ end
151
174
  end
175
+
152
176
  unless name =~ /^([a-z0-9_]+)$/
153
177
  raise ParameterValidationError,
154
178
  "#{target.capitalize} name must only consist of lower-case alpha-numeric characters and '_'."
@@ -174,7 +198,18 @@ class API
174
198
 
175
199
  # @param [String] name
176
200
  def self.validate_column_name(name)
177
- validate_name("column", 1, 255, name)
201
+ target = 'column'
202
+ name = name.to_s
203
+ if name.empty?
204
+ raise ParameterValidationError,
205
+ "Empty #{target} name is not allowed"
206
+ end
207
+ name
208
+ end
209
+
210
+ # @param [String] name
211
+ def self.validate_sql_alias_name(name)
212
+ validate_name("sql_alias", 1, nil, name)
178
213
  end
179
214
 
180
215
  # @param [String] name
@@ -199,25 +234,6 @@ class API
199
234
  normalize_database_name(name)
200
235
  end
201
236
 
202
- # TODO support array types
203
- # @param [String] name
204
- def self.normalize_type_name(name)
205
- case name
206
- when /int/i, /integer/i
207
- "int"
208
- when /long/i, /bigint/i
209
- "long"
210
- when /string/i
211
- "string"
212
- when /float/i
213
- "float"
214
- when /double/i
215
- "double"
216
- else
217
- raise "Type name must either of int, long, string float or double"
218
- end
219
- end
220
-
221
237
  # for fluent-plugin-td / td command to check table existence with import onlt user
222
238
  # @return [String]
223
239
  def self.create_empty_gz_data
@@ -241,6 +257,7 @@ private
241
257
  do_get(url, params, &block)
242
258
  end
243
259
  end
260
+
244
261
  # @param [String] url
245
262
  # @param [Hash] params
246
263
  # @yield [response]
@@ -295,7 +312,7 @@ private
295
312
  retry_delay *= 2
296
313
  redo # restart from beginning of do-while loop
297
314
  end
298
- rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError, OpenSSL::SSL::SSLError, SocketError, IncompleteError => e
315
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError, OpenSSL::SSL::SSLError, SocketError, IncompleteError, HTTPClient::TimeoutError => e
299
316
  if block_given?
300
317
  raise e
301
318
  end
@@ -591,18 +608,15 @@ private
591
608
  end
592
609
 
593
610
  def parse_error_response(res)
594
- error = {}
595
-
596
611
  begin
597
- js = JSON.load(res.body)
598
- if js.nil?
599
- error['message'] = res.reason
612
+ error = JSON.load(res.body)
613
+ if error
614
+ error['message'] = error['error'] unless error['message']
600
615
  else
601
- error['message'] = js['message'] || js['error']
602
- error['stacktrace'] = js['stacktrace']
616
+ error = {'message' => res.reason}
603
617
  end
604
618
  rescue JSON::ParserError
605
- error['message'] = res.body
619
+ error = {'message' => res.body}
606
620
  end
607
621
 
608
622
  error
@@ -624,6 +638,7 @@ private
624
638
  when "404"
625
639
  NotFoundError
626
640
  when "409"
641
+ message = "#{message}: conflicts_with job:#{error["details"]["conflicts_with"]}" if error["details"] && error["details"]["conflicts_with"]
627
642
  AlreadyExistsError
628
643
  when "401"
629
644
  AuthError
@@ -635,11 +650,15 @@ private
635
650
  end
636
651
  end
637
652
 
638
- if error_class.method_defined?(:api_backtrace)
639
- raise error_class.new(message, error['stacktrace'])
653
+ exc = nil
654
+ if error_class.method_defined?(:conflicts_with) && error["details"] && error["details"]["conflicts_with"]
655
+ exc = error_class.new(message, error['stacktrace'], error["details"]["conflicts_with"])
656
+ elsif error_class.method_defined?(:api_backtrace)
657
+ exc = error_class.new(message, error['stacktrace'])
640
658
  else
641
- raise error_class, message
659
+ exc = error_class.new(message)
642
660
  end
661
+ raise exc
643
662
  end
644
663
 
645
664
  if ''.respond_to?(:encode)
@@ -161,8 +161,7 @@ module BulkImport
161
161
  end
162
162
  end
163
163
  require File.expand_path('../compat_gzip_reader', File.dirname(__FILE__))
164
- io = StringIO.new(Zlib::GzipReader.new(StringIO.new(body)).read)
165
- u = MessagePack::Unpacker.new(io)
164
+ u = MessagePack::Unpacker.new(Zlib::GzipReader.new(StringIO.new(body)))
166
165
  if block
167
166
  begin
168
167
  u.each(&block)
@@ -116,10 +116,10 @@ module BulkLoad
116
116
  JSON.load(res.body)
117
117
  end
118
118
 
119
- # name: String, job: Hash -> Hash
120
- def bulk_load_update(name, job)
119
+ # name: String, settings: Hash -> Hash
120
+ def bulk_load_update(name, settings)
121
121
  path = session_path(name)
122
- res = api { put(path, job.to_json) }
122
+ res = api { put(path, settings.to_json) }
123
123
  unless res.ok?
124
124
  raise_error("BulkLoadSession: #{name} update failed", res)
125
125
  end
@@ -22,5 +22,17 @@ module Export
22
22
  return js['job_id'].to_s
23
23
  end
24
24
 
25
+ # => jobId:String
26
+ # @param [String] target_job_id
27
+ # @param [Hash] opts
28
+ # @return [String] job_id
29
+ def result_export(target_job_id, opts={})
30
+ code, body, res = post("/v3/job/result_export/#{target_job_id}", opts)
31
+ if code != "200"
32
+ raise_error("Result Export failed", res)
33
+ end
34
+ js = checked_json(body, %w[job_id])
35
+ return js['job_id'].to_s
36
+ end
25
37
  end
26
38
  end
@@ -21,8 +21,9 @@ module Import
21
21
  opts = {}
22
22
  if @host == DEFAULT_ENDPOINT
23
23
  opts[:host] = DEFAULT_IMPORT_ENDPOINT
24
- elsif @host == NEW_DEFAULT_ENDPOINT
25
- opts[:host] = NEW_DEFAULT_IMPORT_ENDPOINT
24
+ elsif @host == TreasureData::API::OLD_ENDPOINT # backward compatibility
25
+ opts[:host] = 'api-import.treasure-data.com'
26
+ opts[:ssl] = false
26
27
  end
27
28
  code, body, res = put(path, stream, size, opts)
28
29
  if code[0] != ?2
@@ -36,9 +36,10 @@ module Job
36
36
  priority = m['priority']
37
37
  retry_limit = m['retry_limit']
38
38
  duration = m['duration']
39
+ num_records = m['num_records']
39
40
  result << [job_id, type, status, query, start_at, end_at, cpu_time,
40
41
  result_size, result_url, priority, retry_limit, nil, database,
41
- duration]
42
+ duration, num_records]
42
43
  }
43
44
  return result
44
45
  end
@@ -63,6 +64,8 @@ module Job
63
64
  end_at = js['end_at']
64
65
  cpu_time = js['cpu_time']
65
66
  result_size = js['result_size'] # compressed result size in msgpack.gz format
67
+ num_records = js['num_records']
68
+ duration = js['duration']
66
69
  result = js['result'] # result target URL
67
70
  hive_result_schema = (js['hive_result_schema'] || '')
68
71
  if hive_result_schema.empty?
@@ -97,7 +100,7 @@ module Job
97
100
  priority = js['priority']
98
101
  retry_limit = js['retry_limit']
99
102
  return [type, query, status, url, debug, start_at, end_at, cpu_time,
100
- result_size, result, hive_result_schema, priority, retry_limit, nil, database]
103
+ result_size, result, hive_result_schema, priority, retry_limit, nil, database, duration, num_records]
101
104
  end
102
105
 
103
106
  # @param [String] job_id
@@ -115,14 +118,13 @@ module Job
115
118
  # @param [String] job_id
116
119
  # @return [Array]
117
120
  def job_result(job_id)
118
- code, body, res = get("/v3/job/result/#{e job_id}", {'format'=>'msgpack'})
119
- if code != "200"
120
- raise_error("Get job result failed", res)
121
- end
122
121
  result = []
123
- MessagePack::Unpacker.new.feed_each(body) {|row|
124
- result << row
125
- }
122
+ unpacker = MessagePack::Unpacker.new
123
+ job_result_download(job_id) do |chunk|
124
+ unpacker.feed_each(chunk) do |row|
125
+ result << row
126
+ end
127
+ end
126
128
  return result
127
129
  end
128
130
 
@@ -133,24 +135,17 @@ module Job
133
135
  # @param [IO] io
134
136
  # @param [Proc] block
135
137
  # @return [nil, String]
136
- def job_result_format(job_id, format, io=nil, &block)
138
+ def job_result_format(job_id, format, io=nil)
137
139
  if io
138
- infl = nil
139
- code, body, res = get("/v3/job/result/#{e job_id}", {'format'=>format}) {|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
- }
140
+ job_result_download(job_id, format) do |chunk, total|
141
+ io.write chunk
142
+ yield total if block_given?
143
+ end
149
144
  nil
150
145
  else
151
- code, body, res = get("/v3/job/result/#{e job_id}", {'format'=>format})
152
- if code != "200"
153
- raise_error("Get job result failed", res)
146
+ body = String.new
147
+ job_result_download(job_id, format) do |chunk|
148
+ body << chunk
154
149
  end
155
150
  body
156
151
  end
@@ -163,22 +158,11 @@ module Job
163
158
  # @return [nil]
164
159
  def job_result_each(job_id, &block)
165
160
  upkr = MessagePack::Unpacker.new
166
- infl = nil
167
-
168
- get("/v3/job/result/#{e job_id}", {'format'=>'msgpack'}) {|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
- }
161
+ # default to decompressing the response since format is fixed to 'msgpack'
162
+ job_result_download(job_id) do |chunk|
163
+ upkr.feed_each(chunk, &block)
164
+ end
179
165
  nil
180
- ensure
181
- infl.close if infl
182
166
  end
183
167
 
184
168
  # block is optional and must accept 1 argument
@@ -186,50 +170,30 @@ module Job
186
170
  # @param [String] job_id
187
171
  # @param [Proc] block
188
172
  # @return [nil]
189
- def job_result_each_with_compr_size(job_id, &block)
173
+ def job_result_each_with_compr_size(job_id)
190
174
  upkr = MessagePack::Unpacker.new
191
- infl = nil
192
-
193
- get("/v3/job/result/#{e job_id}", {'format'=>'msgpack'}) {|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?
175
+ # default to decompressing the response since format is fixed to 'msgpack'
176
+ job_result_download(job_id) do |chunk, total|
177
+ upkr.feed_each(chunk) {|unpacked|
178
+ yield unpacked, total if block_given?
204
179
  }
205
- }
180
+ end
206
181
  nil
207
- ensure
208
- infl.close if infl
209
182
  end
210
183
 
211
184
  # @param [String] job_id
212
185
  # @param [String] format
213
186
  # @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}) {|res, chunk, current_total_chunk_size|
218
- if res.code != 200
219
- raise_error("Get job result failed", res)
220
- end
221
-
187
+ def job_result_raw(job_id, format, io = nil)
188
+ body = io ? nil : String.new
189
+ job_result_download(job_id, format, false) do |chunk, total|
222
190
  if io
223
191
  io.write(chunk)
224
- block.call(current_total_chunk_size) if block_given?
192
+ yield total if block_given?
225
193
  else
226
- if body
227
- body += chunk
228
- else
229
- body = chunk
230
- end
194
+ body << chunk
231
195
  end
232
- }
196
+ end
233
197
  body
234
198
  end
235
199
 
@@ -287,6 +251,117 @@ module Job
287
251
 
288
252
  private
289
253
 
254
+ def validate_content_length_with_range(response, current_total_chunk_size)
255
+ if expected_size = response.header['Content-Range'][0]
256
+ expected_size = expected_size[/\d+$/].to_i
257
+ elsif expected_size = response.header['Content-Length'][0]
258
+ expected_size = expected_size.to_i
259
+ end
260
+
261
+ if expected_size.nil?
262
+ elsif current_total_chunk_size < expected_size
263
+ # too small
264
+ # NOTE:
265
+ # ext/openssl raises EOFError in case where underlying connection
266
+ # causes an error, but httpclient ignores it.
267
+ # https://github.com/nahi/httpclient/blob/v3.2.8/lib/httpclient/session.rb#L1003
268
+ raise EOFError, 'httpclient IncompleteError'
269
+ elsif current_total_chunk_size > expected_size
270
+ # too large
271
+ raise_error("Get job result failed", response)
272
+ end
273
+ end
274
+
275
+ def job_result_download(job_id, format='msgpack', autodecode=true)
276
+ client, header = new_client
277
+ client.send_timeout = @send_timeout
278
+ client.receive_timeout = @read_timeout
279
+ header['Accept-Encoding'] = 'deflate, gzip'
280
+
281
+ url = build_endpoint("/v3/job/result/#{e job_id}", @host)
282
+ params = {'format' => format}
283
+
284
+ unless ENV['TD_CLIENT_DEBUG'].nil?
285
+ puts "DEBUG: REST GET call:"
286
+ puts "DEBUG: header: " + header.to_s
287
+ puts "DEBUG: url: " + url.to_s
288
+ puts "DEBUG: params: " + params.to_s
289
+ end
290
+
291
+ # up to 7 retries with exponential (base 2) back-off starting at 'retry_delay'
292
+ retry_delay = @retry_delay
293
+ cumul_retry_delay = 0
294
+ current_total_chunk_size = 0
295
+ infl = nil
296
+ begin # LOOP of Network/Server errors
297
+ response = nil
298
+ client.get(url, params, header) do |res, chunk|
299
+ unless response
300
+ case res.status
301
+ when 200
302
+ if current_total_chunk_size != 0
303
+ # try to resume but the server returns 200
304
+ raise_error("Get job result failed", res)
305
+ end
306
+ when 206 # resuming
307
+ else
308
+ if res.status/100 == 5 && cumul_retry_delay < @max_cumul_retry_delay
309
+ $stderr.puts "Error #{res.status}: #{get_error(res)}. Retrying after #{retry_delay} seconds..."
310
+ sleep retry_delay
311
+ cumul_retry_delay += retry_delay
312
+ retry_delay *= 2
313
+ redo
314
+ end
315
+ raise_error("Get job result failed", res)
316
+ end
317
+ if infl.nil? && autodecode
318
+ case res.header['Content-Encoding'][0].to_s.downcase
319
+ when 'gzip'
320
+ infl = Zlib::Inflate.new(Zlib::MAX_WBITS + 16)
321
+ when 'deflate'
322
+ infl = Zlib::Inflate.new
323
+ end
324
+ end
325
+ end
326
+ response = res
327
+ current_total_chunk_size += chunk.bytesize
328
+ chunk = infl.inflate(chunk) if infl
329
+ yield chunk, current_total_chunk_size
330
+ end
331
+
332
+ # completed?
333
+ validate_content_length_with_range(response, current_total_chunk_size)
334
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError, OpenSSL::SSL::SSLError, SocketError => e
335
+ if response # at least a chunk is downloaded
336
+ if etag = response.header['ETag'][0]
337
+ header['If-Range'] = etag
338
+ header['Range'] = "bytes=#{current_total_chunk_size}-"
339
+ end
340
+ end
341
+
342
+ $stderr.print "#{e.class}: #{e.message}. "
343
+ if cumul_retry_delay < @max_cumul_retry_delay
344
+ $stderr.puts "Retrying after #{retry_delay} seconds..."
345
+ sleep retry_delay
346
+ cumul_retry_delay += retry_delay
347
+ retry_delay *= 2
348
+ retry
349
+ end
350
+ raise
351
+ end
352
+
353
+ unless ENV['TD_CLIENT_DEBUG'].nil?
354
+ puts "DEBUG: REST GET response:"
355
+ puts "DEBUG: header: " + response.header.to_s
356
+ puts "DEBUG: status: " + response.code.to_s
357
+ puts "DEBUG: body: " + response.body.to_s
358
+ end
359
+
360
+ nil
361
+ ensure
362
+ infl.close if infl
363
+ end
364
+
290
365
  class NullInflate
291
366
  def inflate(chunk)
292
367
  chunk