ecoportal-api 0.10.2 → 0.10.4
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/CHANGELOG.md +24 -1
- data/ecoportal-api.gemspec +4 -3
- data/lib/ecoportal/api/common/batch_operation.rb +19 -12
- data/lib/ecoportal/api/common/client/elastic_apm_integration.rb +111 -0
- data/lib/ecoportal/api/common/client/error/checks.rb +39 -0
- data/lib/ecoportal/api/common/client/error.rb +17 -0
- data/lib/ecoportal/api/common/client/rate_throttling.rb +43 -0
- data/lib/ecoportal/api/common/client/throughput/stats.rb +119 -0
- data/lib/ecoportal/api/common/client/throughput.rb +93 -0
- data/lib/ecoportal/api/common/client/time_out.rb +70 -0
- data/lib/ecoportal/api/common/client/with_retry.rb +86 -0
- data/lib/ecoportal/api/common/client.rb +53 -78
- data/lib/ecoportal/api/common.rb +0 -2
- data/lib/ecoportal/api/v1/job/awaiter.rb +115 -0
- data/lib/ecoportal/api/v1/job/status.rb +42 -0
- data/lib/ecoportal/api/v1/job.rb +131 -0
- data/lib/ecoportal/api/v1/people.rb +6 -83
- data/lib/ecoportal/api/v1.rb +0 -1
- data/lib/ecoportal/api/version.rb +1 -1
- metadata +27 -5
- data/lib/ecoportal/api/common/elastic_apm_integration.rb +0 -112
- data/lib/ecoportal/api/common/time_out.rb +0 -26
- data/lib/ecoportal/api/v1/job_status.rb +0 -33
@@ -0,0 +1,86 @@
|
|
1
|
+
module Ecoportal
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
class Client
|
5
|
+
module WithRetry
|
6
|
+
DELAY_REQUEST_RETRY = 5
|
7
|
+
RETRY_ATTEMPTS = 5
|
8
|
+
HANDLED_CONNECTION_ERRORS = [
|
9
|
+
HTTP::ConnectionError,
|
10
|
+
IOError
|
11
|
+
].freeze
|
12
|
+
|
13
|
+
include Ecoportal::API::Common::Client::ElasticApmIntegration
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# Helper to ensure unexpected server errors do not bring
|
18
|
+
# client scripts immediately down
|
19
|
+
# @note it manages limited range of errors, the rest
|
20
|
+
# are not handled.
|
21
|
+
def with_retry(
|
22
|
+
attempts = retry_attemps,
|
23
|
+
delay = delay_request_retry,
|
24
|
+
error_safe: true,
|
25
|
+
&block
|
26
|
+
)
|
27
|
+
response = nil
|
28
|
+
|
29
|
+
attempts.times do |i|
|
30
|
+
remaining = attempts - i - 1
|
31
|
+
|
32
|
+
response = with_connection_error_handling(
|
33
|
+
remaining,
|
34
|
+
error_safe: error_safe,
|
35
|
+
callback: block
|
36
|
+
) do
|
37
|
+
block.call
|
38
|
+
end
|
39
|
+
|
40
|
+
return response unless some_unexpected_error?(response)
|
41
|
+
|
42
|
+
# handle server errors (5xx) & server bugs (i.e. empty body)
|
43
|
+
msg = "re-attempting (remaining: "
|
44
|
+
msg << "#{remaining} attempts out of #{attempts})"
|
45
|
+
log(:debug) { msg }
|
46
|
+
|
47
|
+
log_unexpected_server_error(response)
|
48
|
+
|
49
|
+
msg = "Got server error (#{response.status}): #{response.body}\n"
|
50
|
+
msg << "Going to retry (##{i} of #{attempts})"
|
51
|
+
log(:debug) { msg }
|
52
|
+
|
53
|
+
sleep(delay) if i < attempts
|
54
|
+
end
|
55
|
+
|
56
|
+
response
|
57
|
+
end
|
58
|
+
|
59
|
+
def with_connection_error_handling(remaining, callback:, error_safe: true)
|
60
|
+
yield
|
61
|
+
rescue *handled_connection_errors => err
|
62
|
+
raise unless error_safe && remaining.positive?
|
63
|
+
|
64
|
+
msg = "Got #{err.class}: #{err.message}"
|
65
|
+
log(:debug) { msg }
|
66
|
+
|
67
|
+
with_retry(remaining, error_safe: error_safe, &callback)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Add here other connection errors
|
71
|
+
def handled_connection_errors
|
72
|
+
self.class::HANDLED_CONNECTION_ERRORS
|
73
|
+
end
|
74
|
+
|
75
|
+
def retry_attemps
|
76
|
+
self.class::RETRY_ATTEMPTS
|
77
|
+
end
|
78
|
+
|
79
|
+
def delay_request_retry
|
80
|
+
self.class::DELAY_REQUEST_RETRY
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -1,4 +1,12 @@
|
|
1
1
|
require 'http'
|
2
|
+
|
3
|
+
require 'ecoportal/api/common/client/error'
|
4
|
+
require 'ecoportal/api/common/client/elastic_apm_integration'
|
5
|
+
require 'ecoportal/api/common/client/rate_throttling'
|
6
|
+
require 'ecoportal/api/common/client/throughput'
|
7
|
+
require 'ecoportal/api/common/client/time_out'
|
8
|
+
require 'ecoportal/api/common/client/with_retry'
|
9
|
+
|
2
10
|
module Ecoportal
|
3
11
|
module API
|
4
12
|
module Common
|
@@ -16,9 +24,10 @@ module Ecoportal
|
|
16
24
|
# @attr_reader logger [Logger] the logger.
|
17
25
|
# @attr_reader host [String] the remote target server.
|
18
26
|
class Client
|
19
|
-
include
|
20
|
-
|
21
|
-
|
27
|
+
include WithRetry
|
28
|
+
include RateThrottling
|
29
|
+
|
30
|
+
DEFAULT_HOST = 'live.ecoportal.com'.freeze
|
22
31
|
|
23
32
|
attr_accessor :logger
|
24
33
|
attr_reader :host
|
@@ -28,40 +37,32 @@ module Ecoportal
|
|
28
37
|
# @param version [String] it is part of the base url and will determine the api version we query against.
|
29
38
|
# @param host [String] api server domain.
|
30
39
|
# @param logger [Logger] an object with `Logger` interface to generate logs.
|
31
|
-
# @param
|
40
|
+
# @param deep_logging [Boolean] whether or not batch responses should be logged
|
32
41
|
# @return [Client] an object that holds the configuration of the api connection.
|
33
|
-
def initialize(api_key:, version: "v1", host:
|
34
|
-
@version
|
35
|
-
@api_key
|
36
|
-
@logger
|
37
|
-
@host
|
38
|
-
@
|
42
|
+
def initialize(api_key:, version: "v1", host: DEFAULT_HOST, logger: nil, deep_logging: false)
|
43
|
+
@version = version
|
44
|
+
@api_key = api_key
|
45
|
+
@logger = logger
|
46
|
+
@host = host
|
47
|
+
@deep_logging = deep_logging
|
39
48
|
|
40
49
|
if host.match(/^localhost|^127\.0\.0\.1/)
|
41
50
|
@base_uri = "http://#{host}/api/"
|
42
51
|
else
|
43
52
|
@base_uri = "https://#{host}/api/"
|
44
53
|
end
|
45
|
-
|
54
|
+
|
55
|
+
if deep_logging?
|
56
|
+
log(:debug) {
|
57
|
+
"#{version} client initialized pointing at #{host}"
|
58
|
+
}
|
59
|
+
end
|
46
60
|
|
47
61
|
return unless @api_key.nil? || @api_key.match(/\A\W*\z/)
|
48
62
|
|
49
63
|
log(:error) { "Api-key missing!" }
|
50
64
|
end
|
51
65
|
|
52
|
-
# Logger interface.
|
53
|
-
# @example:
|
54
|
-
# log(:info) {"General information on what's going on"}
|
55
|
-
# log(:warn) {"This is a warning that something is likely to have gone amiss"}
|
56
|
-
# log(:error) {"Something went wrong"}
|
57
|
-
# log(:fatal) {"An unrecoverable error has happend"}
|
58
|
-
# @param level [Symbol] the level that the message should be logged.
|
59
|
-
# @yield [] generates the message.
|
60
|
-
# @yieldreturn [String] the generated message.
|
61
|
-
def log(level, &block)
|
62
|
-
logger&.send(level, &block)
|
63
|
-
end
|
64
|
-
|
65
66
|
# Sends an http `GET` request against the api version using `path` to complete the base url,
|
66
67
|
# and adding the key_value pairs of `params` in the http _header_.
|
67
68
|
# @param path [String] the tail that completes the url of the request.
|
@@ -152,7 +153,7 @@ module Ecoportal
|
|
152
153
|
# @param path [String] the tail that completes the url of the request.
|
153
154
|
# @return [String] the final url.
|
154
155
|
def url_for(path)
|
155
|
-
@base_uri
|
156
|
+
"#{@base_uri}#{@version}#{path}"
|
156
157
|
end
|
157
158
|
|
158
159
|
private
|
@@ -161,71 +162,45 @@ module Ecoportal
|
|
161
162
|
raise "Expected block" unless block_given?
|
162
163
|
|
163
164
|
start_time = Time.now.to_f
|
164
|
-
log(:info) { "#{method} #{url_for(path)}" }
|
165
|
-
log(:debug) { "Data: #{JSON.pretty_generate(data)}" }
|
166
165
|
|
167
|
-
|
166
|
+
if deep_logging?
|
167
|
+
log(:debug) { "#{method} #{url_for(path)}" }
|
168
|
+
log(:debug) { "Data: #{JSON.pretty_generate(data)}" }
|
169
|
+
end
|
170
|
+
|
171
|
+
with_retry do
|
172
|
+
rate_throttling(&block)
|
173
|
+
end.tap do |result|
|
174
|
+
next unless deep_logging?
|
175
|
+
|
168
176
|
end_time = Time.now.to_f
|
169
|
-
|
177
|
+
|
178
|
+
log(:debug) {
|
170
179
|
"Took %.2fs, Status #{result.status}" % (end_time - start_time) # rubocop:disable Style/FormatString
|
171
180
|
}
|
172
181
|
|
173
|
-
|
174
|
-
|
175
|
-
log(result.success?? :debug : :warn) {
|
182
|
+
log(:debug) {
|
176
183
|
"Response: #{JSON.pretty_generate(result.body)}"
|
177
184
|
}
|
178
185
|
end
|
179
186
|
end
|
180
187
|
|
181
|
-
|
182
|
-
|
183
|
-
response = nil
|
184
|
-
attempts.times do |i|
|
185
|
-
remaining = attempts - i - 1
|
186
|
-
|
187
|
-
begin
|
188
|
-
response = block.call
|
189
|
-
rescue HTTP::ConnectionError => e
|
190
|
-
raise unless error_safe && remaining.positive?
|
191
|
-
log(:error) { "Got connection error: #{e.message}" }
|
192
|
-
response = with_retry(remaining, error_safe: error_safe, &block)
|
193
|
-
rescue IOError => e
|
194
|
-
raise unless error_safe && remaining.positive?
|
195
|
-
log(:error) { "Got IO error: #{e.message}" }
|
196
|
-
response = with_retry(remaining, error_safe: error_safe, &block)
|
197
|
-
end
|
198
|
-
|
199
|
-
return response unless some_unexpected_error?(response)
|
200
|
-
|
201
|
-
puts "re-attempting (remaining: #{remaining} attempts out of #{attempts})"
|
202
|
-
|
203
|
-
log_unexpected_server_error(response)
|
204
|
-
|
205
|
-
msg = "Got server error (#{response.status}): #{response.body}\n"
|
206
|
-
msg += "Going to retry (#{i} out of #{attempts})"
|
207
|
-
log(:error) { msg }
|
208
|
-
|
209
|
-
sleep(delay) if i < attempts
|
210
|
-
end
|
211
|
-
response
|
212
|
-
end
|
213
|
-
|
214
|
-
# Sometimes response body is wrong but status code
|
215
|
-
# doesn't reflect. Let it retry
|
216
|
-
def some_unexpected_error?(response)
|
217
|
-
unexpected_server_error?(response.status) ||
|
218
|
-
unexpected_body?(response)
|
188
|
+
def deep_logging?
|
189
|
+
@deep_logging
|
219
190
|
end
|
220
191
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
192
|
+
# Logger interface.
|
193
|
+
# @example:
|
194
|
+
# log(:info) {"General information on what's going on"}
|
195
|
+
# log(:warn) {"This is a warning that something is likely to have gone amiss"}
|
196
|
+
# log(:error) {"Something went wrong"}
|
197
|
+
# log(:fatal) {"An unrecoverable error has happend"}
|
198
|
+
# @param level [Symbol] the level that the message should be logged.
|
199
|
+
# @yield [] generates the message.
|
200
|
+
# @yieldreturn [String] the generated message.
|
201
|
+
def log(level, &block)
|
202
|
+
puts "(#{level}) #{yield}"
|
203
|
+
logger&.send(level, &block)
|
229
204
|
end
|
230
205
|
end
|
231
206
|
end
|
data/lib/ecoportal/api/common.rb
CHANGED
@@ -10,8 +10,6 @@ require 'ecoportal/api/common/hash_diff'
|
|
10
10
|
require 'ecoportal/api/common/base_model'
|
11
11
|
require 'ecoportal/api/common/doc_helpers'
|
12
12
|
require 'ecoportal/api/common/logging'
|
13
|
-
require 'ecoportal/api/common/elastic_apm_integration'
|
14
|
-
require 'ecoportal/api/common/time_out'
|
15
13
|
require 'ecoportal/api/common/client'
|
16
14
|
require 'ecoportal/api/common/response'
|
17
15
|
require 'ecoportal/api/common/wrapped_response'
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Ecoportal
|
2
|
+
module API
|
3
|
+
class V1
|
4
|
+
class Job
|
5
|
+
class Awaiter
|
6
|
+
include Common::Client::TimeOut
|
7
|
+
|
8
|
+
DELAY_STATUS_CHECK = 4
|
9
|
+
MIN_STATUS_CHECK = 2
|
10
|
+
|
11
|
+
TIMEOUT_APPROACH = :conservative # adaptative timeout
|
12
|
+
TIMEOUT_FALLBACK = :min
|
13
|
+
|
14
|
+
attr_reader :job, :job_id, :total
|
15
|
+
attr_accessor :timeout_approach
|
16
|
+
|
17
|
+
def initialize(job, job_id:, total:)
|
18
|
+
@job = job
|
19
|
+
@job_id = job_id
|
20
|
+
@total = total
|
21
|
+
@checked = false
|
22
|
+
self.timeout_approach = self.class::TIMEOUT_APPROACH
|
23
|
+
end
|
24
|
+
|
25
|
+
# Allows to preserve the learned throughput
|
26
|
+
def new(**kargs)
|
27
|
+
self.class.new(job, **kargs).tap do |out|
|
28
|
+
out.throughput = throughput
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def await_completion! # rubocop:disable Metrics/AbcSize
|
33
|
+
max_timeout = timeout_for(total, approach: timeout_approach)
|
34
|
+
|
35
|
+
# timeout library is evil. So we make poor-man timeout.
|
36
|
+
# https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
|
37
|
+
before = Time.now
|
38
|
+
delay_status_check = nil
|
39
|
+
|
40
|
+
loop do
|
41
|
+
status = job.status(job_id)
|
42
|
+
waited = Time.now - before
|
43
|
+
|
44
|
+
adapted = waited
|
45
|
+
adapted = waited - (delay_status_check / 2) if delay_status_check
|
46
|
+
ratio = throughput!(adapted, count: status.progress)
|
47
|
+
|
48
|
+
break status if status.complete?(total)
|
49
|
+
|
50
|
+
pending = status.pending(total)
|
51
|
+
left = max_timeout - waited
|
52
|
+
|
53
|
+
timeout!(status, timeout: max_timeout) unless left.positive?
|
54
|
+
|
55
|
+
delay_status_check = status_check_in(pending, timeout_in: left)
|
56
|
+
|
57
|
+
msg = " ... Awaiting #{delay_status_check} s. -- "
|
58
|
+
msg << " TimeOut: #{left.round(2)} s. "
|
59
|
+
msg << "(job '#{job_id}') "
|
60
|
+
msg << "Done: #{status.progress} (est. #{ratio} rec/s) "
|
61
|
+
msg << " \r"
|
62
|
+
|
63
|
+
print msg
|
64
|
+
$stdout.flush
|
65
|
+
|
66
|
+
sleep(delay_status_check)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def timeout!(status, timeout:)
|
73
|
+
self.timeout_approach = self.class::TIMEOUT_FALLBACK
|
74
|
+
|
75
|
+
msg = "Job '#{job_id}' not complete (size: #{total}).\n"
|
76
|
+
msg << " Timed out after #{timeout} seconds.\n"
|
77
|
+
msg << " Current status: #{status}"
|
78
|
+
|
79
|
+
raise API::Errors::TimeOut, msg
|
80
|
+
end
|
81
|
+
|
82
|
+
def checked?
|
83
|
+
@checked
|
84
|
+
end
|
85
|
+
|
86
|
+
def status_check_in(pending, timeout_in:)
|
87
|
+
unless checked?
|
88
|
+
@checked = true
|
89
|
+
return min_delay_status_check
|
90
|
+
end
|
91
|
+
|
92
|
+
return default_delay_status_check if around_min_throughput?
|
93
|
+
|
94
|
+
eta = eta_for(pending, approach: :optimistic)
|
95
|
+
check_in_max = [eta, timeout_in].min * 0.90
|
96
|
+
|
97
|
+
check_in_best = check_in_max / 2.0
|
98
|
+
default_5 = default_delay_status_check * 5
|
99
|
+
|
100
|
+
top_check = [default_5, check_in_best].min.ceil
|
101
|
+
[top_check, min_delay_status_check].max
|
102
|
+
end
|
103
|
+
|
104
|
+
def default_delay_status_check
|
105
|
+
self.class::DELAY_STATUS_CHECK
|
106
|
+
end
|
107
|
+
|
108
|
+
def min_delay_status_check
|
109
|
+
self.class::MIN_STATUS_CHECK
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Ecoportal
|
2
|
+
module API
|
3
|
+
class V1
|
4
|
+
class Job
|
5
|
+
class Status
|
6
|
+
attr_reader :id, :progress
|
7
|
+
|
8
|
+
def initialize(id, complete, errored, progress)
|
9
|
+
@id = id
|
10
|
+
@complete = complete
|
11
|
+
@errored = errored
|
12
|
+
@progress = progress
|
13
|
+
end
|
14
|
+
|
15
|
+
def complete?(total = nil)
|
16
|
+
return @complete if total.nil?
|
17
|
+
|
18
|
+
progress >= total
|
19
|
+
end
|
20
|
+
|
21
|
+
def pending(total)
|
22
|
+
return 1 unless total
|
23
|
+
return 0 if total <= progress
|
24
|
+
|
25
|
+
total - progress
|
26
|
+
end
|
27
|
+
|
28
|
+
def errored?
|
29
|
+
@errored
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
msg = complete? ? "Completed" : "In progress"
|
34
|
+
msg = "Errored" if errored?
|
35
|
+
msg << " with #{progress} done."
|
36
|
+
msg
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'ecoportal/api/v1/job/status'
|
2
|
+
require 'ecoportal/api/v1/job/awaiter'
|
3
|
+
|
4
|
+
module Ecoportal
|
5
|
+
module API
|
6
|
+
class V1
|
7
|
+
class Job
|
8
|
+
attr_reader :client, :person_class
|
9
|
+
|
10
|
+
# @param client [Common::Client] a `Common::Client` object that holds the configuration of the api connection.
|
11
|
+
# @return [People] an instance object ready to make people api requests.
|
12
|
+
def initialize(client, person_class:)
|
13
|
+
@client = client
|
14
|
+
@person_class = person_class
|
15
|
+
@created = false
|
16
|
+
end
|
17
|
+
|
18
|
+
# Allows to preserve the learned througoutput
|
19
|
+
def new
|
20
|
+
self.class.new(client, person_class: person_class).tap do |out|
|
21
|
+
out.awaiter = @awaiter
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def created?
|
26
|
+
@created
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Ecoportal::API::Common::Response] the results of the batch job
|
30
|
+
def batch(recover: false)
|
31
|
+
raise_if_already_launched! unless recover
|
32
|
+
|
33
|
+
@operation ||= Common::BatchOperation.new(
|
34
|
+
"/people",
|
35
|
+
person_class,
|
36
|
+
logger: client.logger
|
37
|
+
)
|
38
|
+
|
39
|
+
yield operation unless recover
|
40
|
+
|
41
|
+
total = operation.count
|
42
|
+
job_id = create(operation, recover: recover)
|
43
|
+
stat = awaiter(
|
44
|
+
job_id: job_id,
|
45
|
+
total: total
|
46
|
+
).await_completion!
|
47
|
+
|
48
|
+
job_result(job_id, operation) if stat&.complete?(total)
|
49
|
+
end
|
50
|
+
|
51
|
+
def status(job_id)
|
52
|
+
response = client.get("/people/job/#{CGI.escape(job_id)}/status")
|
53
|
+
body = response && body_data(response.body)
|
54
|
+
|
55
|
+
msg = "Status error (#{response.status}) - "
|
56
|
+
msg << "Errors: #{body}"
|
57
|
+
raise msg unless response.success?
|
58
|
+
|
59
|
+
Status.new(*body.values_at(*%w[id complete errored progress]))
|
60
|
+
end
|
61
|
+
|
62
|
+
protected
|
63
|
+
|
64
|
+
attr_writer :awaiter
|
65
|
+
|
66
|
+
def awaiter(job_id:, total:, preserve: true)
|
67
|
+
return @awaiter = Awaiter.new(self, job_id: job_id, total: total) unless preserve && @awaiter
|
68
|
+
|
69
|
+
@awaiter = @awaiter.new(job_id: job_id, total: total)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
attr_reader :operation
|
75
|
+
|
76
|
+
def operations
|
77
|
+
@operations ||= {}
|
78
|
+
end
|
79
|
+
|
80
|
+
def job_operation(job_id)
|
81
|
+
operations[job_id]
|
82
|
+
end
|
83
|
+
|
84
|
+
def raise_if_already_launched!
|
85
|
+
return unless created?
|
86
|
+
|
87
|
+
msg = "Missusage: job was already created."
|
88
|
+
msg << " Can't call batch more than once"
|
89
|
+
raise msg
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Ecoportal::API::Common::Response] the results of the batch job
|
93
|
+
def job_result(job_id, operation)
|
94
|
+
client.get("/people/job/#{CGI.escape(job_id)}").tap do |response|
|
95
|
+
operation.process_response(response)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [String] the `id` of the created batch job
|
100
|
+
def create(operation, recover: false)
|
101
|
+
raise_if_already_launched! unless recover
|
102
|
+
|
103
|
+
job_id = nil
|
104
|
+
|
105
|
+
client.post("/people/job", data: operation.as_json).tap do |response|
|
106
|
+
job_id = body_data(response.body)["id"] if response.success?
|
107
|
+
|
108
|
+
next if job_id
|
109
|
+
|
110
|
+
msg = "Could not create job - Error (#{response.status}): "
|
111
|
+
msg << body_data(response.body).to_s
|
112
|
+
raise msg
|
113
|
+
end
|
114
|
+
|
115
|
+
if job_id
|
116
|
+
@created = true
|
117
|
+
operations[job_id] = operation
|
118
|
+
end
|
119
|
+
|
120
|
+
job_id
|
121
|
+
end
|
122
|
+
|
123
|
+
# Hook for other api versions to obtain the raw data of a response
|
124
|
+
# @note this was introduced to allow `v2` to reuse this class
|
125
|
+
def body_data(body)
|
126
|
+
body
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -6,11 +6,8 @@ module Ecoportal
|
|
6
6
|
class People
|
7
7
|
extend Common::BaseClass
|
8
8
|
include Common::DocHelpers
|
9
|
-
include Common::TimeOut
|
10
9
|
include Enumerable
|
11
10
|
|
12
|
-
DELAY_STATUS_CHECK = 5
|
13
|
-
|
14
11
|
class_resolver :person_class, "Ecoportal::API::V1::Person"
|
15
12
|
|
16
13
|
attr_reader :client
|
@@ -143,7 +140,7 @@ module Ecoportal
|
|
143
140
|
# @param job_mode [Boolean] whether or not it should use batch jobs
|
144
141
|
# @return [Ecoportal::API::Common::Response] the results of the batch
|
145
142
|
def batch(job_mode: true, &block)
|
146
|
-
return job(&block) if job_mode
|
143
|
+
return job.batch(&block) if job_mode
|
147
144
|
|
148
145
|
operation = Common::BatchOperation.new(
|
149
146
|
"/people",
|
@@ -159,33 +156,11 @@ module Ecoportal
|
|
159
156
|
end
|
160
157
|
end
|
161
158
|
|
162
|
-
# @return [Ecoportal::API::
|
163
|
-
def job
|
164
|
-
|
165
|
-
"/people",
|
166
|
-
person_class,
|
167
|
-
logger: client.logger
|
168
|
-
)
|
169
|
-
|
170
|
-
yield operation
|
171
|
-
|
172
|
-
total = operation.count
|
173
|
-
timeout = timeout_for(total)
|
174
|
-
|
175
|
-
job_id = create_job(operation)
|
176
|
-
status = wait_for_job_completion(job_id, timeout: timeout, total: total)
|
159
|
+
# @return [Ecoportal::API::V1::Job]
|
160
|
+
def job(preserve_stats: true)
|
161
|
+
return @last_job = Job.new(client, person_class: person_class) unless preserve_stats && @last_job
|
177
162
|
|
178
|
-
|
179
|
-
# if total == status.progress
|
180
|
-
if status&.complete?(total)
|
181
|
-
job_result(job_id, operation)
|
182
|
-
else
|
183
|
-
msg = "Job '#{job_id}' not complete (size: #{total}).\n"
|
184
|
-
msg << " Probably timeout after #{timeout} seconds.\n"
|
185
|
-
msg << " Current status: #{status}"
|
186
|
-
|
187
|
-
raise API::Errors::TimeOut, msg
|
188
|
-
end
|
163
|
+
@last_job = @last_job.new
|
189
164
|
end
|
190
165
|
|
191
166
|
# Creates a new `Person` object.
|
@@ -196,59 +171,6 @@ module Ecoportal
|
|
196
171
|
|
197
172
|
private
|
198
173
|
|
199
|
-
def job_status(job_id)
|
200
|
-
response = client.get("/people/job/#{CGI.escape(job_id)}/status")
|
201
|
-
body = response && body_data(response.body)
|
202
|
-
|
203
|
-
msg = "Status error (#{response.status}) - "
|
204
|
-
msg << "Errors: #{body}"
|
205
|
-
raise msg unless response.success?
|
206
|
-
|
207
|
-
JobStatus.new(*body.values_at(*%w[id complete errored progress]))
|
208
|
-
end
|
209
|
-
|
210
|
-
# @return [Ecoportal::API::Common::Response] the results of the batch job
|
211
|
-
def job_result(job_id, operation)
|
212
|
-
client.get("/people/job/#{CGI.escape(job_id)}").tap do |response|
|
213
|
-
operation.process_response(response)
|
214
|
-
end
|
215
|
-
end
|
216
|
-
|
217
|
-
def wait_for_job_completion(job_id, timeout:, total:)
|
218
|
-
# timeout library is evil. So we make poor-man timeout.
|
219
|
-
# https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
|
220
|
-
before = Time.now
|
221
|
-
|
222
|
-
loop do
|
223
|
-
status = job_status(job_id)
|
224
|
-
break status if status.complete?(total)
|
225
|
-
|
226
|
-
left = (before + timeout) - Time.now
|
227
|
-
break status unless left.positive?
|
228
|
-
# break status if Time.now >= before + timeout
|
229
|
-
|
230
|
-
msg = " ... Await job "
|
231
|
-
msg << "('#{job_id}'; done: #{status.progress}): "
|
232
|
-
msg << "#{left.ceil} sec. \r"
|
233
|
-
|
234
|
-
print msg
|
235
|
-
$stdout.flush
|
236
|
-
|
237
|
-
sleep(DELAY_STATUS_CHECK)
|
238
|
-
status
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
# @return [String] the `id` of the created batch job
|
243
|
-
def create_job(operation)
|
244
|
-
job_id = nil
|
245
|
-
client.post("/people/job", data: operation.as_json).tap do |response|
|
246
|
-
job_id = body_data(response.body)["id"] if response.success?
|
247
|
-
raise "Could not create job - Error (#{response.status}): #{body_data(response.body)}" unless job_id
|
248
|
-
end
|
249
|
-
job_id
|
250
|
-
end
|
251
|
-
|
252
174
|
# Hook for other api versions to obtain the raw data of a response
|
253
175
|
# @note this was introduced to allow `v2` to reuse this class
|
254
176
|
def body_data(body)
|
@@ -262,3 +184,4 @@ end
|
|
262
184
|
require 'ecoportal/api/v1/schema_field_value'
|
263
185
|
require 'ecoportal/api/v1/person_details'
|
264
186
|
require 'ecoportal/api/v1/person'
|
187
|
+
require 'ecoportal/api/v1/job'
|