ecoportal-api 0.10.3 → 0.10.5
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 +23 -1
- data/ecoportal-api.gemspec +4 -3
- 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 +52 -10
- data/lib/ecoportal/api/common/client.rb +9 -2
- data/lib/ecoportal/api/v1/job/awaiter.rb +116 -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 +22 -3
- data/lib/ecoportal/api/v1/job_status.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31b8d7b7f90a351a2335c73140458010669a42bf19538f0ec8a65df565881374
|
4
|
+
data.tar.gz: 064e126df498599971d9c3ea999cba7b85eb303d944ea34738f17490c60c435b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b9020a08208cdc1da174e9addb03bb81e5d484fccc461f45e739edaf32a5a998e1b8508f8fdf3663282733352de1e025f2c4486ebda68109d153c4c1c8326d6b
|
7
|
+
data.tar.gz: 2d4fa049b1cb31edcd28d783a50589922ce04560a2cab5e6a409abc7c3b21ffc807aad6a09a592013eb800b4e3c35e2ecc926d0f7c1e12a061df820e0dc1c37b
|
data/CHANGELOG.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
4
4
|
|
5
|
-
## [0.10.
|
5
|
+
## [0.10.6] - 2024-10-xx
|
6
6
|
|
7
7
|
### Added
|
8
8
|
|
@@ -10,6 +10,28 @@ All notable changes to this project will be documented in this file.
|
|
10
10
|
|
11
11
|
### Fixed
|
12
12
|
|
13
|
+
## [0.10.5] - 2024-10-16
|
14
|
+
|
15
|
+
### Fixed
|
16
|
+
|
17
|
+
- `Ecoportal::API::V1::Job::Awaiter`
|
18
|
+
- Made it calculate TimeOut always on `0.2` throughput
|
19
|
+
|
20
|
+
## [0.10.4] - 2024-10-15
|
21
|
+
|
22
|
+
### Added
|
23
|
+
|
24
|
+
- `Ecoportal::API::Common::Client::RateThrottling`
|
25
|
+
- See: <https://github.com/zombocom/rate_throttle_client?tab=readme-ov-file#ratethrottleclient>
|
26
|
+
- Add adapter to the RateLimit, may it be present.
|
27
|
+
- **Default:** `:proportional_decrease`
|
28
|
+
|
29
|
+
### Changed
|
30
|
+
|
31
|
+
- `Ecoportal::API::Common::Client::TimeOut`
|
32
|
+
- Const `MIN_SIZE` changed from `10` to `5` to speed up small requests.
|
33
|
+
- Full revamp of `Job` endpoint
|
34
|
+
|
13
35
|
## [0.10.3] - 2024-10-01
|
14
36
|
|
15
37
|
### Changed
|
data/ecoportal-api.gemspec
CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
|
|
13
13
|
'oscar@ecoportal.co.nz'
|
14
14
|
]
|
15
15
|
|
16
|
-
spec.summary =
|
16
|
+
spec.summary = "A collection of helpers for interacting with the ecoPortal MS's various APIs"
|
17
17
|
spec.homepage = "https://www.ecoportal.com"
|
18
18
|
spec.licenses = %w[MIT]
|
19
19
|
|
@@ -28,17 +28,18 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
29
29
|
spec.require_paths = ['lib']
|
30
30
|
|
31
|
-
spec.add_development_dependency 'pry'
|
31
|
+
spec.add_development_dependency 'pry', '~> 0.14'
|
32
32
|
spec.add_development_dependency 'rake', '>= 13.0.3', '< 14'
|
33
33
|
spec.add_development_dependency 'redcarpet', '>= 3.6.0', '< 4'
|
34
34
|
spec.add_development_dependency 'rspec', '>= 3.12.0', '< 4'
|
35
35
|
spec.add_development_dependency 'rubocop', '~> 1'
|
36
36
|
spec.add_development_dependency 'rubocop-rake', '~> 0'
|
37
|
-
spec.add_development_dependency 'yard',
|
37
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
38
38
|
|
39
39
|
spec.add_dependency 'dotenv', '~> 3'
|
40
40
|
spec.add_dependency 'elastic-apm', '>= 4.7', "< 5"
|
41
41
|
spec.add_dependency 'http', '~> 5.1', "< 6"
|
42
|
+
spec.add_dependency 'rate_throttle_client', '~> 0.1'
|
42
43
|
end
|
43
44
|
|
44
45
|
# rubocop:enable Gemspec/DevelopmentDependencies
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rate_throttle_client'
|
2
|
+
|
3
|
+
module Ecoportal
|
4
|
+
module API
|
5
|
+
module Common
|
6
|
+
class Client
|
7
|
+
module RateThrottling
|
8
|
+
private
|
9
|
+
|
10
|
+
def rate_throttling(strategy: nil, &block)
|
11
|
+
throttle(strategy).call(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def throttle(strategy = nil)
|
15
|
+
@throttle ||= {}
|
16
|
+
|
17
|
+
return @throttle[strategy] if @throttle.key?(strategy)
|
18
|
+
|
19
|
+
@throttle[strategy] = throttle_class(strategy).new
|
20
|
+
end
|
21
|
+
|
22
|
+
def throttle_class(strategy = default_throttle_strategy)
|
23
|
+
case strategy
|
24
|
+
when :gradual_decrease
|
25
|
+
RateThrottleClient::ExponentialIncreaseGradualDecrease
|
26
|
+
when :proportional_decrease
|
27
|
+
RateThrottleClient::ExponentialIncreaseProportionalDecrease
|
28
|
+
when :remaining_decrease
|
29
|
+
# requires RateLimit-Remaining in the response
|
30
|
+
RateThrottleClient::ExponentialIncreaseProportionalRemainingDecrease
|
31
|
+
else # :backoff
|
32
|
+
RateThrottleClient::ExponentialBackoff
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def default_throttle_strategy
|
37
|
+
:proportional_decrease
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Ecoportal
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
class Client
|
5
|
+
class Throughput
|
6
|
+
class Stats
|
7
|
+
DEFAULT_SD = 1.0
|
8
|
+
DUMMY_DIFF = 0.001
|
9
|
+
CONFIDENCE = {
|
10
|
+
_90: 1645,
|
11
|
+
_95: 1.96,
|
12
|
+
_98: 2.326,
|
13
|
+
_99: 2.576,
|
14
|
+
_995: 2.807
|
15
|
+
}.freeze
|
16
|
+
DEFAULT_CONFI = :_99
|
17
|
+
|
18
|
+
attr_reader :average, :standard_deviation, :count
|
19
|
+
attr_reader :default_margin_error
|
20
|
+
|
21
|
+
def initialize(average, margin: DEFAULT_CONFI)
|
22
|
+
@average = average.to_f
|
23
|
+
@standard_deviation = default_sd
|
24
|
+
@count = 1
|
25
|
+
@default_margin_error = margin || DEFAULT_CONFI
|
26
|
+
end
|
27
|
+
|
28
|
+
def empty?
|
29
|
+
count <= 1
|
30
|
+
end
|
31
|
+
|
32
|
+
def max(value = default_margin_error)
|
33
|
+
confidence_interval(value).max
|
34
|
+
end
|
35
|
+
|
36
|
+
def min(value = default_margin_error)
|
37
|
+
confidence_interval(value).min
|
38
|
+
end
|
39
|
+
|
40
|
+
def record!(value)
|
41
|
+
if count == 1
|
42
|
+
@average = value
|
43
|
+
else
|
44
|
+
@average = ((average * count) + value) / (count + 1)
|
45
|
+
end
|
46
|
+
|
47
|
+
@standard_deviation = new_sd(value)
|
48
|
+
@count += 1
|
49
|
+
average
|
50
|
+
end
|
51
|
+
|
52
|
+
alias_method :<<, :record!
|
53
|
+
|
54
|
+
# @return [Interval]
|
55
|
+
def confidence_interval(value = default_margin_error)
|
56
|
+
me = margin_error(value)
|
57
|
+
pair = [me * -1, me].map {|err| average + err}
|
58
|
+
(pair.first..pair.last)
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def margin_error(value = default_margin_error)
|
64
|
+
to_z(value) * (standard_deviation / Math.sqrt(count))
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def new_sd(value)
|
70
|
+
pre_sum = (sd_per_item ** 2) * count
|
71
|
+
cur_sum = pre_sum + ((value - average) ** 2)
|
72
|
+
Math.sqrt(cur_sum / count)
|
73
|
+
end
|
74
|
+
|
75
|
+
# (diff^2) * N
|
76
|
+
# SD^2 = -------------
|
77
|
+
# N - 1
|
78
|
+
#
|
79
|
+
# (diff^2) * N = SD^2 * (N - 1)
|
80
|
+
#
|
81
|
+
# --------------
|
82
|
+
# diff = 2 / SD^2 * (N - 1)
|
83
|
+
# V -------------
|
84
|
+
# N
|
85
|
+
def sd_per_item
|
86
|
+
base = (sd_for_calcs ** 2) * (count - 1) / count.to_f
|
87
|
+
Math.sqrt(base)
|
88
|
+
end
|
89
|
+
|
90
|
+
def sd_for_calcs
|
91
|
+
return standard_deviation unless empty?
|
92
|
+
|
93
|
+
standard_deviation + dummy_diff
|
94
|
+
end
|
95
|
+
|
96
|
+
def dummy_diff
|
97
|
+
self.class::DUMMY_DIFF
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_z(value)
|
101
|
+
value = default_margin_error unless confidences.key?(value)
|
102
|
+
|
103
|
+
confidences[value]
|
104
|
+
end
|
105
|
+
|
106
|
+
def confidences
|
107
|
+
self.class::CONFIDENCE
|
108
|
+
end
|
109
|
+
|
110
|
+
def default_sd
|
111
|
+
default = self.class::DEFAULT_SD
|
112
|
+
default * average / 10
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'ecoportal/api/common/client/throughput/stats'
|
2
|
+
|
3
|
+
module Ecoportal
|
4
|
+
module API
|
5
|
+
module Common
|
6
|
+
class Client
|
7
|
+
class Throughput
|
8
|
+
MIN_THROUGHPUT = 0.2 # records per second
|
9
|
+
MAX_THROUGHPUT = 12
|
10
|
+
|
11
|
+
attr_reader :default_approach
|
12
|
+
|
13
|
+
def initialize(approach = :conservative)
|
14
|
+
@default_approach = approach || :conservative
|
15
|
+
end
|
16
|
+
|
17
|
+
def eta_for(count, approach: default_approach, ratio: self.ratio(approach))
|
18
|
+
to_seconds(count, ratio: ratio)
|
19
|
+
end
|
20
|
+
|
21
|
+
def ratio(approach = default_approach)
|
22
|
+
case approach
|
23
|
+
when :min
|
24
|
+
min_throughput
|
25
|
+
when :last
|
26
|
+
@last_throughput || stats.average
|
27
|
+
when :average
|
28
|
+
stats.average
|
29
|
+
when :optimistic
|
30
|
+
[stats.max, max_throughput].min
|
31
|
+
else # :conservative
|
32
|
+
[stats.min, min_throughput].max
|
33
|
+
end.then do |relation|
|
34
|
+
next relation if relation < max_throughput
|
35
|
+
|
36
|
+
max_throughput
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def record!(secs = nil, count: nil)
|
41
|
+
return ratio if secs.to_f.zero? || count.to_i.zero?
|
42
|
+
|
43
|
+
(count / secs.to_f).round(3).tap do |last|
|
44
|
+
stats << @last_throughput = last
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Keeps track of the current processing speed
|
49
|
+
# @note it needs to be called in specific spots
|
50
|
+
def push(value)
|
51
|
+
stats.record!(value)
|
52
|
+
end
|
53
|
+
alias_method :<<, :push
|
54
|
+
|
55
|
+
def around_min_throughput?(margin)
|
56
|
+
rate = ratio
|
57
|
+
return true if rate == min_throughput
|
58
|
+
|
59
|
+
right_under = min_throughput * (1 - margin)
|
60
|
+
return false if rate < right_under
|
61
|
+
|
62
|
+
right_above = min_throughput * (1 + margin)
|
63
|
+
return false if rate > right_above
|
64
|
+
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
def stats
|
71
|
+
@stats ||= Stats.new(min_throughput)
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_seconds(count, ratio: min_throughput)
|
75
|
+
return 1 unless count
|
76
|
+
|
77
|
+
(count.ceil / ratio.to_f).ceil
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def min_throughput
|
83
|
+
self.class::MIN_THROUGHPUT
|
84
|
+
end
|
85
|
+
|
86
|
+
def max_throughput
|
87
|
+
self.class::MAX_THROUGHPUT
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -3,23 +3,65 @@ module Ecoportal
|
|
3
3
|
module Common
|
4
4
|
class Client
|
5
5
|
module TimeOut
|
6
|
-
|
7
|
-
MIN_SIZE =
|
6
|
+
TIMEOUT_MARGIN = 0.15 # adaptative timeout with margin
|
7
|
+
MIN_SIZE = 5 # requests
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
attr_writer :throughput
|
12
|
+
|
13
|
+
# Keeps track of the current processing speed
|
14
|
+
# @note it needs to be called in specific spots
|
15
|
+
def throughput!(secs = nil, count: nil)
|
16
|
+
throughput.record!(secs, count: count)
|
17
|
+
end
|
18
|
+
|
19
|
+
def throughput
|
20
|
+
@throughput ||= Throughput.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_count(value)
|
24
|
+
value ||= 0
|
25
|
+
return min_size if value.positive? && value < min_size
|
26
|
+
return value if value.positive?
|
27
|
+
|
28
|
+
1
|
29
|
+
end
|
8
30
|
|
9
31
|
private
|
10
32
|
|
11
|
-
def
|
12
|
-
|
33
|
+
def eta_for(count, approach: :conservative)
|
34
|
+
kargs = {}.tap do |params|
|
35
|
+
next params.merge!({ratio: rectified_throughput}) if approach == :conservative
|
36
|
+
|
37
|
+
params.merge!({approach: approach})
|
38
|
+
end
|
39
|
+
|
40
|
+
throughput.eta_for(to_count(count), **kargs)
|
13
41
|
end
|
14
42
|
|
15
|
-
def
|
16
|
-
|
43
|
+
def timeout_for(count, waited: 0, max_wait: nil, approach: :conservative)
|
44
|
+
(waited + eta_for(count, approach: approach)).then do |time|
|
45
|
+
next time unless max_wait
|
46
|
+
|
47
|
+
[max_wait, time].min
|
48
|
+
end
|
17
49
|
end
|
18
50
|
|
19
|
-
def
|
20
|
-
|
21
|
-
|
22
|
-
|
51
|
+
def rectified_throughput
|
52
|
+
throughput.ratio * (1 - timeout_margin)
|
53
|
+
end
|
54
|
+
|
55
|
+
def around_min_throughput?
|
56
|
+
throughput.around_min_throughput?(timeout_margin)
|
57
|
+
end
|
58
|
+
|
59
|
+
def timeout_margin
|
60
|
+
self.class::TIMEOUT_MARGIN
|
61
|
+
end
|
62
|
+
|
63
|
+
def min_size
|
64
|
+
self.class::MIN_SIZE
|
23
65
|
end
|
24
66
|
end
|
25
67
|
end
|
@@ -2,6 +2,8 @@ require 'http'
|
|
2
2
|
|
3
3
|
require 'ecoportal/api/common/client/error'
|
4
4
|
require 'ecoportal/api/common/client/elastic_apm_integration'
|
5
|
+
require 'ecoportal/api/common/client/rate_throttling'
|
6
|
+
require 'ecoportal/api/common/client/throughput'
|
5
7
|
require 'ecoportal/api/common/client/time_out'
|
6
8
|
require 'ecoportal/api/common/client/with_retry'
|
7
9
|
|
@@ -23,6 +25,9 @@ module Ecoportal
|
|
23
25
|
# @attr_reader host [String] the remote target server.
|
24
26
|
class Client
|
25
27
|
include WithRetry
|
28
|
+
include RateThrottling
|
29
|
+
|
30
|
+
DEFAULT_HOST = 'live.ecoportal.com'.freeze
|
26
31
|
|
27
32
|
attr_accessor :logger
|
28
33
|
attr_reader :host
|
@@ -34,7 +39,7 @@ module Ecoportal
|
|
34
39
|
# @param logger [Logger] an object with `Logger` interface to generate logs.
|
35
40
|
# @param deep_logging [Boolean] whether or not batch responses should be logged
|
36
41
|
# @return [Client] an object that holds the configuration of the api connection.
|
37
|
-
def initialize(api_key:, version: "v1", host:
|
42
|
+
def initialize(api_key:, version: "v1", host: DEFAULT_HOST, logger: nil, deep_logging: false)
|
38
43
|
@version = version
|
39
44
|
@api_key = api_key
|
40
45
|
@logger = logger
|
@@ -163,7 +168,9 @@ module Ecoportal
|
|
163
168
|
log(:debug) { "Data: #{JSON.pretty_generate(data)}" }
|
164
169
|
end
|
165
170
|
|
166
|
-
with_retry
|
171
|
+
with_retry do
|
172
|
+
rate_throttling(&block)
|
173
|
+
end.tap do |result|
|
167
174
|
next unless deep_logging?
|
168
175
|
|
169
176
|
end_time = Time.now.to_f
|
@@ -0,0 +1,116 @@
|
|
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 = :min # :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
|
+
done = status.progress
|
43
|
+
waited = Time.now - before
|
44
|
+
|
45
|
+
adapted = waited
|
46
|
+
adapted = waited - (delay_status_check / 2) if delay_status_check
|
47
|
+
ratio = throughput!(adapted, count: done)
|
48
|
+
|
49
|
+
break status if status.complete?(total)
|
50
|
+
|
51
|
+
pending = status.pending(total)
|
52
|
+
left = max_timeout - waited
|
53
|
+
|
54
|
+
timeout!(status, timeout: max_timeout) unless left.positive?
|
55
|
+
|
56
|
+
delay_status_check = status_check_in(pending, timeout_in: left)
|
57
|
+
|
58
|
+
msg = " ... Awaiting #{delay_status_check} s. -- "
|
59
|
+
msg << " TimeOut: #{left.round(2)} s. "
|
60
|
+
msg << "(job '#{job_id}') "
|
61
|
+
msg << "Done: #{done} (est. #{ratio} rec/s) "
|
62
|
+
msg << " \r"
|
63
|
+
|
64
|
+
print msg
|
65
|
+
$stdout.flush
|
66
|
+
|
67
|
+
sleep(delay_status_check)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def timeout!(status, timeout:)
|
74
|
+
self.timeout_approach = self.class::TIMEOUT_FALLBACK
|
75
|
+
|
76
|
+
msg = "Job '#{job_id}' not complete (size: #{total}).\n"
|
77
|
+
msg << " Timed out after #{timeout} seconds.\n"
|
78
|
+
msg << " Current status: #{status}"
|
79
|
+
|
80
|
+
raise API::Errors::TimeOut, msg
|
81
|
+
end
|
82
|
+
|
83
|
+
def checked?
|
84
|
+
@checked
|
85
|
+
end
|
86
|
+
|
87
|
+
def status_check_in(pending, timeout_in:)
|
88
|
+
unless checked?
|
89
|
+
@checked = true
|
90
|
+
return min_delay_status_check
|
91
|
+
end
|
92
|
+
|
93
|
+
return default_delay_status_check if around_min_throughput?
|
94
|
+
|
95
|
+
eta = eta_for(pending, approach: :optimistic)
|
96
|
+
check_in_max = [eta, timeout_in].min * 0.90
|
97
|
+
|
98
|
+
check_in_best = check_in_max / 2.0
|
99
|
+
default_5 = default_delay_status_check * 5
|
100
|
+
|
101
|
+
top_check = [default_5, check_in_best].min.ceil
|
102
|
+
[top_check, min_delay_status_check].max
|
103
|
+
end
|
104
|
+
|
105
|
+
def default_delay_status_check
|
106
|
+
self.class::DELAY_STATUS_CHECK
|
107
|
+
end
|
108
|
+
|
109
|
+
def min_delay_status_check
|
110
|
+
self.class::MIN_STATUS_CHECK
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
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::Client::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'
|
data/lib/ecoportal/api/v1.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ecoportal-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.10.
|
4
|
+
version: 0.10.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tapio Saarinen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-10-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pry
|
@@ -180,6 +180,20 @@ dependencies:
|
|
180
180
|
- - "<"
|
181
181
|
- !ruby/object:Gem::Version
|
182
182
|
version: '6'
|
183
|
+
- !ruby/object:Gem::Dependency
|
184
|
+
name: rate_throttle_client
|
185
|
+
requirement: !ruby/object:Gem::Requirement
|
186
|
+
requirements:
|
187
|
+
- - "~>"
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0.1'
|
190
|
+
type: :runtime
|
191
|
+
prerelease: false
|
192
|
+
version_requirements: !ruby/object:Gem::Requirement
|
193
|
+
requirements:
|
194
|
+
- - "~>"
|
195
|
+
- !ruby/object:Gem::Version
|
196
|
+
version: '0.1'
|
183
197
|
description:
|
184
198
|
email:
|
185
199
|
- tapio@ecoportal.co.nz
|
@@ -212,6 +226,9 @@ files:
|
|
212
226
|
- lib/ecoportal/api/common/client/elastic_apm_integration.rb
|
213
227
|
- lib/ecoportal/api/common/client/error.rb
|
214
228
|
- lib/ecoportal/api/common/client/error/checks.rb
|
229
|
+
- lib/ecoportal/api/common/client/rate_throttling.rb
|
230
|
+
- lib/ecoportal/api/common/client/throughput.rb
|
231
|
+
- lib/ecoportal/api/common/client/throughput/stats.rb
|
215
232
|
- lib/ecoportal/api/common/client/time_out.rb
|
216
233
|
- lib/ecoportal/api/common/client/with_retry.rb
|
217
234
|
- lib/ecoportal/api/common/doc_helpers.rb
|
@@ -239,7 +256,9 @@ files:
|
|
239
256
|
- lib/ecoportal/api/internal/schema_field_value.rb
|
240
257
|
- lib/ecoportal/api/logger.rb
|
241
258
|
- lib/ecoportal/api/v1.rb
|
242
|
-
- lib/ecoportal/api/v1/
|
259
|
+
- lib/ecoportal/api/v1/job.rb
|
260
|
+
- lib/ecoportal/api/v1/job/awaiter.rb
|
261
|
+
- lib/ecoportal/api/v1/job/status.rb
|
243
262
|
- lib/ecoportal/api/v1/people.rb
|
244
263
|
- lib/ecoportal/api/v1/person.rb
|
245
264
|
- lib/ecoportal/api/v1/person_details.rb
|
@@ -1,33 +0,0 @@
|
|
1
|
-
module Ecoportal
|
2
|
-
module API
|
3
|
-
class V1
|
4
|
-
class JobStatus
|
5
|
-
attr_reader :id, :progress
|
6
|
-
|
7
|
-
def initialize(id, complete, errored, progress)
|
8
|
-
@id = id
|
9
|
-
@complete = complete
|
10
|
-
@errored = errored
|
11
|
-
@progress = progress
|
12
|
-
end
|
13
|
-
|
14
|
-
def complete?(total = nil)
|
15
|
-
return @complete if total.nil?
|
16
|
-
|
17
|
-
progress >= total
|
18
|
-
end
|
19
|
-
|
20
|
-
def errored?
|
21
|
-
@errored
|
22
|
-
end
|
23
|
-
|
24
|
-
def to_s
|
25
|
-
msg = complete? ? "Completed" : "In progress"
|
26
|
-
msg = "Errored" if errored?
|
27
|
-
msg << " with #{progress} done."
|
28
|
-
msg
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|