diff --git a/backend/explore/cache_test.go b/backend/explore/cache_test.go index 8feb4d2..a1bc0a4 100644 --- a/backend/explore/cache_test.go +++ b/backend/explore/cache_test.go @@ -46,23 +46,42 @@ func TestCacheMiss(t *testing.T) { } } +// TestCacheTTLExpiry checks both halves of the TTL contract, and uses two +// entries to do it. +// +// **No assertion here may depend on an upper bound of elapsed wall-clock +// time**, which is what the single-entry version of this test did: it set +// a 1s TTL and immediately asserted a *hit*, so on a loaded runner — one +// goroutine descheduled for over a second while the rest of the suite +// runs — the entry was correctly gone and the test failed with "expected +// cache hit immediately after set". It did exactly that in CI while +// passing five times out of five locally. +// +// Sleeping *past* a TTL is always safe, so the expiry half keeps a short +// one; the presence half gets a TTL nothing can outrun. func TestCacheTTLExpiry(t *testing.T) { c := newTestCache(t) data := []byte(`{"ephemeral":true}`) - c.Set("ttl-test-key", data, 1*time.Second, "", "") + c.Set("ttl-live-key", data, time.Hour, "", "") + c.Set("ttl-expiring-key", data, 1*time.Second, "", "") - // Verify it's there immediately. - if _, ok := c.Get("ttl-test-key"); !ok { - t.Fatal("expected cache hit immediately after set") + if _, ok := c.Get("ttl-live-key"); !ok { + t.Fatal("expected a cache hit on an entry with an hour to live") } - // Wait for expiry. + // Wait for the short one to expire. time.Sleep(2 * time.Second) - if _, ok := c.Get("ttl-test-key"); ok { + if _, ok := c.Get("ttl-expiring-key"); ok { t.Error("expected cache miss after TTL expiry, got hit") } + + // And the long-lived entry is still there, which is what says the + // sweep above expired an entry rather than the cache. + if _, ok := c.Get("ttl-live-key"); !ok { + t.Error("the hour-long entry expired too") + } } func TestCacheMBID(t *testing.T) {