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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28e9d05f838bf52d038a6d9533a8bcbb98f7b66b5ffa6fa7e99a4c30b2d0e486
|
4
|
+
data.tar.gz: a6586e72297c5e109ed4f2e8a04beb32e1e435ea3eb0332803b4ba9fd91e8fb3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b8ecb9337ed3b2f11913ae5823bdb3b4771d1bcb364a50afc68bba38518dff39fd7986de641a4030030cfeeb0019a5960d9ac2440a10ee2bb49ed15aad77f0a8
|
7
|
+
data.tar.gz: 1f3ca204cb6c8b4a6cab2396285bf50644cb955f1120c15253f1d81ac1663bf2874f2b3706f5dedcfbdc34bf7c67d5a0520f1e62c0e8891259b337d92394cbc3
|
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.5] - 2024-10-xx
|
6
6
|
|
7
7
|
### Added
|
8
8
|
|
@@ -10,6 +10,29 @@ All notable changes to this project will be documented in this file.
|
|
10
10
|
|
11
11
|
### Fixed
|
12
12
|
|
13
|
+
## [0.10.4] - 2024-10-15
|
14
|
+
|
15
|
+
### Added
|
16
|
+
|
17
|
+
- `Ecoportal::API::Common::Client::RateThrottling`
|
18
|
+
- See: <https://github.com/zombocom/rate_throttle_client?tab=readme-ov-file#ratethrottleclient>
|
19
|
+
- Add adapter to the RateLimit, may it be present.
|
20
|
+
- **Default:** `:proportional_decrease`
|
21
|
+
|
22
|
+
### Changed
|
23
|
+
|
24
|
+
- `Ecoportal::API::Common::Client::TimeOut`
|
25
|
+
- Const `MIN_SIZE` changed from `10` to `5` to speed up small requests.
|
26
|
+
- Full revamp of `Job` endpoint
|
27
|
+
|
28
|
+
## [0.10.3] - 2024-10-01
|
29
|
+
|
30
|
+
### Changed
|
31
|
+
|
32
|
+
- refactored Client
|
33
|
+
- initialized with `deep_logging` named argumnet
|
34
|
+
- previously `response_logging`
|
35
|
+
|
13
36
|
## [0.10.2] - 2024-09-27
|
14
37
|
|
15
38
|
### Added
|
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
|
@@ -4,11 +4,12 @@ module Ecoportal
|
|
4
4
|
class BatchOperation
|
5
5
|
include Common::DocHelpers
|
6
6
|
|
7
|
-
def initialize(base_path, wrapper, logger: nil)
|
8
|
-
@base_path
|
9
|
-
@wrapper
|
10
|
-
@operations
|
11
|
-
@logger
|
7
|
+
def initialize(base_path, wrapper, logger: nil, deep_logging: false)
|
8
|
+
@base_path = base_path
|
9
|
+
@wrapper = wrapper
|
10
|
+
@operations = []
|
11
|
+
@logger = logger
|
12
|
+
@deep_logging = deep_logging
|
12
13
|
end
|
13
14
|
|
14
15
|
def count
|
@@ -25,11 +26,12 @@ module Ecoportal
|
|
25
26
|
|
26
27
|
def process_response(response)
|
27
28
|
unless response.success?
|
28
|
-
|
29
|
-
|
29
|
+
msg = "Error: total failure in batch operation."
|
30
|
+
log(:debug) { msg }
|
31
|
+
raise msg
|
30
32
|
end
|
31
33
|
|
32
|
-
log(:
|
34
|
+
log(:debug) { "Processing batch responses" } if deep_logging?
|
33
35
|
|
34
36
|
body_data(response.body).each.with_index do |subresponse, idx|
|
35
37
|
status = subresponse["status"]
|
@@ -110,14 +112,19 @@ module Ecoportal
|
|
110
112
|
end
|
111
113
|
|
112
114
|
def log_batch_response(operation, response)
|
113
|
-
|
114
|
-
log(:info) { "Status #{response.status}" }
|
115
|
+
return unless deep_logging?
|
115
116
|
|
116
|
-
|
117
|
-
log(
|
117
|
+
log(:debug) { "BATCH #{operation[:method]} #{operation[:path]}" }
|
118
|
+
log(:debug) { "Status #{response.status}" }
|
119
|
+
log(:debug) { "Response: #{JSON.pretty_generate(response.body)}" }
|
120
|
+
end
|
121
|
+
|
122
|
+
def deep_logging?
|
123
|
+
@deep_logging
|
118
124
|
end
|
119
125
|
|
120
126
|
def log(level, &block)
|
127
|
+
puts "(#{level}) #{yield}"
|
121
128
|
@logger&.send(level, &block)
|
122
129
|
end
|
123
130
|
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'elastic-apm'
|
2
|
+
module Ecoportal
|
3
|
+
module API
|
4
|
+
module Common
|
5
|
+
class Client
|
6
|
+
module ElasticApmIntegration
|
7
|
+
include Ecoportal::API::Common::Client::Error::Checks
|
8
|
+
|
9
|
+
APM_SERVICE_NAME = 'ecoportal-api-gem'.freeze
|
10
|
+
|
11
|
+
# Log only errors that are only server's responsibility
|
12
|
+
def log_unexpected_server_error(response)
|
13
|
+
msg = "Expecting Ecoportal::API::Common::Response. Given: #{response.class}"
|
14
|
+
raise msg unless response.is_a?(Common::Response)
|
15
|
+
|
16
|
+
return unless elastic_apm_service
|
17
|
+
return unless unexpected_server_error_code?(response.status)
|
18
|
+
return unless ElasticAPM.running?
|
19
|
+
|
20
|
+
ElasticAPM.report(
|
21
|
+
Ecoportal::API::Common::Client::Error::UnexpectedServerError.new(
|
22
|
+
response.body,
|
23
|
+
code: response.status
|
24
|
+
)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# finalizer to stop the agent
|
31
|
+
close_elastic_apm = proc do |_id|
|
32
|
+
next unless ElasticAPM.running?
|
33
|
+
|
34
|
+
puts "Stopping ElasticAPM service"
|
35
|
+
ElasticAPM.stop
|
36
|
+
rescue StandardError
|
37
|
+
# Silent
|
38
|
+
end
|
39
|
+
|
40
|
+
ObjectSpace.define_finalizer("ElasticAPM", close_elastic_apm)
|
41
|
+
|
42
|
+
def elastic_apm_service
|
43
|
+
return false if @disable_apm
|
44
|
+
|
45
|
+
ElasticAPM.start(**elastic_apm_options) unless ElasticAPM.running?
|
46
|
+
rescue StandardError => err
|
47
|
+
@disable_apm = true
|
48
|
+
puts "ElasticAPM services not available: #{err}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def elastic_apm_options
|
52
|
+
{
|
53
|
+
service_name: APM_SERVICE_NAME,
|
54
|
+
server_url: elastic_apm_url,
|
55
|
+
secret_token: elastic_apm_key,
|
56
|
+
environment: environment,
|
57
|
+
# http_compression: false,
|
58
|
+
transaction_sample_rate: 0.1,
|
59
|
+
transaction_max_spans: 100,
|
60
|
+
span_frames_min_duration: "5ms"
|
61
|
+
}.tap do |options|
|
62
|
+
# next unless false
|
63
|
+
|
64
|
+
options.merge!({
|
65
|
+
log_level: Logger::DEBUG,
|
66
|
+
log_path: File.join(__dir__, "elastic_apm.log")
|
67
|
+
})
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def elastic_apm_url
|
72
|
+
@elastic_apm_url ||= "https://".tap do |url|
|
73
|
+
url << elastic_apm_account_id.to_s
|
74
|
+
url << ".#{elastic_apm_base_url}"
|
75
|
+
url << ":#{elastic_apm_port}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def elastic_apm_key
|
80
|
+
@elastic_apm_key ||= ENV['ELASTIC_APM_KEY']
|
81
|
+
end
|
82
|
+
|
83
|
+
def elastic_apm_account_id
|
84
|
+
@elastic_apm_account_id ||= ENV['ELASTIC_APM_ACCOUNT_ID']
|
85
|
+
end
|
86
|
+
|
87
|
+
def elastic_apm_base_url
|
88
|
+
@elastic_apm_base_url ||= "apm.#{elastic_apm_region}.aws.cloud.es.io"
|
89
|
+
end
|
90
|
+
|
91
|
+
def elastic_apm_region
|
92
|
+
@elastic_apm_region ||= ENV['ELASTIC_APM_REGION'] || "ap-southeast-2"
|
93
|
+
end
|
94
|
+
|
95
|
+
def elastic_apm_port
|
96
|
+
@elastic_apm_port ||= ENV['ELASTIC_APM_PORT'] || "443"
|
97
|
+
end
|
98
|
+
|
99
|
+
def environment
|
100
|
+
@environment ||= "unknown".tap do |value|
|
101
|
+
next unless instance_variable_defined?(:@host)
|
102
|
+
next unless (env = @host.gsub(".ecoportal.com", ''))
|
103
|
+
|
104
|
+
value.clear << env
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Ecoportal
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
class Client
|
5
|
+
module Error
|
6
|
+
module Checks
|
7
|
+
private
|
8
|
+
|
9
|
+
def unexpected_server_error_code?(code)
|
10
|
+
return true unless code
|
11
|
+
return true if (code >= 500) && (code <= 599)
|
12
|
+
|
13
|
+
code <= 99
|
14
|
+
end
|
15
|
+
|
16
|
+
# Sometimes response body is wrong but status code
|
17
|
+
# doesn't reflect. Let it retry
|
18
|
+
def some_unexpected_error?(response)
|
19
|
+
return true if unexpected_server_error_code?(response.status)
|
20
|
+
|
21
|
+
unexpected_body?(response)
|
22
|
+
end
|
23
|
+
|
24
|
+
def unexpected_body?(response)
|
25
|
+
response.body.nil?.tap do |wrong|
|
26
|
+
next unless wrong
|
27
|
+
|
28
|
+
msg = "Received non json body in response "
|
29
|
+
msg << "(#{response.src_body.class}):\n "
|
30
|
+
msg << response.src_body
|
31
|
+
puts
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'ecoportal/api/common/client/error/checks'
|
2
|
+
|
3
|
+
module Ecoportal
|
4
|
+
module API
|
5
|
+
module Common
|
6
|
+
class Client
|
7
|
+
module Error
|
8
|
+
class UnexpectedServerError < StandardError
|
9
|
+
def initialize(msg, code:)
|
10
|
+
super("Code: #{code} -- Error: #{msg}")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -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
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Ecoportal
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
class Client
|
5
|
+
module TimeOut
|
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
|
30
|
+
|
31
|
+
private
|
32
|
+
|
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)
|
41
|
+
end
|
42
|
+
|
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
|
49
|
+
end
|
50
|
+
|
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
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|