apisonator 3.4.0 → 3.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a260bc57e82268c54885b58a12c79d28c9b7c53f8fb5e48182c887488888184
4
- data.tar.gz: 7dbed84c520c06e914d15e99f6e1b78365a1b695d8cf8e1fd66a58c02fa36fa9
3
+ metadata.gz: 78b328c64420fa58f4480eb39b555856dd9a032ba78eed32f084767988cea59d
4
+ data.tar.gz: de454b45677bc1c133eeda34c8fb4ebfe56253c4b4de851f1d192652c5baf999
5
5
  SHA512:
6
- metadata.gz: 4d0bdd90d90972bc416faf04a852ff56c3895da1cb191b264617f71add2f22c5942d928d85c9877faceab4f00e7fe26955dfeed7c6cbc615a8fe555b7c6e251a
7
- data.tar.gz: 22b6dbdb74d73117ba7358d231b5f747f54746687e452285b77883df4b352d20d59b3997b38a0c5c7b6cba9bce618b41900d6a0ee3ea1e169727470bce8158cb
6
+ metadata.gz: bf7613f456c9810217adfbdd5f345b86aa59d9abdccd44d308d846e7f271849d484601900348a4dcd4a16437d9e0376b7927e3675374a9be7a2af723c2820626
7
+ data.tar.gz: deb0823a0648f55bf24c24da6727afa4e62d2d5d92eeeee5545b9f6c8af38b2105bc8bb8caf3f7b28c187e020080b9b9f0b43721a42dfece2d6963d698a23e9a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  Notable changes to Apisonator will be tracked in this document.
4
4
 
