Skip to content

Commit 9c11f64

Browse files
authored
Merge pull request #5030 from DataDog/no-ticket-extract-lru-cache-into-core-utils
[NO-TICKET] Extract AppSec LRUCache implementation into Core::Utils
2 parents 20f7cba + 874b7f7 commit 9c11f64

File tree

6 files changed

+61
-111
lines changed

6 files changed

+61
-111
lines changed

lib/datadog/appsec/api_security/sampler.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# frozen_string_literal: true
22

33
require 'zlib'
4-
require_relative 'lru_cache'
54
require_relative 'route_extractor'
65
require_relative '../../core/utils/time'
6+
require_relative '../../core/utils/lru_cache'
77

88
module Datadog
99
module AppSec
@@ -37,7 +37,7 @@ def sample_delay
3737
def initialize(sample_delay)
3838
raise ArgumentError, 'sample_delay must be an Integer' unless sample_delay.is_a?(Integer)
3939

40-
@cache = LRUCache.new(MAX_CACHE_SIZE)
40+
@cache = Core::Utils::LRUCache.new(MAX_CACHE_SIZE)
4141
@sample_delay_seconds = sample_delay
4242
end
4343

@@ -51,7 +51,7 @@ def sample?(request, response)
5151

5252
return false if current_timestamp - cached_timestamp <= @sample_delay_seconds
5353

54-
@cache.store(key, current_timestamp)
54+
@cache[key] = current_timestamp
5555
true
5656
end
5757
end

lib/datadog/appsec/api_security/lru_cache.rb renamed to lib/datadog/core/utils/lru_cache.rb

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
require 'forwardable'
44

55
module Datadog
6-
module AppSec
7-
module APISecurity
6+
module Core
7+
module Utils
88
# An LRU (Least Recently Used) cache implementation that relies on the
99
# Ruby 1.9+ `Hash` implementation that guarantees insertion order.
1010
#
1111
# WARNING: This implementation is NOT thread-safe and should be used
12-
# in a single-threaded context.
12+
# in a single-threaded context or guarded by Mutex.
1313
class LRUCache
1414
extend Forwardable
1515

@@ -30,25 +30,14 @@ def [](key)
3030
end
3131
end
3232

33-
def store(key, value)
34-
return @store[key] = value if @store.delete(key)
35-
36-
# NOTE: evict the oldest entry if store reached the maximum allowed size
37-
@store.shift if @store.size >= @max_size
38-
@store[key] = value
39-
end
40-
41-
# NOTE: If the key exists, it's moved to the end of the list and
42-
# if does not, the given block will be executed and the result
43-
# will be stored (which will add it to the end of the list).
44-
def fetch_or_store(key)
45-
if (entry = @store.delete(key))
46-
return @store[key] = entry
33+
def []=(key, value)
34+
if @store.delete(key)
35+
@store[key] = value
36+
else
37+
# NOTE: evict the oldest entry if store reached the maximum allowed size
38+
@store.shift if @store.size >= @max_size
39+
@store[key] = value
4740
end
48-
49-
# NOTE: evict the oldest entry if store reached the maximum allowed size
50-
@store.shift if @store.size >= @max_size
51-
@store[key] = yield
5241
end
5342
end
5443
end

sig/datadog/appsec/api_security/lru_cache.rbs

Lines changed: 0 additions & 25 deletions
This file was deleted.

sig/datadog/appsec/api_security/sampler.rbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module Datadog
66

77
MAX_CACHE_SIZE: Integer
88

9-
@cache: LRUCache
9+
@cache: Core::Utils::LRUCache
1010

1111
@sample_delay_seconds: Integer
1212

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
module Datadog
2+
module Core
3+
module Utils
4+
class LRUCache
5+
extend Forwardable
6+
7+
@store: ::Hash[untyped, untyped]
8+
9+
@max_size: ::Integer
10+
11+
def initialize: (::Integer max_size) -> void
12+
13+
def []: (untyped key) -> untyped?
14+
15+
def []=: (untyped key, untyped value) -> untyped
16+
17+
def clear: () -> void
18+
19+
def empty?: () -> bool
20+
end
21+
end
22+
end
23+
end

spec/datadog/appsec/api_security/lru_cache_spec.rb renamed to spec/datadog/core/utils/lru_cache_spec.rb

Lines changed: 24 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# frozen_string_literal: true
22

33
require 'spec_helper'
4-
require 'datadog/appsec/api_security/lru_cache'
4+
require 'datadog/core/utils/lru_cache'
55

6-
RSpec.describe Datadog::AppSec::APISecurity::LRUCache do
6+
RSpec.describe Datadog::Core::Utils::LRUCache do
77
describe '#initialize' do
88
it { expect(described_class.new(3)).to be_empty }
99
it { expect { described_class.new('0') }.to raise_error(ArgumentError) }
@@ -19,19 +19,19 @@
1919
end
2020

2121
context 'when key exists' do
22-
before { lru_cache.fetch_or_store(:key) { 'value' } }
22+
before { lru_cache[:key] = 'value' }
2323

2424
it { expect(lru_cache[:key]).to eq('value') }
2525
end
2626

2727
context 'when key exists and is accessed' do
2828
it 'updates the key order' do
29-
lru_cache.fetch_or_store(:key1) { 'value1' }
30-
lru_cache.fetch_or_store(:key2) { 'value2' }
31-
lru_cache.fetch_or_store(:key3) { 'value3' }
29+
lru_cache[:key1] = 'value1'
30+
lru_cache[:key2] = 'value2'
31+
lru_cache[:key3] = 'value3'
3232

