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.
- checksums.yaml +4 -4
- data/lib/td/client.rb +16 -8
- data/lib/td/client/api.rb +66 -47
- data/lib/td/client/api/bulk_import.rb +1 -2
- data/lib/td/client/api/bulk_load.rb +3 -3
- data/lib/td/client/api/export.rb +12 -0
- data/lib/td/client/api/import.rb +3 -2
- data/lib/td/client/api/job.rb +146 -71
- data/lib/td/client/api/schedule.rb +1 -1
- data/lib/td/client/api_error.rb +5 -0
- data/lib/td/client/model.rb +92 -28
- data/lib/td/client/version.rb +1 -1
- data/spec/spec_helper.rb +5 -5
- data/spec/td/client/account_api_spec.rb +5 -5
- data/spec/td/client/api_error_spec.rb +77 -0
- data/spec/td/client/api_spec.rb +76 -52
- data/spec/td/client/api_ssl_connection_spec.rb +1 -1
- data/spec/td/client/bulk_import_spec.rb +28 -29
- data/spec/td/client/bulk_load_spec.rb +60 -35
- data/spec/td/client/db_api_spec.rb +1 -1
- data/spec/td/client/export_api_spec.rb +11 -1
- data/spec/td/client/import_api_spec.rb +85 -10
- data/spec/td/client/job_api_spec.rb +568 -61
- data/spec/td/client/model_job_spec.rb +27 -10
- data/spec/td/client/model_schedule_spec.rb +2 -2
- data/spec/td/client/model_schema_spec.rb +134 -0
- data/spec/td/client/partial_delete_api_spec.rb +1 -1
- data/spec/td/client/result_api_spec.rb +3 -3
- data/spec/td/client/sched_api_spec.rb +12 -4
- data/spec/td/client/server_status_api_spec.rb +2 -2
- data/spec/td/client/spec_resources.rb +1 -0
- data/spec/td/client/table_api_spec.rb +14 -14
- data/spec/td/client/user_api_spec.rb +12 -12
- data/spec/td/client_sched_spec.rb +31 -6
- data/spec/td/client_spec.rb +1 -0
- metadata +42 -81
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 140238f578d4cee66cc4ef09c155375719ad1c9e
|
4
|
+
data.tar.gz: cc7df46122c3186fc80c99ca1911aff0bcf8c994
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bdd1ba7cfb27afbc22a07845c06cf420afa1d9d40411137573ec2406ceeb923838a65be2e3ce7dbdc5120126a3386aa6234dca79426e9341d57e3a82ebe559d3
|
7
|
+
data.tar.gz: 8045b7325a7492b35db7962ed36d3bb6895d3307990c1e43d7523e0832b9a33b7d2ac4d792a0b8284755342726c231ea994f83d0829a72a19a3b7a891789a738
|
data/lib/td/client.rb
CHANGED
@@ -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,
|
578
|
-
def bulk_load_update(name,
|
579
|
-
@api.bulk_load_update(name,
|
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
|
data/lib/td/client/api.rb
CHANGED
@@ -36,16 +36,19 @@ class API
|
|
36
36
|
include API::Table
|
37
37
|
include API::User
|
38
38
|
|
39
|
-
DEFAULT_ENDPOINT = 'api.
|
40
|
-
DEFAULT_IMPORT_ENDPOINT = 'api-import.
|
39
|
+
DEFAULT_ENDPOINT = 'api.treasuredata.com'
|
40
|
+
DEFAULT_IMPORT_ENDPOINT = 'api-import.treasuredata.com'
|
41
41
|
|
42
|
-
|
43
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
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?(
|
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
|
144
|
-
|
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
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
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
|
-
|
598
|
-
if
|
599
|
-
error['message']
|
612
|
+
error = JSON.load(res.body)
|
613
|
+
if error
|
614
|
+
error['message'] = error['error'] unless error['message']
|
600
615
|
else
|
601
|
-
error
|
602
|
-
error['stacktrace'] = js['stacktrace']
|
616
|
+
error = {'message' => res.reason}
|
603
617
|
end
|
604
618
|
rescue JSON::ParserError
|
605
|
-
error
|
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
|
-
|
639
|
-
|
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
|
-
|
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
|
-
|
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,
|
120
|
-
def bulk_load_update(name,
|
119
|
+
# name: String, settings: Hash -> Hash
|
120
|
+
def bulk_load_update(name, settings)
|
121
121
|
path = session_path(name)
|
122
|
-
res = api { put(path,
|
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
|
data/lib/td/client/api/export.rb
CHANGED
@@ -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
|
data/lib/td/client/api/import.rb
CHANGED
@@ -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 ==
|
25
|
-
opts[:host] =
|
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
|
data/lib/td/client/api/job.rb
CHANGED
@@ -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
|
124
|
-
|
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
|
138
|
+
def job_result_format(job_id, format, io=nil)
|
137
139
|
if io
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
173
|
+
def job_result_each_with_compr_size(job_id)
|
190
174
|
upkr = MessagePack::Unpacker.new
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
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
|
-
|
192
|
+
yield total if block_given?
|
225
193
|
else
|
226
|
-
|
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
|