check / check (push) Waiting to run
TestService_Get_ReturnsByItsDeadline failed on a busy host because its request, past its deadline, still waited for its miss count to be written, a database write with no deadline; in pixad that write waits for the one database connection every request shares. The hit, miss, upstream fetch and transform counts are now each written in a goroutine of their own, which the request waits for only until its deadline; a count not written by then is written after it returns. Shutdown waits for those writes within ShutdownTimeout and reports any left unfinished. The test phase also runs go test with -parallel 4: on a busy host, as many tests at once as there are CPUs wait so long to be scheduled that a timed request can fail. Model: opus-5-5
117 lines
3.3 KiB
Go
117 lines
3.3 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// holdDatabase takes the one connection of the test service's database, so
|
|
// that every other query waits for it, and returns the func that frees it.
|
|
func holdDatabase(t *testing.T, svc *Service) func() {
|
|
t.Helper()
|
|
|
|
conn, err := svc.cache.db.Conn(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("failed to take the database connection: %v", err)
|
|
}
|
|
|
|
release := func() { _ = conn.Close() }
|
|
t.Cleanup(release)
|
|
|
|
return release
|
|
}
|
|
|
|
// TestService_Get_ReturnsByItsDeadlineWhileTheDatabaseIsBusy holds the
|
|
// database's one connection while a request whose fetch is held reaches its
|
|
// deadline. The request must still return by its deadline with the
|
|
// deadline's error, and its miss must be counted once the connection is free.
|
|
func TestService_Get_ReturnsByItsDeadlineWhileTheDatabaseIsBusy(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
svc, fixtures, fetcher := setupHeldFetchService(t)
|
|
|
|
const timeout = 200 * time.Millisecond
|
|
|
|
ctx, cancel := context.WithTimeout(t.Context(), timeout)
|
|
defer cancel()
|
|
|
|
results := startGet(ctx, svc, photoVariant(fixtures, 85, FitCover))
|
|
|
|
// The request has made its database reads by the time it fetches.
|
|
<-fetcher.started
|
|
|
|
releaseDatabase := holdDatabase(t, svc)
|
|
|
|
select {
|
|
case got := <-results:
|
|
deadline, _ := ctx.Deadline()
|
|
t.Logf("Get() returned %v after its deadline, error = %v",
|
|
time.Since(deadline), got.err)
|
|
|
|
if !errors.Is(got.err, context.DeadlineExceeded) {
|
|
t.Errorf("Get() error = %v, want %v", got.err, context.DeadlineExceeded)
|
|
}
|
|
case <-time.After(timeout + time.Second):
|
|
t.Fatal("request did not return by its deadline while the database was busy")
|
|
}
|
|
|
|
releaseDatabase()
|
|
|
|
waitCtx, cancelWait := context.WithTimeout(t.Context(), 5*time.Second)
|
|
defer cancelWait()
|
|
|
|
if !svc.WaitForCountWrites(waitCtx) {
|
|
t.Fatal("the miss was not counted once the database was free")
|
|
}
|
|
|
|
want := cacheStatsCounters{missCount: 1}
|
|
if got := readCacheStatsCounters(t, svc.cache); got != want {
|
|
t.Errorf("counters = %+v, want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
// TestService_WaitForCountWrites holds the database's one connection while a
|
|
// request past its deadline counts a miss. WaitForCountWrites must report the
|
|
// count unwritten when its context ends, and written once the connection is
|
|
// free.
|
|
func TestService_WaitForCountWrites(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
svc, _, _ := setupHeldFetchService(t)
|
|
|
|
releaseDatabase := holdDatabase(t, svc)
|
|
|
|
ended, cancel := context.WithDeadline(t.Context(), time.Now())
|
|
defer cancel()
|
|
|
|
svc.writeCount(ended, func(writeCtx context.Context) {
|
|
svc.cache.IncrementStats(writeCtx, false, 0)
|
|
})
|
|
|
|
shortCtx, cancelShort := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
|
defer cancelShort()
|
|
|
|
written := svc.WaitForCountWrites(shortCtx)
|
|
t.Logf("WaitForCountWrites() while the database was busy = %t", written)
|
|
|
|
if written {
|
|
t.Fatal("WaitForCountWrites() = true while the database was busy")
|
|
}
|
|
|
|
releaseDatabase()
|
|
|
|
waitCtx, cancelWait := context.WithTimeout(t.Context(), 5*time.Second)
|
|
defer cancelWait()
|
|
|
|
if !svc.WaitForCountWrites(waitCtx) {
|
|
t.Fatal("WaitForCountWrites() = false once the database was free")
|
|
}
|
|
|
|
want := cacheStatsCounters{missCount: 1}
|
|
if got := readCacheStatsCounters(t, svc.cache); got != want {
|
|
t.Errorf("counters = %+v, want %+v", got, want)
|
|
}
|
|
}
|