5
+ ## 3.4.1 - 2021-06-16
6
+
7
+ ### Changed
8
+
9
+ - Introduced performance optimizations around alerts detection
10
+ ([#287](https://github.com/3scale/apisonator/pull/287)).
11
+
5
12
  ## 3.4.0 - 2021-06-14
6
13
 
7
14
  ### Added
data/Gemfile.lock CHANGED
@@ -36,7 +36,7 @@ GIT
36
36
  PATH
37
37
  remote: .
38
38
  specs:
39
- apisonator (3.4.0)
39
+ apisonator (3.4.1)
40
40
 
41
41
  GEM
42
42
  remote: https://rubygems.org/
data/Gemfile.on_prem.lock CHANGED
@@ -36,7 +36,7 @@ GIT
36
36
  PATH
37
37
  remote: .
38
38
  specs:
39
- apisonator (3.4.0)
39
+ apisonator (3.4.1)
40
40
 
41
41
  GEM
42
42
  remote: https://rubygems.org/
@@ -44,6 +44,7 @@ require '3scale/backend/queue_storage'
44
44
  require '3scale/backend/errors'
45
45
  require '3scale/backend/stats'
46
46
  require '3scale/backend/usage_limit'
47
+ require '3scale/backend/utilization'
47
48
  require '3scale/backend/alerts'
48
49
  require '3scale/backend/event_storage'
49
50
  require '3scale/backend/worker'
@@ -9,7 +9,11 @@ module ThreeScale
9
9
  attr_accessor :service_id, :value
10
10
 
11
11
  def save
12
- storage.sadd(key_allowed_set(service_id), value.to_i) if valid?
12
+ if valid?
13
+ res = storage.sadd(key_allowed_set(service_id), value.to_i)
14
+ Alerts::UsagesChecked.invalidate_for_service(service_id)
15
+ res
16
+ end
13
17
  end
14
18
 
15
19
  def to_hash
@@ -33,6 +33,11 @@ module ThreeScale
33
33
  def key_current_id
34
34
  'alerts/current_id'.freeze
35
35
  end
36
+
37
+ def key_usage_already_checked(service_id, app_id)
38
+ prefix = key_prefix(service_id, app_id)
39
+ "#{prefix}usage_already_checked"
40
+ end
36
41
  end
37
42
 
38
43
  extend self
@@ -45,43 +50,82 @@ module ThreeScale
45
50
  FIRST_ALERT_BIN = ALERT_BINS.first
46
51
  RALERT_BINS = ALERT_BINS.reverse.freeze
47
52
 
48
- def can_raise_more_alerts?(service_id, app_id)
49
- allowed_bins = allowed_set_for_service(service_id).sort
53
+ # This class is useful to reduce the amount of information that we need to
54
+ # fetch from Redis to determine whether an alert should be raised.
55
+ # In summary, alerts are raised at the application level and we need to
56
+ # wait for 24h before raising a new one for the same level (ALERTS_BIN
57
+ # above).
58
+ #
59
+ # This class allows us to check all the usage limits once and then not
60
+ # check all of them again (just the ones in the report job) until:
61
+ # 1) A specific alert has expired (24h passed since it was triggered).
62
+ # 2) A new alert bin is enabled for the service.
63
+ class UsagesChecked
64
+ extend KeyHelpers
65
+ extend StorageHelpers
66
+ include Memoizer::Decorator
67
+
68
+ def self.need_to_check_all?(service_id, app_id)
69
+ !storage.exists(key_usage_already_checked(service_id, app_id))
70
+ end
71
+ memoize :need_to_check_all?
50
72
 
51
- return false if allowed_bins.empty?
73
+ def self.mark_all_checked(service_id, app_id)
74
+ ttl = ALERT_BINS.map do |bin|
75
+ ttl = storage.ttl(key_already_notified(service_id, app_id, bin))
52
76
 
53
- # If the bin with the highest value has already been notified, there's
54
- # no need to notify anything else.
55
- not notified?(service_id, app_id, allowed_bins.last)
56
- end
77
+ # Redis returns -2 if key does not exist, and -1 if it exists without
78
+ # a TTL (we know this should not happen for the alert bins).
79
+ # In those cases we should just set the TTL to the max (ALERT_TTL).
80
+ ttl >= 0 ? ttl : ALERT_TTL
81
+ end.min
57
82
 
58
- def utilization(app_usage_reports)
59
- max_utilization = -1.0
60
- max_record = nil
61
- max = proc do |item|
62
- if item.max_value > 0
63
- utilization = item.current_value / item.max_value.to_f
64
-
65
- if utilization > max_utilization
66
- max_record = item
67
- max_utilization = utilization
68
- end
69
- end
83
+ storage.setex(key_usage_already_checked(service_id, app_id), ttl, '1'.freeze)
84
+ Memoizer.clear(Memoizer.build_key(self, :need_to_check_all?, service_id, app_id))
70
85
  end
71
86
 
72
- app_usage_reports.each(&max)
87
+ def self.invalidate(service_id, app_id)
88
+ storage.del(key_usage_already_checked(service_id, app_id))
89
+ end
90
+
91
+ def self.invalidate_for_service(service_id)
92
+ app_ids = []
93
+ cursor = 0
94
+
95
+ loop do
96
+ cursor, ids = storage.sscan(
97
+ Application.applications_set_key(service_id), cursor, count: SCAN_SLICE
98
+ )
99
+
100
+ app_ids += ids
73
101
 
74
- if max_utilization == -1
75
- ## case that all the limits have max_value==0
76
- max_utilization = 0
77
- max_record = app_usage_reports.first
102
+ break if cursor.to_i == 0
103
+ end
104
+
105
+ invalidate_batch(service_id, app_ids)
106
+ end
107
+
108
+ def self.invalidate_batch(service_id, app_ids)
109
+ app_ids.each_slice(PIPELINED_SLICE_SIZE) do |ids|
110
+ keys = ids.map { |app_id| key_usage_already_checked(service_id, app_id) }
111
+ storage.del(keys)
112
+ end
78
113
  end
114
+ private_class_method :invalidate_batch
115
+ end
116
+
117
+ def can_raise_more_alerts?(service_id, app_id)
118
+ allowed_bins = allowed_set_for_service(service_id).sort
119
+
120
+ return false if allowed_bins.empty?
79
121
 
80
- [max_utilization, max_record]
122
+ # If the bin with the highest value has already been notified, there's
123
+ # no need to notify anything else.
124
+ not notified?(service_id, app_id, allowed_bins.last)
81
125
  end
82
126
 
83
- def update_utilization(service_id, app_id, max_utilization, max_record, timestamp)
84
- discrete = utilization_discrete(max_utilization)
127
+ def update_utilization(service_id, app_id, utilization)
128
+ discrete = utilization_discrete(utilization.ratio)
85
129
 
86
130
  keys = alert_keys(service_id, app_id, discrete)
87
131
 
@@ -91,18 +135,19 @@ module ThreeScale
91
135
  end
92
136
 
93
137
  if already_alerted.nil? && allowed && discrete.to_i > 0
94
- next_id, _ = storage.pipelined do
138
+ next_id, _, _ = storage.pipelined do
95
139
  storage.incr(keys[:current_id])
96
140
  storage.setex(keys[:already_notified], ALERT_TTL, "1")
141
+ UsagesChecked.invalidate(service_id, app_id)
97
142
  end
98
143
 
99
144
  alert = { :id => next_id,
100
145
  :utilization => discrete,
101
- :max_utilization => max_utilization,
146
+ :max_utilization => utilization.ratio,
102
147
  :application_id => app_id,
103
148
  :service_id => service_id,
104
- :timestamp => timestamp,
105
- :limit => formatted_limit(max_record) }
149
+ :timestamp => Time.now.utc,
150
+ :limit => utilization.to_s }
106
151
 
107
152
  Backend::EventStorage::store(:alert, alert)
108
153
  end
@@ -116,11 +161,6 @@ module ThreeScale
116
161
  end || FIRST_ALERT_BIN
117
162
  end
118
163
 
119
- def formatted_limit(record)
120
- "#{record.metric_name} per #{record.period}: "\
121
- "#{record.current_value}/#{record.max_value}"
122
- end
123
-
124
164
  def allowed_set_for_service(service_id)
125
165
  storage.smembers(key_allowed_set(service_id)).map(&:to_i) # Redis returns strings always
126
166
  end
@@ -4,6 +4,8 @@ module ThreeScale
4
4
  module All
5
5
  TIME_FORMAT = '%Y-%m-%d %H:%M:%S %z'.freeze
6
6
  PIPELINED_SLICE_SIZE = 400
7
+ SLEEP_BETWEEN_SCANS = 0.01 # In seconds
8
+ SCAN_SLICE = 500
7
9
  end
8
10
 
9
11
  def self.included(base)
@@ -59,7 +59,7 @@ module ThreeScale
59
59
  touched_apps = aggregate(transactions, current_bucket)
60
60
 
61
61
  ApplicationEvents.generate(touched_apps.values)
62
- update_alerts(touched_apps)
62
+ update_alerts(transactions)
63
63
  begin
64
64
  ApplicationEvents.ping
65
65
  rescue ApplicationEvents::PingFailed => e
@@ -137,39 +137,35 @@ module ThreeScale
137
137
  logger.info(MAX_BUCKETS_CREATED_MSG)
138
138
  end
139
139
 
140
- def update_alerts(applications)
141
- current_timestamp = Time.now.getutc
142
-
143
- applications.each do |_appid, values|
144
- service_id = values[:service_id]
145
- application = Backend::Application.load(service_id,
146
- values[:application_id])
147
-
148
- # The app could have been deleted at some point since the job was
149
- # enqueued. No need to update alerts in that case.
150
- next unless application
151
-
152
- # The operations below are costly. They load all the usage limits
153
- # and current usages to find the current utilization levels.
154
- # That's why before that, we check if there are any alerts that
155
- # can be raised.
156
- next unless Alerts.can_raise_more_alerts?(service_id, values[:application_id])
157
-
158
- application.load_metric_names
159
- usage = Usage.application_usage(application, current_timestamp)
160
- status = Transactor::Status.new(service_id: service_id,
161
- application: application,
162
- values: usage)
163
-
164
- max_utilization, max_record = Alerts.utilization(
165
- status.application_usage_reports)
166
-
167
- if max_utilization >= 0.0
168
- Alerts.update_utilization(service_id,
169
- values[:application_id],
170
- max_utilization,
171
- max_record,
172
- current_timestamp)
140
+ def update_alerts(transactions)
141
+ transactions.group_by { |tx| tx.application_id }.each do |app_id, txs|
142
+ service_id = txs.first.service_id # All the txs of an app belong to the same service
143
+
144
+ # Finding the max utilization can be costly because it involves
145
+ # loading usage limits and current usages. That's why before that,
146
+ # we check if there are any alerts that can be raised.
147
+ next unless Alerts.can_raise_more_alerts?(service_id, app_id)
148
+
149
+ begin
150
+ max_utilization = if Alerts::UsagesChecked.need_to_check_all?(service_id, app_id)
151
+ Utilization.max_in_all_metrics(service_id, app_id).tap do
152
+ Alerts::UsagesChecked.mark_all_checked(service_id, app_id)
153
+ end
154
+ else
155
+ # metrics_ids here includes the metrics
156
+ # explicitly reported plus their parents in
157
+ # the hierarchy.
158
+ metric_ids = txs.map { |tx| tx.usage.keys }.flatten.uniq
159
+ Utilization.max_in_metrics(service_id, app_id, metric_ids)
160
+ end
161
+ rescue ApplicationNotFound
162
+ # The app could have been deleted at some point since the job
163
+ # was enqueued. No need to update alerts in that case.
164
+ next
165
+ end
166
+
167
+ if max_utilization && max_utilization.ratio > 0
168
+ Alerts.update_utilization(service_id, app_id, max_utilization)
173
169
  end
174
170
  end
175
171
  end
@@ -36,12 +36,6 @@ module ThreeScale
36
36
  KEY_SERVICES_TO_DELETE = 'set_with_services_marked_for_deletion'.freeze
37
37
  private_constant :KEY_SERVICES_TO_DELETE
38
38
 
39
- SLEEP_BETWEEN_SCANS = 0.01 # In seconds
40
- private_constant :SLEEP_BETWEEN_SCANS
41
-
42
- SCAN_SLICE = 500
43
- private_constant :SCAN_SLICE
44
-
45
39
  STATS_KEY_PREFIX = 'stats/'.freeze
46
40
  private_constant :STATS_KEY_PREFIX
47
41
 
@@ -3,7 +3,7 @@ module ThreeScale
3
3
  module Transactor
4
4
  class Status
5
5
  class UsageReport
6
- attr_reader :type, :period
6
+ attr_reader :type, :usage_limit, :period
7
7
 
8
8
  def initialize(status, usage_limit)
9
9
  @status = status
@@ -3,12 +3,19 @@ module ThreeScale
3
3
  class Usage
4
4
  class << self
5
5
  def application_usage(application, timestamp)
6
- usage(application, timestamp) do |metric_id, instance_period|
6
+ usage(application.usage_limits, timestamp) do |metric_id, instance_period|
7
7
  Stats::Keys.application_usage_value_key(
8
8
  application.service_id, application.id, metric_id, instance_period)
9
9
  end
10
10
  end
11
11
 
12
+ def application_usage_for_limits(application, timestamp, usage_limits)
13
+ usage(usage_limits, timestamp) do |metric_id, instance_period|
14
+ Stats::Keys.application_usage_value_key(
15
+ application.service_id, application.id, metric_id, instance_period)
16
+ end
17
+ end
18
+
12
19
  def is_set?(usage_str)
13
20
  usage_str && usage_str[0] == '#'.freeze
14
21
  end
@@ -25,7 +32,7 @@ module ThreeScale
25
32
 
26
33
  private
27
34
 
28
- def usage(obj, timestamp)
35
+ def usage(usage_limits, timestamp)
29
36
  # The timestamp does not change, so we can generate all the
30
37
  # instantiated periods just once.
31
38
  # This is important. Without this, the code can generate many instance
@@ -33,7 +40,7 @@ module ThreeScale
33
40
  # time.
34
41
  instance_periods = Period::instance_periods_for_ts(timestamp)
35
42
 
36
- pairs = metric_period_pairs obj.usage_limits
43
+ pairs = metric_period_pairs usage_limits
37
44
  return {} if pairs.empty?
38
45
 
39
46
  keys = pairs.map do |(metric_id, period)|
@@ -0,0 +1,83 @@
1
+ module ThreeScale
2
+ module Backend
3
+ class Utilization
4
+ include Comparable
5
+
6
+ attr_reader :metric_id, :period, :max_value, :current_value
7
+
8
+ def initialize(limit, current_value)
9
+ @metric_id = limit.metric_id
10
+ @period = limit.period
11
+ @max_value = limit.value
12
+ @current_value = current_value
13
+ @encoded = encoded(limit, current_value)
14
+ end
15
+
16
+ def ratio
17
+ return 0 if max_value == 0 # Disabled metric
18
+ current_value/max_value.to_f
19
+ end
20
+
21
+ # Returns in the format needed by the Alerts class.
22
+ def to_s
23
+ @encoded
24
+ end
25
+
26
+ def <=>(other)
27
+ # Consider "disabled" the lowest ones
28
+ if ratio == 0 && other.ratio == 0
29
+ return max_value <=> other.max_value
30
+ end
31
+
32
+ ratio <=> other.ratio
33
+ end
34
+
35
+ # Note: this can return nil
36
+ def self.max_in_all_metrics(service_id, app_id)
37
+ application = Backend::Application.load!(service_id, app_id)
38
+
39
+ usage = Usage.application_usage(application, Time.now.getutc)
40
+
41
+ status = Transactor::Status.new(service_id: service_id,
42
+ application: application,
43
+ values: usage)
44
+
45
+ # Preloads all the metric names to avoid fetching them one by one when
46
+ # generating the usage reports
47
+ application.load_metric_names
48
+
49
+ max = status.application_usage_reports.map do |usage_report|
50
+ Utilization.new(usage_report.usage_limit, usage_report.current_value)
51
+ end.max
52
+
53
+ # Avoid returning a utilization for disabled metrics
54
+ max && max.max_value > 0 ? max : nil
55
+ end
56
+
57
+ # Note: this can return nil
58
+ def self.max_in_metrics(service_id, app_id, metric_ids)
59
+ application = Backend::Application.load!(service_id, app_id)
60
+
61
+ limits = UsageLimit.load_for_affecting_metrics(
62
+ service_id, application.plan_id, metric_ids
63
+ )
64
+
65
+ usage = Usage.application_usage_for_limits(application, Time.now.getutc, limits)
66
+
67
+ max = limits.map do |limit|
68
+ Utilization.new(limit, usage[limit.period][limit.metric_id])
69
+ end.max
70
+
71
+ # Avoid returning a utilization for disabled metrics
72
+ max && max.max_value > 0 ? max : nil
73
+ end
74
+
75
+ private
76
+
77
+ def encoded(limit, current_value)
78
+ metric_name = Metric.load_name(limit.service_id, limit.metric_id)
79
+ "#{metric_name} per #{limit.period}: #{current_value}/#{limit.value}"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,5 +1,5 @@
1
1
  module ThreeScale
2
2
  module Backend
3
- VERSION = '3.4.0'
3
+ VERSION = '3.4.1'
4
4
  end
5
5
  end
data/licenses.xml CHANGED
@@ -23,7 +23,7 @@
23
23
  </dependency>
24
24
  <dependency>
25
25
  <packageName>apisonator</packageName>
26
- <version>3.4.0</version>
26
+ <version>3.4.1</version>
27
27
  <licenses>
28
28
  <license>
29
29
  <name>Apache 2.0</name>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apisonator
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 3.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Ciganek
@@ -16,7 +16,7 @@ authors:
16
16
  autorequire:
17
17
  bindir: bin
18
18
  cert_chain: []
19
- date: 2021-06-14 00:00:00.000000000 Z
19
+ date: 2021-06-17 00:00:00.000000000 Z
20
20
  dependencies: []
21
21
  description: This gem provides a daemon that handles authorization and reporting of
22
22
  web services managed by 3scale.
@@ -164,6 +164,7 @@ files:
164
164
  - lib/3scale/backend/usage_limit.rb
165
165
  - lib/3scale/backend/use_cases/provider_key_change_use_case.rb
166
166
  - lib/3scale/backend/util.rb
167
+ - lib/3scale/backend/utilization.rb
167
168
  - lib/3scale/backend/validators.rb
168
169
  - lib/3scale/backend/validators/base.rb
169
170
  - lib/3scale/backend/validators/key.rb