Commit af482b0b authored by João Cunha's avatar João Cunha Committed by Sean McGivern

Set limits for reactive cache data

- Adds reactive_cache_limit FF defautl false
- Logs ReactiveCaching::ExceededReactiveCacheLimit to Sentry
- Limit Environments to 10MB
- Limit MergeRequest to 20MB
- Adds Documentation
- Adds changelog
- Adds specs
parent 64c36629
...@@ -6,23 +6,22 @@ module ReactiveCaching ...@@ -6,23 +6,22 @@ module ReactiveCaching
extend ActiveSupport::Concern extend ActiveSupport::Concern
InvalidateReactiveCache = Class.new(StandardError) InvalidateReactiveCache = Class.new(StandardError)
ExceededReactiveCacheLimit = Class.new(StandardError)
included do included do
class_attribute :reactive_cache_lease_timeout
class_attribute :reactive_cache_key class_attribute :reactive_cache_key
class_attribute :reactive_cache_lifetime class_attribute :reactive_cache_lease_timeout
class_attribute :reactive_cache_refresh_interval class_attribute :reactive_cache_refresh_interval
class_attribute :reactive_cache_lifetime
class_attribute :reactive_cache_hard_limit
class_attribute :reactive_cache_worker_finder class_attribute :reactive_cache_worker_finder
# defaults # defaults
self.reactive_cache_key = -> (record) { [model_name.singular, record.id] } self.reactive_cache_key = -> (record) { [model_name.singular, record.id] }
self.reactive_cache_lease_timeout = 2.minutes self.reactive_cache_lease_timeout = 2.minutes
self.reactive_cache_refresh_interval = 1.minute self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 10.minutes self.reactive_cache_lifetime = 10.minutes
self.reactive_cache_hard_limit = 1.megabyte
self.reactive_cache_worker_finder = ->(id, *_args) do self.reactive_cache_worker_finder = ->(id, *_args) do
find_by(primary_key => id) find_by(primary_key => id)
end end
...@@ -71,6 +70,8 @@ module ReactiveCaching ...@@ -71,6 +70,8 @@ module ReactiveCaching
if within_reactive_cache_lifetime?(*args) if within_reactive_cache_lifetime?(*args)
enqueuing_update(*args) do enqueuing_update(*args) do
new_value = calculate_reactive_cache(*args) new_value = calculate_reactive_cache(*args)
check_exceeded_reactive_cache_limit!(new_value)
old_value = Rails.cache.read(key) old_value = Rails.cache.read(key)
Rails.cache.write(key, new_value) Rails.cache.write(key, new_value)
reactive_cache_updated(*args) if new_value != old_value reactive_cache_updated(*args) if new_value != old_value
...@@ -121,5 +122,13 @@ module ReactiveCaching ...@@ -121,5 +122,13 @@ module ReactiveCaching
ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args) ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args)
end end
def check_exceeded_reactive_cache_limit!(data)
return unless Feature.enabled?(:reactive_cache_limit)
data_deep_size = Gitlab::Utils::DeepSize.new(data, max_size: self.class.reactive_cache_hard_limit)
raise ExceededReactiveCacheLimit.new unless data_deep_size.valid?
end
end end
end end
...@@ -6,6 +6,7 @@ class Environment < ApplicationRecord ...@@ -6,6 +6,7 @@ class Environment < ApplicationRecord
self.reactive_cache_refresh_interval = 1.minute self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 55.seconds self.reactive_cache_lifetime = 55.seconds
self.reactive_cache_hard_limit = 10.megabytes
belongs_to :project, required: true belongs_to :project, required: true
......
...@@ -24,6 +24,7 @@ class MergeRequest < ApplicationRecord ...@@ -24,6 +24,7 @@ class MergeRequest < ApplicationRecord
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] } self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
self.reactive_cache_refresh_interval = 10.minutes self.reactive_cache_refresh_interval = 10.minutes
self.reactive_cache_lifetime = 10.minutes self.reactive_cache_lifetime = 10.minutes
self.reactive_cache_hard_limit = 20.megabytes
SORTING_PREFERENCE_FIELD = :merge_requests_sort SORTING_PREFERENCE_FIELD = :merge_requests_sort
......
...@@ -25,5 +25,7 @@ class ReactiveCachingWorker ...@@ -25,5 +25,7 @@ class ReactiveCachingWorker
.reactive_cache_worker_finder .reactive_cache_worker_finder
.call(id, *args) .call(id, *args)
.try(:exclusively_update_reactive_cache!, *args) .try(:exclusively_update_reactive_cache!, *args)
rescue ReactiveCaching::ExceededReactiveCacheLimit => e
Gitlab::ErrorTracking.track_exception(e)
end end
end end
---
title: Sets size limits on data loaded async, like deploy boards and merge request reports
merge_request: 21871
author:
type: changed
...@@ -87,6 +87,20 @@ Plan.default.limits.update!(ci_active_jobs: 500) ...@@ -87,6 +87,20 @@ Plan.default.limits.update!(ci_active_jobs: 500)
NOTE: **Note:** Set the limit to `0` to disable it. NOTE: **Note:** Set the limit to `0` to disable it.
## Environment data on Deploy Boards
[Deploy Boards](../user/project/deploy_boards.md) load information from Kubernetes about
Pods and Deployments. However, data over 10 MB for a certain environment read from
Kubernetes won't be shown.
## Merge Request reports
Reports that go over the 20 MB limit won't be loaded. Affected reports:
- [Merge Request security reports](../user/project/merge_requests/index.md#security-reports-ultimate)
- [CI/CD parameter `artifacts:expose_as`](../ci/yaml/README.md#artifactsexpose_as)
- [JUnit test reports](../ci/junit_test_reports.md)
## Advanced Global Search limits ## Advanced Global Search limits
### Maximum field length ### Maximum field length
......
...@@ -48,6 +48,12 @@ of the cache by the `reactive_cache_lifetime` value. ...@@ -48,6 +48,12 @@ of the cache by the `reactive_cache_lifetime` value.
Once the lifetime has expired, no more background jobs will be enqueued and calling Once the lifetime has expired, no more background jobs will be enqueued and calling
`#with_reactive_cache` will again return `nil` - starting the process all over again. `#with_reactive_cache` will again return `nil` - starting the process all over again.
### 1 MB hard limit
`ReactiveCaching` has a 1 megabyte default limit. [This value is configurable](#selfreactive_cache_worker_finder).
If the data we're trying to cache has over 1 megabyte, it will not be cached and a handled `ReactiveCaching::ExceededReactiveCacheLimit` will be notified on Sentry.
## When to use ## When to use
- If we need to make a request to an external API (for example, requests to the k8s API). - If we need to make a request to an external API (for example, requests to the k8s API).
...@@ -228,6 +234,16 @@ be reset to `reactive_cache_lifetime`. ...@@ -228,6 +234,16 @@ be reset to `reactive_cache_lifetime`.
self.reactive_cache_lifetime = 10.minutes self.reactive_cache_lifetime = 10.minutes
``` ```
#### `self.reactive_cache_hard_limit`
- This is the maximum data size that `ReactiveCaching` allows to be cached.
- The default is 1 megabyte. Data that goes over this value will not be cached
and will silently raise `ReactiveCaching::ExceededReactiveCacheLimit` on Sentry.
```ruby
self.reactive_cache_hard_limit = 5.megabytes
```
#### `self.reactive_cache_worker_finder` #### `self.reactive_cache_worker_finder`
- This is the method used by the background worker to find or generate the object on - This is the method used by the background worker to find or generate the object on
......
...@@ -165,11 +165,25 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do ...@@ -165,11 +165,25 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
describe '#exclusively_update_reactive_cache!' do describe '#exclusively_update_reactive_cache!' do
subject(:go!) { instance.exclusively_update_reactive_cache! } subject(:go!) { instance.exclusively_update_reactive_cache! }
shared_examples 'successful cache' do
it 'caches the result of #calculate_reactive_cache' do
go!
expect(read_reactive_cache(instance)).to eq(calculation.call)
end
it 'does not raise the exception' do
expect { go! }.not_to raise_exception(ReactiveCaching::ExceededReactiveCacheLimit)
end
end
context 'when the lease is free and lifetime is not exceeded' do context 'when the lease is free and lifetime is not exceeded' do
before do before do
stub_reactive_cache(instance, "preexisting") stub_reactive_cache(instance, 'preexisting')
end end
it_behaves_like 'successful cache'
it 'takes and releases the lease' do it 'takes and releases the lease' do
expect_to_obtain_exclusive_lease(cache_key, 'uuid') expect_to_obtain_exclusive_lease(cache_key, 'uuid')
expect_to_cancel_exclusive_lease(cache_key, 'uuid') expect_to_cancel_exclusive_lease(cache_key, 'uuid')
...@@ -177,19 +191,13 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do ...@@ -177,19 +191,13 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
go! go!
end end
it 'caches the result of #calculate_reactive_cache' do it 'enqueues a repeat worker' do
go!
expect(read_reactive_cache(instance)).to eq(calculation.call)
end
it "enqueues a repeat worker" do
expect_reactive_cache_update_queued(instance) expect_reactive_cache_update_queued(instance)
go! go!
end end
it "calls a reactive_cache_updated only once if content did not change on subsequent update" do it 'calls a reactive_cache_updated only once if content did not change on subsequent update' do
expect(instance).to receive(:calculate_reactive_cache).twice expect(instance).to receive(:calculate_reactive_cache).twice
expect(instance).to receive(:reactive_cache_updated).once expect(instance).to receive(:reactive_cache_updated).once
...@@ -202,6 +210,43 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do ...@@ -202,6 +210,43 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
go! go!
end end
context 'when calculated object size exceeds default reactive_cache_hard_limit' do
let(:calculation) { -> { 'a' * 2 * 1.megabyte } }
shared_examples 'ExceededReactiveCacheLimit' do
it 'raises ExceededReactiveCacheLimit exception and does not cache new data' do
expect { go! }.to raise_exception(ReactiveCaching::ExceededReactiveCacheLimit)
expect(read_reactive_cache(instance)).not_to eq(calculation.call)
end
end
context 'when reactive_cache_hard_limit feature flag is enabled' do
it_behaves_like 'ExceededReactiveCacheLimit'
context 'when reactive_cache_hard_limit is overridden' do
let(:test_class) { Class.new(CacheTest) { self.reactive_cache_hard_limit = 3.megabytes } }
let(:instance) { test_class.new(666, &calculation) }
it_behaves_like 'successful cache'
context 'when cache size is over the overridden limit' do
let(:calculation) { -> { 'a' * 4 * 1.megabyte } }
it_behaves_like 'ExceededReactiveCacheLimit'
end
end
end
context 'when reactive_cache_limit feature flag is disabled' do
before do
stub_feature_flags(reactive_cache_limit: false)
end
it_behaves_like 'successful cache'
end
end
context 'and #calculate_reactive_cache raises an exception' do context 'and #calculate_reactive_cache raises an exception' do
before do before do
stub_reactive_cache(instance, "preexisting") stub_reactive_cache(instance, "preexisting")
...@@ -256,8 +301,8 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do ...@@ -256,8 +301,8 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
it { expect(subject.reactive_cache_lease_timeout).to be_a(ActiveSupport::Duration) } it { expect(subject.reactive_cache_lease_timeout).to be_a(ActiveSupport::Duration) }
it { expect(subject.reactive_cache_refresh_interval).to be_a(ActiveSupport::Duration) } it { expect(subject.reactive_cache_refresh_interval).to be_a(ActiveSupport::Duration) }
it { expect(subject.reactive_cache_lifetime).to be_a(ActiveSupport::Duration) } it { expect(subject.reactive_cache_lifetime).to be_a(ActiveSupport::Duration) }
it { expect(subject.reactive_cache_key).to respond_to(:call) } it { expect(subject.reactive_cache_key).to respond_to(:call) }
it { expect(subject.reactive_cache_hard_limit).to be_a(Integer) }
it { expect(subject.reactive_cache_worker_finder).to respond_to(:call) } it { expect(subject.reactive_cache_worker_finder).to respond_to(:call) }
end end
end end
...@@ -14,6 +14,18 @@ describe ReactiveCachingWorker do ...@@ -14,6 +14,18 @@ describe ReactiveCachingWorker do
described_class.new.perform("Environment", environment.id) described_class.new.perform("Environment", environment.id)
end end
context 'when ReactiveCaching::ExceededReactiveCacheLimit is raised' do
it 'avoids failing the job and tracks via Gitlab::ErrorTracking' do
allow_any_instance_of(Environment).to receive(:exclusively_update_reactive_cache!)
.and_raise(ReactiveCaching::ExceededReactiveCacheLimit)
expect(Gitlab::ErrorTracking).to receive(:track_exception)
.with(kind_of(ReactiveCaching::ExceededReactiveCacheLimit))
described_class.new.perform("Environment", environment.id)
end
end
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment