Skip to content

Commit 314e4ed

Browse files
authored
fix(cardinality): Fix edge case where keys have no expiry (#3058)
In the case with "perfect" cardinality, no additional values and below the limit, it was possible that new sets were correctly created but their expiry was not updated.
1 parent f2e83c3 commit 314e4ed

File tree

5 files changed

+82
-8
lines changed

5 files changed

+82
-8
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

relay-cardinality/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ thiserror = { workspace = true }
2828
[dev-dependencies]
2929
criterion = { workspace = true }
3030
serde_json = { workspace = true }
31+
uuid = { workspace = true }
3132

3233
[[bench]]
3334
name = "redis"

relay-cardinality/src/redis/cardinality.lua

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,20 +86,23 @@ local results = {
8686
--
8787
-- Returns the total amount of values added to the 'working set'.
8888
local function sadd(t, offset, max)
89-
local added = 0;
89+
local working_set_cardinality = 0
90+
local any_modifications = false
9091

9192
for i = 1, #KEYS do
9293
local is_working_set = i == 1
9394

9495
for from, to in batches(#t, 7000, offset, max) do
9596
local r = redis.call('SADD', KEYS[i], unpack(t, from, to))
97+
98+
any_modifications = any_modifications or r > 0
9699
if is_working_set then
97-
added = added + r
100+
working_set_cardinality = working_set_cardinality + r
98101
end
99102
end
100103
end
101104

102-
return added
105+
return working_set_cardinality, any_modifications
103106
end
104107

105108
-- Bumps to expiry of all sets by the passed expiry
@@ -116,15 +119,15 @@ local budget = math.max(0, max_cardinality - current_cardinality)
116119

117120
-- Fast Path: we have enough budget to fit all elements
118121
if budget >= num_hashes then
119-
local added = sadd(ARGV, HASHES_OFFSET)
122+
local added, any_modifications = sadd(ARGV, HASHES_OFFSET)
120123
-- New current cardinality is current + amount of keys that have been added to the set
121124
current_cardinality = current_cardinality + added
122125

123126
for _ = 1, num_hashes do
124127
table.insert(results, ACCEPTED)
125128
end
126129

127-
if added > 0 then
130+
if any_modifications then
128131
bump_expiry()
129132
end
130133

@@ -137,10 +140,10 @@ local offset = HASHES_OFFSET
137140
local needs_expiry_bumped = false
138141
while budget > 0 and offset < #ARGV do
139142
local len = math.min(#ARGV - offset, budget)
140-
local added = sadd(ARGV, offset, len)
143+
local added, any_modifications = sadd(ARGV, offset, len)
141144

142145
current_cardinality = current_cardinality + added
143-
needs_expiry_bumped = needs_expiry_bumped or added > 0
146+
needs_expiry_bumped = needs_expiry_bumped or any_modifications
144147

145148
for _ = 1, len do
146149
table.insert(results, ACCEPTED)

relay-cardinality/src/redis/limiter.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,11 @@ mod tests {
320320
let url = std::env::var("RELAY_REDIS_URL")
321321
.unwrap_or_else(|_| "redis://127.0.0.1:6379".to_owned());
322322

323-
let redis = RedisPool::single(&url, RedisConfigOptions::default()).unwrap();
323+
let opts = RedisConfigOptions {
324+
max_connections: 1,
325+
..Default::default()
326+
};
327+
let redis = RedisPool::single(&url, opts).unwrap();
324328

325329
RedisSetLimiter::new(
326330
RedisSetLimiterOptions {

relay-cardinality/src/redis/script.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,68 @@ impl CardinalityScript {
119119
Ok(result)
120120
}
121121
}
122+
123+
#[cfg(test)]
124+
mod tests {
125+
use relay_redis::{RedisConfigOptions, RedisPool};
126+
use uuid::Uuid;
127+
128+
use super::*;
129+
130+
fn build_redis() -> RedisPool {
131+
let url = std::env::var("RELAY_REDIS_URL")
132+
.unwrap_or_else(|_| "redis://127.0.0.1:6379".to_owned());
133+
134+
let opts = RedisConfigOptions {
135+
max_connections: 1,
136+
..Default::default()
137+
};
138+
RedisPool::single(&url, opts).unwrap()
139+
}
140+
141+
fn keys(prefix: Uuid, keys: &[&str]) -> impl Iterator<Item = String> {
142+
keys.iter()
143+
.map(move |key| format!("{prefix}-{key}"))
144+
.collect::<Vec<_>>()
145+
.into_iter()
146+
}
147+
148+
fn assert_ttls(connection: &mut Connection, prefix: Uuid) {
149+
let keys = redis::cmd("KEYS")
150+
.arg(format!("{prefix}-*"))
151+
.query::<Vec<String>>(connection)
152+
.unwrap();
153+
154+
for key in keys {
155+
let ttl = redis::cmd("TTL")
156+
.arg(&key)
157+
.query::<i64>(connection)
158+
.unwrap();
159+
160+
assert!(ttl >= 0, "Key {key} has no TTL");
161+
}
162+
}
163+
164+
#[test]
165+
fn test_below_limit_perfect_cardinality_ttl() {
166+
let redis = build_redis();
167+
let mut client = redis.client().unwrap();
168+
let mut connection = client.connection().unwrap();
169+
170+
let script = CardinalityScript::load();
171+
172+
let prefix = Uuid::new_v4();
173+
let k1 = &["a", "b", "c"];
174+
let k2 = &["b", "c", "d"];
175+
176+
script
177+
.invoke(&mut connection, 50, 3600, 0..30, keys(prefix, k1))
178+
.unwrap();
179+
180+
script
181+
.invoke(&mut connection, 50, 3600, 0..30, keys(prefix, k2))
182+
.unwrap();
183+
184+
assert_ttls(&mut connection, prefix);
185+
}
186+
}

0 commit comments

Comments
 (0)