package imageproxy import ( "context" "database/sql" "errors" "testing" "time" "sneak.berlin/go/pixa/internal/imagerequest" ) // pendingAccessTimesWait is how long a test waits for the database's // connection, for a cache hit to be answered, for an access time to reach // the database, or for a request to return past its deadline. const pendingAccessTimesWait = 5 * time.Second // holdDatabase takes the one connection of db, so that every other query // waits for it, and returns the func that frees it. func holdDatabase(t *testing.T, db *sql.DB) func() { t.Helper() ctx, cancel := context.WithTimeout(t.Context(), pendingAccessTimesWait) defer cancel() conn, err := db.Conn(ctx) if err != nil { t.Fatalf("failed to take the database connection: %v", err) } return func() { _ = conn.Close() } } // TestProxy_Get_ReturnsByItsDeadlineWhileTheDatabaseIsBusy holds the // database's one connection while a request whose fetch is held reaches its // deadline. The request must return with the deadline's error while the // connection is still held, as it waits for no database write past its // deadline. func TestProxy_Get_ReturnsByItsDeadlineWhileTheDatabaseIsBusy(t *testing.T) { t.Parallel() svc, fixtures, fetcher := setupHeldFetchProxy(t) svc.cache.StartPendingAccessTimeWrites() defer func() { _ = svc.cache.StopPendingAccessTimeWrites(t.Context()) }() // Room for the request to reach its fetch on a busy host const timeout = 2 * time.Second ctx, cancel := context.WithTimeout(t.Context(), timeout) defer cancel() deadline, _ := ctx.Deadline() results := startGet(ctx, svc, photoVariant(fixtures, 85, imagerequest.FitCover)) // The request has made its database reads by the time it fetches. select { case <-fetcher.started: case <-time.After(timeout): t.Fatal("request did not reach its fetch by its deadline") } releaseDatabase := holdDatabase(t, svc.db) defer releaseDatabase() select { case got := <-results: 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(time.Until(deadline) + pendingAccessTimesWait): t.Fatal("request did not return while the database was busy") } }