td-client 1.0.0-java

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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/data/ca-bundle.crt +3448 -0
  3. data/lib/td-client.rb +1 -0
  4. data/lib/td/client.rb +606 -0
  5. data/lib/td/client/api.rb +707 -0
  6. data/lib/td/client/api/access_control.rb +74 -0
  7. data/lib/td/client/api/account.rb +45 -0
  8. data/lib/td/client/api/bulk_import.rb +184 -0
  9. data/lib/td/client/api/bulk_load.rb +172 -0
  10. data/lib/td/client/api/database.rb +50 -0
  11. data/lib/td/client/api/export.rb +38 -0
  12. data/lib/td/client/api/import.rb +38 -0
  13. data/lib/td/client/api/job.rb +390 -0
  14. data/lib/td/client/api/partial_delete.rb +27 -0
  15. data/lib/td/client/api/result.rb +46 -0
  16. data/lib/td/client/api/schedule.rb +120 -0
  17. data/lib/td/client/api/server_status.rb +21 -0
  18. data/lib/td/client/api/table.rb +132 -0
  19. data/lib/td/client/api/user.rb +134 -0
  20. data/lib/td/client/api_error.rb +37 -0
  21. data/lib/td/client/compat_gzip_reader.rb +22 -0
  22. data/lib/td/client/model.rb +816 -0
  23. data/lib/td/client/version.rb +5 -0
  24. data/lib/td/core_ext/openssl/ssl/sslcontext/set_params.rb +18 -0
  25. data/spec/spec_helper.rb +63 -0
  26. data/spec/td/client/access_control_api_spec.rb +37 -0
  27. data/spec/td/client/account_api_spec.rb +34 -0
  28. data/spec/td/client/api_error_spec.rb +77 -0
  29. data/spec/td/client/api_spec.rb +269 -0
  30. data/spec/td/client/api_ssl_connection_spec.rb +109 -0
  31. data/spec/td/client/bulk_import_spec.rb +199 -0
  32. data/spec/td/client/bulk_load_spec.rb +401 -0
  33. data/spec/td/client/db_api_spec.rb +123 -0
  34. data/spec/td/client/export_api_spec.rb +51 -0
  35. data/spec/td/client/import_api_spec.rb +148 -0
  36. data/spec/td/client/job_api_spec.rb +833 -0
  37. data/spec/td/client/model_job_spec.rb +136 -0
  38. data/spec/td/client/model_schedule_spec.rb +26 -0
  39. data/spec/td/client/model_schema_spec.rb +134 -0
  40. data/spec/td/client/partial_delete_api_spec.rb +58 -0
  41. data/spec/td/client/result_api_spec.rb +77 -0
  42. data/spec/td/client/sched_api_spec.rb +109 -0
  43. data/spec/td/client/server_status_api_spec.rb +25 -0
  44. data/spec/td/client/spec_resources.rb +99 -0
  45. data/spec/td/client/table_api_spec.rb +226 -0
  46. data/spec/td/client/user_api_spec.rb +118 -0
  47. data/spec/td/client_sched_spec.rb +79 -0
  48. data/spec/td/client_spec.rb +46 -0
  49. metadata +271 -0
