apisonator 3.4.0 → 3.4.1

Sign up to get free protection for your applications and to get access to all the features.
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