3333
lru_cache[:key1] # NOTE: as key accessed, it's moved to the end of the list
34-
lru_cache.fetch_or_store(:key4) { 'value4' }
34+
lru_cache[:key4] = 'value4'
3535

3636
aggregate_failures 'verifying LRU cache state after key access and eviction' do
3737
expect(lru_cache[:key2]).to be_nil
@@ -43,30 +43,30 @@
4343
end
4444
end
4545

46-
describe '#store' do
46+
describe '#[]=' do
4747
let(:lru_cache) { described_class.new(3) }
4848

4949
it 'stores a key-value pair' do
50-
expect { lru_cache.store(:key, 'value') }.to change { lru_cache[:key] }
50+
expect { lru_cache[:key] = 'value' }.to change { lru_cache[:key] }
5151
.from(nil).to('value')
5252
end
5353

5454
it 'overwrites existing values' do
55-
lru_cache.store(:key, 'old-value')
55+
lru_cache[:key] = 'old-value'
5656

57-
expect { lru_cache.store(:key, 'new-value') }.to change { lru_cache[:key] }
57+
expect { lru_cache[:key] = 'new-value' }.to change { lru_cache[:key] }
5858
.from('old-value').to('new-value')
5959
end
6060

6161
context 'when cache is full and an existing key is updated' do
6262
it 'does not remove other entries' do
63-
lru_cache.store(:key2, 'value2')
64-
lru_cache.store(:key3, 'value3')
65-
lru_cache.store(:key1, 'value1')
63+
lru_cache[:key2] = 'value2'
64+
lru_cache[:key3] = 'value3'
65+
lru_cache[:key1] = 'value1'
6666

67-
lru_cache.store(:key1, 'new-value1')
68-
lru_cache.store(:key3, 'new-value3')
69-
lru_cache.store(:key2, 'new-value2')
67+
lru_cache[:key1] = 'new-value1'
68+
lru_cache[:key3] = 'new-value3'
69+
lru_cache[:key2] = 'new-value2'
7070

7171
aggregate_failures 'verifying LRU cache contents' do
7272
expect(lru_cache[:key1]).to eq('new-value1')
@@ -78,47 +78,10 @@
7878

7979
context 'when maximum size is reached' do
8080
it 'evicts the least recently used item' do
81-
lru_cache.store(:key1, 'value1')
82-
lru_cache.store(:key2, 'value2')
83-
lru_cache.store(:key3, 'value3')
84-
lru_cache.store(:key4, 'value4')
85-
86-
aggregate_failures 'verifying LRU cache state after eviction' do
87-
expect(lru_cache[:key1]).to be_nil
88-
expect(lru_cache[:key2]).to eq('value2')
89-
expect(lru_cache[:key3]).to eq('value3')
90-
expect(lru_cache[:key4]).to eq('value4')
91-
end
92-
end
93-
end
94-
end
95-
96-
describe '#fetch_or_store' do
97-
context 'when key does not exist' do
98-
let(:lru_cache) { described_class.new(3) }
99-
100-
it 'computes and stores the value' do
101-
expect(lru_cache.fetch_or_store(:key) { 'value' }).to eq('value')
102-
expect(lru_cache[:key]).to eq('value')
103-
end
104-
105-
it 'computes the missing key only once' do
106-
expect { lru_cache.fetch_or_store(:key) { 'value' } }
107-
.to change { lru_cache[:key] }.from(nil).to('value')
108-
109-
expect { lru_cache.fetch_or_store(:key) { 'new-value' } }
110-
.not_to(change { lru_cache[:key] })
111-
end
112-
end
113-
114-
context 'when maximum size is reached' do
115-
let(:lru_cache) { described_class.new(3) }
116-
117-
it 'evicts the least recently used item' do
118-
lru_cache.fetch_or_store(:key1) { 'value1' }
119-
lru_cache.fetch_or_store(:key2) { 'value2' }
120-
lru_cache.fetch_or_store(:key3) { 'value3' }
121-
lru_cache.fetch_or_store(:key4) { 'value4' }
81+
lru_cache[:key1] = 'value1'
82+
lru_cache[:key2] = 'value2'
83+
lru_cache[:key3] = 'value3'
84+
lru_cache[:key4] = 'value4'
12285

12386
aggregate_failures 'verifying LRU cache state after eviction' do
12487
expect(lru_cache[:key1]).to be_nil
@@ -134,8 +97,8 @@
13497
let(:lru_cache) { described_class.new(3) }
13598

13699
it 'removes all items from the cache' do
137-
lru_cache.fetch_or_store(:key1) { 'value1' }
138-
lru_cache.fetch_or_store(:key2) { 'value2' }
100+
lru_cache[:key1] = 'value1'
101+
lru_cache[:key2] = 'value2'
139102

140103
expect { lru_cache.clear }.to change { lru_cache[:key1] }.from('value1').to(nil)
141104
.and change { lru_cache[:key2] }.from('value2').to(nil)
@@ -147,7 +110,7 @@
147110
let(:lru_cache) { described_class.new(3) }
148111

149112
it 'returns false when cache has items' do
150-
expect { lru_cache.fetch_or_store(:key) { 'value' } }
113+
expect { lru_cache[:key] = 'value' }
151114
.to change { lru_cache.empty? }.from(true).to(false)
152115
end
153116
end

0 commit comments

Comments
 (0)