@@ -0,0 +1,707 @@
1
+ require 'td/client/api_error'
2
+ require 'td/client/version'
3
+ require 'td/client/api/access_control'
4
+ require 'td/client/api/account'
5
+ require 'td/client/api/bulk_import'
6
+ require 'td/client/api/bulk_load'
7
+ require 'td/client/api/database'
8
+ require 'td/client/api/export'
9
+ require 'td/client/api/import'
10
+ require 'td/client/api/job'
11
+ require 'td/client/api/partial_delete'
12
+ require 'td/client/api/result'
13
+ require 'td/client/api/schedule'
14
+ require 'td/client/api/server_status'
15
+ require 'td/client/api/table'
16
+ require 'td/client/api/user'
17
+
18
+ # For disabling SSLv3 connection in favor of POODLE Attack protection
19
+ require 'td/core_ext/openssl/ssl/sslcontext/set_params'
20
+
21
+ module TreasureData
22
+
23
+ class API
24
+ include API::AccessControl
25
+ include API::Account
26
+ include API::BulkImport
27
+ include API::BulkLoad
28
+ include API::Database
29
+ include API::Export
30
+ include API::Import
31
+ include API::Job
32
+ include API::PartialDelete
33
+ include API::Result
34
+ include API::Schedule
35
+ include API::ServerStatus
36
+ include API::Table
37
+ include API::User
38
+
39
+ DEFAULT_ENDPOINT = 'api.treasuredata.com'
40
+ DEFAULT_IMPORT_ENDPOINT = 'api-import.treasuredata.com'
41
+
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'
46
+
47
+ class IncompleteError < APIError; end
48
+
49
+ # @param [String] apikey
50
+ # @param [Hash] opts
51
+ # for backward compatibility
52
+ def initialize(apikey, opts={})
53
+ require 'json'
54
+ require 'time'
55
+ require 'uri'
56
+ require 'net/http'
57
+ require 'net/https'
58
+ require 'time'
59
+ #require 'faraday' # faraday doesn't support streaming upload with httpclient yet so now disabled
60
+ require 'httpclient'
61
+ require 'zlib'
62
+ require 'stringio'
63
+ require 'cgi'
64
+ require 'msgpack'
65
+
66
+ @apikey = apikey
67
+ @user_agent = "TD-Client-Ruby: #{TreasureData::Client::VERSION}"
68
+ @user_agent = "#{opts[:user_agent]}; " + @user_agent if opts.has_key?(:user_agent)
69
+
70
+ endpoint = opts[:endpoint] || ENV['TD_API_SERVER'] || DEFAULT_ENDPOINT
71
+ uri = URI.parse(endpoint)
72
+
73
+ @connect_timeout = opts[:connect_timeout] || 60
74
+ @read_timeout = opts[:read_timeout] || 600
75
+ @send_timeout = opts[:send_timeout] || 600
76
+ @retry_post_requests = opts[:retry_post_requests] || false
77
+ @retry_delay = opts[:retry_delay] || 5
78
+ @max_cumul_retry_delay = opts[:max_cumul_retry_delay] || 600
79
+
80
+ case uri.scheme
81
+ when 'http', 'https'
82
+ @host = uri.host
83
+ @port = uri.port
84
+ # the opts[:ssl] option is ignored here, it's value
85
+ # overridden by the scheme of the endpoint URI
86
+ @ssl = (uri.scheme == 'https')
87
+ @base_path = uri.path.to_s
88
+
89
+ else
90
+ if uri.port
91
+ # invalid URI
92
+ raise "Invalid endpoint: #{endpoint}"
93
+ end
94
+
95
+ # generic URI
96
+ @host, @port = endpoint.split(':', 2)
97
+ @port = @port.to_i
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)
105
+ @port = 80 if @port == 0
106
+ @ssl = false
107
+ else
108
+ @port = 443 if @port == 0
109
+ @ssl = true
110
+ end
111
+ @base_path = ''
112
+ end
113
+
114
+ @http_proxy = opts[:http_proxy] || ENV['HTTP_PROXY']
115
+ @headers = opts[:headers] || {}
116
+ @api = api_client("#{@ssl ? 'https' : 'http'}://#{@host}:#{@port}")
117
+ end
118
+
119
+ # TODO error check & raise appropriate errors
120
+
121
+ # @!attribute [r] apikey
122
+ attr_reader :apikey
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
+
127
+ # @param [Hash] record
128
+ # @param [IO] out
129
+ def self.normalized_msgpack(record, out = nil)
130
+ record.keys.each { |k|
131
+ v = record[k]
132
+ if v.kind_of?(Integer) && (v > MSGPACK_INT64_MAX || v < MSGPACK_INT64_MIN)
133
+ record[k] = v.to_s
134
+ end
135
+ }
136
+ if out
137
+ out << record.to_msgpack
138
+ out
139
+ else
140
+ record.to_msgpack
141
+ end
142
+ end
143
+
144
+ # @param [String] target
145
+ # @param [Fixnum] min_len
146
+ # @param [Fixnum] max_len
147
+ # @param [String] name
148
+ def self.validate_name(target, min_len, max_len, name)
149
+ if !target.instance_of?(String) || target.empty?
150
+ raise ParameterValidationError,
151
+ "A valid target name is required"
152
+ end
153
+
154
+ name = name.to_s
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,
165
+ "Empty #{target} name is not allowed"
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} " +
171
+ (name.length == 1 ? "character" : "characters") + "."
172
+ end
173
+ end
174
+ end
175
+
176
+ unless name =~ /^([a-z0-9_]+)$/
177
+ raise ParameterValidationError,
178
+ "#{target.capitalize} name must only consist of lower-case alpha-numeric characters and '_'."
179
+ end
180
+
181
+ name
182
+ end
183
+
184
+ # @param [String] name
185
+ def self.validate_database_name(name)
186
+ validate_name("database", 3, 255, name)
187
+ end
188
+
189
+ # @param [String] name
190
+ def self.validate_table_name(name)
191
+ validate_name("table", 3, 255, name)
192
+ end
193
+
194
+ # @param [String] name
195
+ def self.validate_result_set_name(name)
196
+ validate_name("result set", 3, 255, name)
197
+ end
198
+
199
+ # @param [String] name
200
+ def self.validate_column_name(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)
213
+ end
214
+
215
+ # @param [String] name
216
+ def self.normalize_database_name(name)
217
+ name = name.to_s
218
+ if name.empty?
219
+ raise "Empty name is not allowed"
220
+ end
221
+ if name.length < 3
222
+ name += "_" * (3 - name.length)
223
+ end
224
+ if 255 < name.length
225
+ name = name[0, 253] + "__"
226
+ end
227
+ name = name.downcase
228
+ name = name.gsub(/[^a-z0-9_]/, '_')
229
+ name
230
+ end
231
+
232
+ # @param [String] name
233
+ def self.normalize_table_name(name)
234
+ normalize_database_name(name)
235
+ end
236
+
237
+ # for fluent-plugin-td / td command to check table existence with import onlt user
238
+ # @return [String]
239
+ def self.create_empty_gz_data
240
+ io = StringIO.new
241
+ Zlib::GzipWriter.new(io).close
242
+ io.string
243
+ end
244
+
245
+ # @param [String] ssl_ca_file
246
+ def ssl_ca_file=(ssl_ca_file)
247
+ @ssl_ca_file = ssl_ca_file
248
+ end
249
+
250
+ private
251
+
252
+ # @param [String] url
253
+ # @param [Hash] params
254
+ # @yield [response]
255
+ def get(url, params=nil, &block)
256
+ guard_no_sslv3 do
257
+ do_get(url, params, &block)
258
+ end
259
+ end
260
+
261
+ # @param [String] url
262
+ # @param [Hash] params
263
+ # @yield [response]
264
+ def do_get(url, params=nil, &block)
265
+ client, header = new_client
266
+ client.send_timeout = @send_timeout
267
+ client.receive_timeout = @read_timeout
268
+
269
+ header['Accept-Encoding'] = 'deflate, gzip'
270
+
271
+ target = build_endpoint(url, @host)
272
+
273
+ unless ENV['TD_CLIENT_DEBUG'].nil?
274
+ puts "DEBUG: REST GET call:"
275
+ puts "DEBUG: header: " + header.to_s
276
+ puts "DEBUG: path: " + target.to_s
277
+ puts "DEBUG: params: " + params.to_s
278
+ end
279
+
280
+ # up to 7 retries with exponential (base 2) back-off starting at 'retry_delay'
281
+ retry_delay = @retry_delay
282
+ cumul_retry_delay = 0
283
+
284
+ # for both exceptions and 500+ errors retrying is enabled by default.
285
+ # The total number of retries cumulatively should not exceed 10 minutes / 600 seconds
286
+ response = nil
287
+ begin # this block is to allow retry (redo) in the begin part of the begin-rescue block
288
+ begin
289
+ if block
290
+ current_total_chunk_size = 0
291
+ response = client.get(target, params, header) {|res, chunk|
292
+ current_total_chunk_size += chunk.bytesize
293
+ block.call(res, chunk, current_total_chunk_size)
294
+ }
295
+
296
+ # XXX ext/openssl raises EOFError in case where underlying connection causes an error,
297
+ # and msgpack-ruby that used in block handles it as an end of stream == no exception.
298
+ # Therefor, check content size.
299
+ validate_content_length!(response, current_total_chunk_size) if @ssl
300
+ else
301
+ response = client.get(target, params, header)
302
+
303
+ validate_content_length!(response, response.body.bytesize) if @ssl
304
+ end
305
+
306
+ status = response.code
307
+ # retry if the HTTP error code is 500 or higher and we did not run out of retrying attempts
308
+ if !block_given? && status >= 500 && cumul_retry_delay < @max_cumul_retry_delay
309
+ $stderr.puts "Error #{status}: #{get_error(response)}. Retrying after #{retry_delay} seconds..."
310
+ sleep retry_delay
311
+ cumul_retry_delay += retry_delay
312
+ retry_delay *= 2
313
+ redo # restart from beginning of do-while loop
314
+ end
315
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError, OpenSSL::SSL::SSLError, SocketError, IncompleteError, HTTPClient::TimeoutError => e
316
+ if block_given?
317
+ raise e
318
+ end
319
+ $stderr.print "#{e.class}: #{e.message}. "
320
+ if cumul_retry_delay < @max_cumul_retry_delay
321
+ $stderr.puts "Retrying after #{retry_delay} seconds..."
322
+ sleep retry_delay
323
+ cumul_retry_delay += retry_delay
324
+ retry_delay *= 2
325
+ retry
326
+ else
327
+ $stderr.puts "Retrying stopped after #{@max_cumul_retry_delay} seconds."
328
+ raise e
329
+ end
330
+ rescue => e
331
+ raise e
332
+ end
333
+ end while false
334
+
335
+ unless ENV['TD_CLIENT_DEBUG'].nil?
336
+ puts "DEBUG: REST GET response:"
337
+ puts "DEBUG: header: " + response.header.to_s
338
+ puts "DEBUG: status: " + response.code.to_s
339
+ puts "DEBUG: body: " + response.body.to_s
340
+ end
341
+
342
+ body = block ? response.body : inflate_body(response)
343
+
344
+ return [response.code.to_s, body, response]
345
+ end
346
+
347
+ def validate_content_length!(response, body_size)
348
+ content_length = response.header['Content-Length'].first
349
+ raise IncompleteError if @ssl && content_length && content_length.to_i != body_size
350
+ end
351
+
352
+ def inflate_body(response)
353
+ return response.body if (ce = response.header['Content-Encoding']).empty?
354
+
355
+ if ce.include?('gzip')
356
+ infl = Zlib::Inflate.new(Zlib::MAX_WBITS + 16)
357
+ begin
358
+ infl.inflate(response.body)
359
+ ensure
360
+ infl.close
361
+ end
362
+ else
363
+ # NOTE maybe for content-encoding is msgpack.gz ?
364
+ Zlib::Inflate.inflate(response.body)
365
+ end
366
+ end
367
+
368
+ # @param [String] url
369
+ # @param [Hash] params
370
+ def post(url, params=nil, &block)
371
+ guard_no_sslv3 do
372
+ do_post(url, params, &block)
373
+ end
374
+ end
375
+
376
+ # @param [String] url
377
+ # @param [Hash] params
378
+ def do_post(url, params=nil, &block)
379
+ target = build_endpoint(url, @host)
380
+
381
+ client, header = new_client
382
+ client.send_timeout = @send_timeout
383
+ client.receive_timeout = @read_timeout
384
+ header['Accept-Encoding'] = 'gzip'
385
+
386
+ unless ENV['TD_CLIENT_DEBUG'].nil?
387
+ puts "DEBUG: REST POST call:"
388
+ puts "DEBUG: header: " + header.to_s
389
+ puts "DEBUG: path: " + target.to_s
390
+ puts "DEBUG: params: " + params.to_s
391
+ end
392
+
393
+ # up to 7 retries with exponential (base 2) back-off starting at 'retry_delay'
394
+ retry_delay = @retry_delay
395
+ cumul_retry_delay = 0
396
+
397
+ # for both exceptions and 500+ errors retrying can be enabled by initialization
398
+ # parameter 'retry_post_requests'. The total number of retries cumulatively
399
+ # should not exceed 10 minutes / 600 seconds
400
+ response = nil
401
+ begin # this block is to allow retry (redo) in the begin part of the begin-rescue block
402
+ begin
403
+ response = client.post(target, params || {}, header)
404
+
405
+ # if the HTTP error code is 500 or higher and the user requested retrying
406
+ # on post request, attempt a retry
407
+ status = response.code.to_i
408
+ if @retry_post_requests && status >= 500 && cumul_retry_delay < @max_cumul_retry_delay
409
+ $stderr.puts "Error #{status}: #{get_error(response)}. Retrying after #{retry_delay} seconds..."
410
+ sleep retry_delay
411
+ cumul_retry_delay += retry_delay
412
+ retry_delay *= 2
413
+ redo # restart from beginning of do-while loop
414
+ end
415
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError, OpenSSL::SSL::SSLError, SocketError => e
416
+ $stderr.print "#{e.class}: #{e.message}. "
417
+ if @retry_post_requests && cumul_retry_delay < @max_cumul_retry_delay
418
+ $stderr.puts "Retrying after #{retry_delay} seconds..."
419
+ sleep retry_delay
420
+ cumul_retry_delay += retry_delay
421
+ retry_delay *= 2
422
+ retry
423
+ else
424
+ if @retry_post_requests
425
+ $stderr.puts "Retrying stopped after #{@max_cumul_retry_delay} seconds."
426
+ else
427
+ $stderr.puts ""
428
+ end
429
+ raise e
430
+ end
431
+ rescue => e
432
+ raise e
433
+ end
434
+ end while false
435
+
436
+ body = block ? response.body : inflate_body(response)
437
+
438
+ begin
439
+ unless ENV['TD_CLIENT_DEBUG'].nil?
440
+ puts "DEBUG: REST POST response:"
441
+ puts "DEBUG: header: " + response.header.to_s
442
+ puts "DEBUG: status: " + response.code.to_s
443
+ puts "DEBUG: body: <omitted>"
444
+ end
445
+ return [response.code.to_s, body, response]
446
+ ensure
447
+ # Disconnect keep-alive connection explicitly here, not by GC.
448
+ client.reset(target) rescue nil
449
+ end
450
+ end
451
+
452
+ # @param [String] url
453
+ # @param [String, StringIO] stream
454
+ # @param [Fixnum] size
455
+ # @param [Hash] opts
456
+ def put(url, stream, size, opts = {})
457
+ client, header = new_client(opts)
458
+ client.send_timeout = @send_timeout
459
+ client.receive_timeout = @read_timeout
460
+
461
+ header['Content-Type'] = 'application/octet-stream'
462
+ header['Content-Length'] = size.to_s
463
+
464
+ body = if stream.class.name == 'StringIO'
465
+ stream.string
466
+ else
467
+ stream
468
+ end
469
+ target = build_endpoint(url, opts[:host] || @host)
470
+
471
+ unless ENV['TD_CLIENT_DEBUG'].nil?
472
+ puts "DEBUG: REST PUT call:"
473
+ puts "DEBUG: header: " + header.to_s
474
+ puts "DEBUG: target: " + target.to_s
475
+ puts "DEBUG: body: " + body.to_s
476
+ end
477
+
478
+ response = client.put(target, body, header)
479
+ begin
480
+ unless ENV['TD_CLIENT_DEBUG'].nil?
481
+ puts "DEBUG: REST PUT response:"
482
+ puts "DEBUG: header: " + response.header.to_s
483
+ puts "DEBUG: status: " + response.code.to_s
484
+ puts "DEBUG: body: <omitted>"
485
+ end
486
+ return [response.code.to_s, response.body, response]
487
+ ensure
488
+ # Disconnect keep-alive connection explicitly here, not by GC.
489
+ client.reset(target) rescue nil
490
+ end
491
+ end
492
+
493
+ # @param [String] url
494
+ # @param [String] host
495
+ # @return [String]
496
+ def build_endpoint(url, host)
497
+ schema = @ssl ? 'https' : 'http'
498
+ "#{schema}://#{host}:#{@port}#{@base_path + url}"
499
+ end
500
+
501
+ # @yield Disable SSLv3 in given block
502
+ def guard_no_sslv3
503
+ key = :SET_SSL_OP_NO_SSLv3
504
+ backup = Thread.current[key]
505
+ begin
506
+ # Disable SSLv3 connection: See Net::HTTP hack at the bottom
507
+ Thread.current[key] = true
508
+ yield
509
+ ensure
510
+ # backup could be nil, but assigning nil to TLS means 'delete'
511
+ Thread.current[key] = backup
512
+ end
513
+ end
514
+
515
+ # @param [Hash] opts
516
+ # @return [HTTPClient, Hash]
517
+ def new_client(opts = {})
518
+ client = HTTPClient.new(@http_proxy, @user_agent)
519
+ client.connect_timeout = @connect_timeout
520
+
521
+ if @ssl
522
+ client.ssl_config.add_trust_ca(ssl_ca_file)
523
+ client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_PEER
524
+ # Disable SSLv3 connection in favor of POODLE Attack protection
525
+ client.ssl_config.options |= OpenSSL::SSL::OP_NO_SSLv3
526
+ end
527
+
528
+ header = {}
529
+ if @apikey
530
+ header['Authorization'] = "TD1 #{apikey}"
531
+ end
532
+ header['Date'] = Time.now.rfc2822
533
+
534
+ header.merge!(@headers)
535
+
536
+ return client, header
537
+ end
538
+
539
+ # @return [String]
540
+ def api_client(endpoint)
541
+ header = {}.merge(@headers)
542
+ header['Authorization'] = "TD1 #{apikey}" if @apikey
543
+ header['Content-Type'] = 'application/json; charset=utf-8'
544
+ client = HTTPClient.new(:proxy => @http_proxy, :agent_name => @user_agent, :base_url => endpoint, :default_header => header)
545
+ client.connect_timeout = @connect_timeout
546
+ client.send_timeout = @send_timeout
547
+ client.receive_timeout = @read_timeout
548
+ client.transparent_gzip_decompression = true
549
+ client.debug_dev = STDOUT unless ENV['TD_CLIENT_DEBUG'].nil?
550
+ client
551
+ end
552
+
553
+ def api(opt = {:retry_request => true}, &block)
554
+ retry_request = opt[:retry_request]
555
+ # up to 7 retries with exponential (base 2) back-off starting at 'retry_delay'
556
+ retry_delay = @retry_delay
557
+ retry_times = 0
558
+ cumul_retry_delay = 0
559
+
560
+ # for both exceptions and 500+ errors retrying can be enabled by initialization
561
+ # parameter 'retry_post_requests'. The total number of retries cumulatively
562
+ # should not exceed 10 minutes / 600 seconds
563
+ begin # this block is to allow retry (redo) in the begin part of the begin-rescue block
564
+ begin
565
+ response = @api.instance_eval &block
566
+
567
+ # if the HTTP error code is 500 or higher and the user requested retrying
568
+ # on post request, attempt a retry
569
+ status = response.code.to_i
570
+ if retry_request && status >= 500 && cumul_retry_delay < @max_cumul_retry_delay
571
+ $stderr.puts "Error #{status}: #{get_error(response)}. Retrying after #{retry_delay} seconds..."
572
+ sleep retry_delay
573
+ cumul_retry_delay += retry_delay
574
+ retry_delay *= 2
575
+ redo # restart from beginning of do-while loop
576
+ end
577
+ return response
578
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError, OpenSSL::SSL::SSLError, SocketError => e
579
+ $stderr.print "#{e.class}: #{e.message}. "
580
+ if retry_request
581
+ if cumul_retry_delay < @max_cumul_retry_delay
582
+ $stderr.puts "Retrying after #{retry_delay} seconds..."
583
+ sleep retry_delay
584
+ cumul_retry_delay += retry_delay
585
+ retry_delay *= 2
586
+ retry_times += 1
587
+ retry
588
+ else
589
+ $stderr.puts "Retrying stopped after #{@max_cumul_retry_delay} seconds."
590
+ e.message << " (Retried #{retry_times} times in #{cumul_retry_delay} seconds)"
591
+ end
592
+ else
593
+ $stderr.puts "No retry should be performed."
594
+ end
595
+ raise e
596
+ end
597
+ end while false
598
+ end
599
+
600
+ def ssl_ca_file
601
+ @ssl_ca_file ||= File.join(File.dirname(__FILE__), '..', '..', '..', 'data', 'ca-bundle.crt')
602
+ end
603
+
604
+ # @param [response] res
605
+ # @return [String]
606
+ def get_error(res)
607
+ parse_error_response(res)['message']
608
+ end
609
+
610
+ def parse_error_response(res)
611
+ begin
612
+ error = JSON.load(res.body)
613
+ if error
614
+ error['message'] = error['error'] unless error['message']
615
+ else
616
+ error = {'message' => res.reason}
617
+ end
618
+ rescue JSON::ParserError
619
+ error = {'message' => res.body}
620
+ end
621
+
622
+ error
623
+ end
624
+
625
+ # @param [String] msg
626
+ # @param [response] res
627
+ # @param [Class] klass
628
+ def raise_error(msg, res, klass=nil)
629
+ status_code = res.code.to_s
630
+ error = parse_error_response(res)
631
+ message = "#{msg}: #{error['message']}"
632
+
633
+ error_class = if klass
634
+ message = "#{status_code}: #{message}"
635
+ klass
636
+ else
637
+ case status_code
638
+ when "404"
639
+ NotFoundError
640
+ when "409"
641
+ message = "#{message}: conflicts_with job:#{error["details"]["conflicts_with"]}" if error["details"] && error["details"]["conflicts_with"]
642
+ AlreadyExistsError
643
+ when "401"
644
+ AuthError
645
+ when "403"
646
+ ForbiddenError
647
+ else
648
+ message = "#{status_code}: #{message}"
649
+ APIError
650
+ end
651
+ end
652
+
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'])
658
+ else
659
+ exc = error_class.new(message)
660
+ end
661
+ raise exc
662
+ end
663
+
664
+ if ''.respond_to?(:encode)
665
+ # @param [String] s
666
+ # @return [String]
667
+ # * ' ' and '+' must be escaped into %20 and %2B because escaped text may be
668
+ # used as both URI and query.
669
+ # * '.' must be escaped as %2E because it may be cunfused with extension.
670
+ def e(s)
671
+ s = s.to_s.encode(Encoding::UTF_8).force_encoding(Encoding::ASCII_8BIT)
672
+ s.gsub!(/[^\-_!~*'()~0-9A-Z_a-z]/){|x|'%%%02X' % x.ord}
673
+ s
674
+ end
675
+ else
676
+ # @param [String] s
677
+ # @return [String]
678
+ # * ' ' and '+' must be escaped into %20 and %2B because escaped text may be
679
+ # used as both URI and query.
680
+ # * '.' must be escaped as %2E because it may be cunfused with extension.
681
+ def e(s)
682
+ s.to_s.gsub(/[^\-_!~*'()~0-9A-Z_a-z]/){|x|'%%%02X' % x.ord}
683
+ end
684
+ end
685
+
686
+ # @param [String] body
687
+ # @param [Array] required
688
+ def checked_json(body, required = [])
689
+ js = nil
690
+ begin
691
+ js = JSON.load(body)
692
+ rescue
693
+ raise "Unexpected API response: #{$!}"
694
+ end
695
+ unless js.is_a?(Hash)
696
+ raise "Unexpected API response: #{body}"
697
+ end
698
+ required.each {|k|
699
+ unless js[k]
700
+ raise "Unexpected API response: #{body}"
701
+ end
702
+ }
703
+ js
704
+ end
705
+ end
706
+
707
+ end # module TreasureData