td-client 0.9.0dev2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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