cache: add negative cache; router: skip race for cached 404s
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ibeb44b313850395898bb20f2d947b0b76a6a6964
This commit is contained in:
parent
f100342720
commit
b0ea022dc2
7 changed files with 147 additions and 4 deletions
31
internal/cache/db.go
vendored
31
internal/cache/db.go
vendored
|
|
@ -80,6 +80,11 @@ func migrate(db *sql.DB) error {
|
|||
total_queries INTEGER DEFAULT 0,
|
||||
success_rate REAL DEFAULT 1.0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS negative_cache (
|
||||
store_path TEXT PRIMARY KEY,
|
||||
expires_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_negative_expires ON negative_cache(expires_at);
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
|
@ -178,6 +183,32 @@ func (d *DB) RouteCount() (int, error) {
|
|||
return count, err
|
||||
}
|
||||
|
||||
// Records a negative cache entry for storePath with the given TTL.
|
||||
func (d *DB) SetNegative(storePath string, ttl time.Duration) error {
|
||||
_, err := d.db.Exec(
|
||||
`INSERT INTO negative_cache (store_path, expires_at) VALUES (?, ?)
|
||||
ON CONFLICT(store_path) DO UPDATE SET expires_at = excluded.expires_at`,
|
||||
storePath, time.Now().Add(ttl).Unix(),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Returns true if a non-expired negative entry exists for storePath.
|
||||
func (d *DB) IsNegative(storePath string) (bool, error) {
|
||||
var count int
|
||||
err := d.db.QueryRow(
|
||||
`SELECT COUNT(*) FROM negative_cache WHERE store_path = ? AND expires_at > ?`,
|
||||
storePath, time.Now().Unix(),
|
||||
).Scan(&count)
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// Deletes expired negative cache entries.
|
||||
func (d *DB) ExpireNegatives() error {
|
||||
_, err := d.db.Exec(`DELETE FROM negative_cache WHERE expires_at < ?`, time.Now().Unix())
|
||||
return err
|
||||
}
|
||||
|
||||
// Deletes the oldest routes (by last_verified) when over capacity.
|
||||
func (d *DB) evictIfNeeded() error {
|
||||
count, err := d.RouteCount()
|
||||
|
|
|
|||
49
internal/cache/db_test.go
vendored
49
internal/cache/db_test.go
vendored
|
|
@ -180,6 +180,55 @@ func TestRouteCountAfterExpiry(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNegativeCacheSetAndCheck(t *testing.T) {
|
||||
db, err := cache.Open(":memory:", 100)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
neg, err := db.IsNegative("missing-path")
|
||||
if err != nil {
|
||||
t.Fatalf("IsNegative: %v", err)
|
||||
}
|
||||
if neg {
|
||||
t.Error("expected false for unknown path")
|
||||
}
|
||||
|
||||
if err := db.SetNegative("missing-path", 10*time.Minute); err != nil {
|
||||
t.Fatalf("SetNegative: %v", err)
|
||||
}
|
||||
|
||||
neg, err = db.IsNegative("missing-path")
|
||||
if err != nil {
|
||||
t.Fatalf("IsNegative after set: %v", err)
|
||||
}
|
||||
if !neg {
|
||||
t.Error("expected true after SetNegative")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNegativeCacheExpiry(t *testing.T) {
|
||||
db, err := cache.Open(":memory:", 100)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Set with negative duration so it's already expired.
|
||||
if err := db.SetNegative("expires-now", -time.Second); err != nil {
|
||||
t.Fatalf("SetNegative: %v", err)
|
||||
}
|
||||
if err := db.ExpireNegatives(); err != nil {
|
||||
t.Fatalf("ExpireNegatives: %v", err)
|
||||
}
|
||||
|
||||
neg, _ := db.IsNegative("expires-now")
|
||||
if neg {
|
||||
t.Error("expired negative should not be returned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUEviction(t *testing.T) {
|
||||
// Use maxEntries=3 to trigger eviction easily
|
||||
f, _ := os.CreateTemp("", "ncro-lru-*.db")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue