Skip to content

Commit e4d9ac4

Browse files
VeronikaSolovei9VeronikaSolovei9
andauthored
Rules Engine Module: Configurable cache update frequency (#4423)
Co-authored-by: VeronikaSolovei9 <vsolovei@microsoft.com>
1 parent bd3f8d5 commit e4d9ac4

File tree

6 files changed

+184
-79
lines changed

6 files changed

+184
-79
lines changed

modules/prebid/rulesengine/cache.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package rulesengine
33
import (
44
"sync"
55
"sync/atomic"
6+
"time"
7+
8+
"github.com/prebid/prebid-server/v3/util/timeutil"
69
)
710

811
type accountID = string
@@ -11,20 +14,34 @@ type cacher interface {
1114
Get(string) *cacheEntry
1215
Set(string, *cacheEntry)
1316
Delete(id accountID)
17+
Expired(time.Time) bool
1418
}
1519

1620
type cache struct {
1721
sync.Mutex
18-
m atomic.Value
22+
m atomic.Value
23+
refreshFrequency time.Duration
24+
t timeutil.Time
1925
}
2026

21-
func NewCache() *cache {
27+
func NewCache(refreshRateSeconds int) *cache {
2228
var atomicMap atomic.Value
2329
atomicMap.Store(make(map[accountID]*cacheEntry))
2430

2531
return &cache{
26-
m: atomicMap,
32+
m: atomicMap,
33+
refreshFrequency: time.Duration(refreshRateSeconds) * time.Second,
34+
t: &timeutil.RealTime{},
35+
}
36+
}
37+
38+
func (c *cache) Expired(coTimestamp time.Time) bool {
39+
if c.refreshFrequency <= 0 {
40+
return false
2741
}
42+
currentTime := c.t.Now()
43+
delta := currentTime.Sub(coTimestamp)
44+
return delta.Seconds() > c.refreshFrequency.Seconds()
2845
}
2946

3047
// Get has been implemented to read from the cache without further synchronization

modules/prebid/rulesengine/cache_test.go

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package rulesengine
22

33
import (
44
"testing"
5+
"time"
56

67
"github.com/stretchr/testify/assert"
78
)
@@ -12,7 +13,7 @@ func TestGet(t *testing.T) {
1213
hashedConfig: "hash1",
1314
},
1415
}
15-
cache := NewCache()
16+
cache := NewCache(0)
1617
cache.m.Store(innerStorage)
1718

1819
testCases := []struct {
@@ -47,7 +48,7 @@ func TestSet(t *testing.T) {
4748
hashedConfig: "hash1",
4849
},
4950
}
50-
cache := NewCache()
51+
cache := NewCache(0)
5152
cache.m.Store(innerStorage)
5253

5354
testCases := []struct {
@@ -161,7 +162,7 @@ func TestDelete(t *testing.T) {
161162
},
162163
}
163164

164-
cache := NewCache()
165+
cache := NewCache(0)
165166
cache.m.Store(originalInnerStorage)
166167

167168
for _, tc := range testCases {
@@ -174,3 +175,46 @@ func TestDelete(t *testing.T) {
174175
})
175176
}
176177
}
178+
179+
func TestExpired(t *testing.T) {
180+
testCases := []struct {
181+
name string
182+
inTimestamp time.Time
183+
refreshRateSeconds int
184+
expectedResult bool
185+
}{
186+
{
187+
name: "expired",
188+
inTimestamp: mockTimeUtil{}.Now().Add(-6 * time.Second),
189+
refreshRateSeconds: 5,
190+
expectedResult: true,
191+
},
192+
{
193+
name: "not_expired",
194+
inTimestamp: mockTimeUtil{}.Now().Add(-4 * time.Second),
195+
refreshRateSeconds: 5,
196+
expectedResult: false,
197+
},
198+
{
199+
name: "no_refresh_rate",
200+
inTimestamp: mockTimeUtil{}.Now().Add(-10 * time.Second),
201+
refreshRateSeconds: 0,
202+
expectedResult: false,
203+
},
204+
}
205+
206+
for _, tc := range testCases {
207+
t.Run(tc.name, func(t *testing.T) {
208+
c := NewCache(tc.refreshRateSeconds)
209+
c.t = mockTimeUtil{}
210+
res := c.Expired(tc.inTimestamp)
211+
assert.Equal(t, tc.expectedResult, res)
212+
})
213+
}
214+
}
215+
216+
type mockTimeUtil struct{}
217+
218+
func (mt mockTimeUtil) Now() time.Time {
219+
return time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
220+
}

modules/prebid/rulesengine/module.go

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,18 @@ import (
55
"crypto/sha256"
66
"encoding/hex"
77
"encoding/json"
8-
"time"
8+
9+
"github.com/buger/jsonparser"
910

1011
hs "github.com/prebid/prebid-server/v3/hooks/hookstage"
1112
"github.com/prebid/prebid-server/v3/modules/moduledeps"
1213
"github.com/prebid/prebid-server/v3/modules/prebid/rulesengine/config"
13-
"github.com/prebid/prebid-server/v3/util/timeutil"
1414
)
1515

