|
1 | 1 | package db |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "math/rand" |
4 | 5 | "time" |
5 | 6 | ) |
6 | 7 |
|
@@ -47,18 +48,35 @@ func StartActiveExpiry(stop <-chan struct{}) { |
47 | 48 | }() |
48 | 49 | } |
49 | 50 |
|
50 | | -// cleanExpiredKeys scans up to chunkSize keys from KeyTTL, deletes any that |
51 | | -// have expired, and returns the number of keys scanned and the number deleted. |
| 51 | +// cleanExpiredKeys samples up to chunkSize keys from KeyTTL in random order, |
| 52 | +// deletes any that have expired, and returns the number of keys sampled and |
| 53 | +// the number deleted. |
| 54 | +// Randomisation ensures that all keys get a fair chance of being checked |
| 55 | +// regardless of insertion order, avoiding the bias that sync.Map.Range's |
| 56 | +// consistent iteration order would otherwise introduce. |
52 | 57 | func cleanExpiredKeys() (total int, expired int) { |
| 58 | + // Collect all keys that have a TTL set. |
| 59 | + var keys []any |
| 60 | + KeyTTL.Range(func(key, _ any) bool { |
| 61 | + keys = append(keys, key) |
| 62 | + return true |
| 63 | + }) |
| 64 | + |
| 65 | + // Shuffle so every key has an equal chance of landing in the sample window. |
| 66 | + rand.Shuffle(len(keys), func(i, j int) { keys[i], keys[j] = keys[j], keys[i] }) |
| 67 | + |
53 | 68 | now := time.Now().UnixMilli() |
54 | | - KeyTTL.Range(func(key, value any) bool { |
| 69 | + for _, key := range keys { |
| 70 | + if total >= chunkSize { |
| 71 | + break |
| 72 | + } |
55 | 73 | total++ |
56 | | - if value != nil && value.(int64) < now { |
| 74 | + value, ok := KeyTTL.Load(key) |
| 75 | + if ok && value != nil && value.(int64) < now { |
57 | 76 | KeyTTL.Delete(key) |
58 | 77 | DB.Delete(key) |
59 | 78 | expired++ |
60 | 79 | } |
61 | | - return total < chunkSize |
62 | | - }) |
| 80 | + } |
63 | 81 | return |
64 | 82 | } |
0 commit comments