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.
- 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
|