16-
const fiveMinutes = time.Duration(300) * time.Second
17-
1816
// Builder configures the rules engine module initiating an in-memory cache and kicking
1917
// off a go routine that builds tree structures that represent rule sets optimized for finding
2018
// a rule to applies for a given request.
21-
func Builder(_ json.RawMessage, _ moduledeps.ModuleDeps) (interface{}, error) {
19+
func Builder(cfg json.RawMessage, _ moduledeps.ModuleDeps) (interface{}, error) {
2220
schemaValidator, err := config.CreateSchemaValidator(config.RulesEngineSchemaFilePath)
2321
if err != nil {
2422
return nil, err
@@ -30,7 +28,13 @@ func Builder(_ json.RawMessage, _ moduledeps.ModuleDeps) (interface{}, error) {
3028
schemaValidator: schemaValidator,
3129
monitor: &treeManagerLogger{},
3230
}
33-
c := NewCache()
31+
32+
refreshRate, err := getRefreshRate(cfg)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
c := NewCache(refreshRate)
3438

3539
go tm.Run(c)
3640

@@ -77,7 +81,7 @@ func (m Module) HandleProcessedAuctionHook(
7781
}, nil
7882
}
7983
// cache hit
80-
if rebuildTrees(co, &miCtx.AccountConfig) {
84+
if rebuildTrees(co, &miCtx.AccountConfig, m.Cache) {
8185
bi := buildInstruction{
8286
accountID: miCtx.AccountID,
8387
config: &miCtx.AccountConfig,
@@ -103,25 +107,14 @@ func (m Module) Shutdown() {
103107
<-m.TreeManager.done
104108
}
105109

106-
// rebuildTrees returns true if the trees for this account need to be rebuilt; false otherwise
107-
func rebuildTrees(co *cacheEntry, jsonConfig *json.RawMessage) bool {
108-
if !expired(&timeutil.RealTime{}, co.timestamp) {
110+
// rebuildTrees returns true if the trees need to be rebuilt; false otherwise
111+
func rebuildTrees(co *cacheEntry, jsonConfig *json.RawMessage, cacher cacher) bool {
112+
if !cacher.Expired(co.timestamp) {
109113
return false
110114
}
111115
return configChanged(co.hashedConfig, jsonConfig)
112116
}
113117

114-
// expired returns true if the refresh time has expired; false otherwise
115-
func expired(t timeutil.Time, ts time.Time) bool {
116-
currentTime := t.Now().UTC()
117-
118-
delta := currentTime.Sub(ts.UTC())
119-
if delta.Seconds() > fiveMinutes.Seconds() {
120-
return true
121-
}
122-
return false
123-
}
124-
125118
// configChanged hashes the raw JSON config comparing it with the old hash returning
126119
// true with the new hash if the hashes are different and false otherwise
127120
func configChanged(oldHash hash, data *json.RawMessage) bool {
@@ -136,3 +129,15 @@ func configChanged(oldHash hash, data *json.RawMessage) bool {
136129
}
137130
return false
138131
}
132+
133+
func getRefreshRate(jsonCfg json.RawMessage) (int, error) {
134+
updateFrequency, err := jsonparser.GetInt(jsonCfg, "refreshrateseconds")
135+
136+
if err == jsonparser.KeyPathNotFoundError {
137+
return 0, nil
138+
}
139+
if err != nil {
140+
return 0, err
141+
}
142+
return int(updateFrequency), nil
143+
}

modules/prebid/rulesengine/module_test.go

Lines changed: 88 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,9 @@ import (
55
"testing"
66
"time"
77

8-
"github.com/prebid/prebid-server/v3/util/timeutil"
98
"github.com/stretchr/testify/assert"
109
)
1110

12-
func TestExpired(t *testing.T) {
13-
testCases := []struct {
14-
name string
15-
inTime timeutil.Time
16-
inTimestamp time.Time
17-
expectedResult bool
18-
}{
19-
{
20-
name: "expired",
21-
inTime: mockTimeUtil{},
22-
inTimestamp: mockTimeUtil{}.Now().Add(-time.Hour),
23-
expectedResult: true,
24-
},
25-
{
26-
name: "not_expired",
27-
inTime: mockTimeUtil{},
28-
inTimestamp: mockTimeUtil{}.Now().Add(time.Hour),
29-
expectedResult: false,
30-
},
31-
}
32-
33-
for _, tc := range testCases {
34-
t.Run(tc.name, func(t *testing.T) {
35-
res := expired(tc.inTime, tc.inTimestamp)
36-
assert.Equal(t, tc.expectedResult, res)
37-
})
38-
}
39-
}
40-
41-
type mockTimeUtil struct{}
42-
43-
func (mt mockTimeUtil) Now() time.Time {
44-
return time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
45-
}
46-
4711
var sampleJsonConfig json.RawMessage = json.RawMessage(`{"enabled": true, "ruleSets": []}`)
4812

4913
func TestConfigChanged(t *testing.T) {
@@ -84,42 +48,117 @@ func TestConfigChanged(t *testing.T) {
8448

8549
func TestRebuildTrees(t *testing.T) {
8650
testCases := []struct {
87-
name string
88-
inCacheEntry *cacheEntry
89-
inJsonConfig *json.RawMessage
90-
expectedResult bool
51+
name string
52+
inCacheEntry *cacheEntry
53+
inJsonConfig *json.RawMessage
54+
refreshRateSeconds int
55+
expectedResult bool
9156
}{
9257
{
9358
name: "non_expired_cache_entry_so_no_rebuild",
9459
inCacheEntry: &cacheEntry{
95-
timestamp: time.Date(2050, 1, 1, 0, 0, 0, 0, time.UTC),
60+
timestamp: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
9661
},
97-
expectedResult: false,
62+
inJsonConfig: &sampleJsonConfig,
63+
refreshRateSeconds: 10,
64+
expectedResult: false,
9865
},
9966
{
100-
name: "expired_entry_but_same_config_so_no_rebuild",
67+
name: "expired_entry_but_same_config_and_default_no_update_so_no_rebuild",
10168
inCacheEntry: &cacheEntry{
10269
timestamp: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
10370
hashedConfig: "e21c19982a618f9dd3286fc2eb08dad62a1e9ee81d51ffa94b267ab2e3813964",
10471
},
105-
inJsonConfig: &sampleJsonConfig,
106-
expectedResult: false,
72+
inJsonConfig: &sampleJsonConfig,
73+
refreshRateSeconds: 1,
74+
expectedResult: false,
10775
},
10876
{
109-
name: "expired_entry_and_different_config_so_rebuild",
77+
name: "expired_entry_but_same_config_and_zero_minutes_update_so_no_rebuild",
11078
inCacheEntry: &cacheEntry{
11179
timestamp: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
80+
hashedConfig: "e21c19982a618f9dd3286fc2eb08dad62a1e9ee81d51ffa94b267ab2e3813964",
81+
},
82+
inJsonConfig: &sampleJsonConfig,
83+
refreshRateSeconds: 0,
84+
expectedResult: false,
85+
},
86+
{
87+
name: "expired_entry_and_different_config_so_rebuild",
88+
inCacheEntry: &cacheEntry{
89+
timestamp: time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC),
11290
hashedConfig: "oldHash",
11391
},
114-
inJsonConfig: &sampleJsonConfig,
115-
expectedResult: true,
92+
inJsonConfig: &sampleJsonConfig,
93+
refreshRateSeconds: 1,
94+
expectedResult: true,
11695
},
11796
}
11897

11998
for _, tc := range testCases {
12099
t.Run(tc.name, func(t *testing.T) {
121-
res := rebuildTrees(tc.inCacheEntry, tc.inJsonConfig)
100+
refreshFreq := time.Duration(tc.refreshRateSeconds) * time.Second
101+
var c cacher = &cache{
102+
refreshFrequency: refreshFreq,
103+
t: mockTimeUtil{},
104+
}
105+
res := rebuildTrees(tc.inCacheEntry, tc.inJsonConfig, c)
122106
assert.Equal(t, tc.expectedResult, res)
123107
})
124108
}
125109
}
110+
111+
func TestGetRefreshRate(t *testing.T) {
112+
113+
testCases := []struct {
114+
name string
115+
inData json.RawMessage
116+
expectedRefreshRate int
117+
expectError bool
118+
}{
119+
{
120+
name: "nil_data",
121+
inData: nil,
122+
expectedRefreshRate: 0,
123+
},
124+
{
125+
name: "valid_config",
126+
inData: json.RawMessage(`{"enabled": true, "refreshrateseconds": 10}`),
127+
expectedRefreshRate: 10,
128+
},
129+
{
130+
name: "valid_config_negative_refresh_rate",
131+
inData: json.RawMessage(`{"enabled": true, "refreshrateseconds": -10}`),
132+
expectedRefreshRate: -10,
133+
},
134+
{
135+
name: "valid_config_no_refresh_rate",
136+
inData: json.RawMessage(`{"enabled": true}`),
137+
expectedRefreshRate: 0,
138+
},
139+
{
140+
name: "invalid_config",
141+
inData: json.RawMessage(`{"enabled": true, "refreshrateseconds": "test"}`),
142+
expectedRefreshRate: 0,
143+
expectError: true,
144+
},
145+
{
146+
name: "path_not_foud",
147+
inData: json.RawMessage(`{"enabled": true, "test": 10}`),
148+
expectedRefreshRate: 0,
149+
expectError: false,
150+
},
151+
}
152+
153+
for _, tc := range testCases {
154+
t.Run(tc.name, func(t *testing.T) {
155+
res, err := getRefreshRate(tc.inData)
156+
if tc.expectError {
157+
assert.Error(t, err, "Expected an error but got none")
158+
} else {
159+
assert.NoError(t, err, "Expected no error but got one")
160+
}
161+
assert.Equal(t, tc.expectedRefreshRate, res)
162+
})
163+
}
164+
}

0 commit comments

Comments
 (0)