package dbreads import ( "context" "errors" "testing" "time" ) type unitsSeam struct { n int err error } func (u *unitsSeam) count(context.Context) (int, error) { return u.n, u.err } func gib(n uint64) uint64 { return n << 30 } func TestAdmissionTwoReadsThenBusy(t *testing.T) { t.Parallel() u := &unitsSeam{} a := newAdmission(u.count, func() (uint64, uint64, error) { return gib(100), gib(200), nil }) ctx := context.Background() for _, id := range []string{"r1", "r2"} { if err := a.admitRead(ctx, id); err != nil { t.Fatalf("%s: %v", id, err) } } if err := a.admitRead(ctx, "r3"); !errors.Is(err, errBusy) { t.Fatalf("a third read: %v", err) } // The probe still has its connection (spec §7.1, codex 9). if !a.opened() || !a.opened() || !a.admitProbe(ctx) || !a.opened() { t.Fatal("two reads and a probe do not fit three connections") } if a.opened() { t.Fatal("a fourth connection was allowed") } } // T12, unit half: free space that fits one read admits one. func TestAdmissionDiskFitsOneOfTwo(t *testing.T) { t.Parallel() a := newAdmission((&unitsSeam{}).count, func() (uint64, uint64, error) { return gib(12), gib(20), nil }) ctx := context.Background() if err := a.admitRead(ctx, "r1"); err != nil { t.Fatal(err) } if err := a.admitRead(ctx, "r2"); !errors.Is(err, errNoRoom) { t.Fatalf("a second read on a box with room for one: %v", err) } if err := a.grow("r1", int64(gib(8))); !errors.Is(err, errNoRoom) { t.Fatalf("growing past the floor and 10%%: %v", err) } if err := a.grow("r1", int64(gib(6))); err != nil { t.Fatalf("growing within room: %v", err) } } // An orphan (a unit this agent is not running) blocks every admission until // it is gone, and no size is guessed for it (spec §5.7, codex 8). func TestAdmissionRefusesBesideAnOrphan(t *testing.T) { t.Parallel() u := &unitsSeam{n: 1} a := newAdmission(u.count, func() (uint64, uint64, error) { return gib(100), gib(200), nil }) ctx := context.Background() if err := a.admitRead(ctx, "r1"); !errors.Is(err, errOrphans) { t.Fatalf("beside an orphan: %v", err) } if a.admitProbe(ctx) { t.Fatal("a probe beside an orphan") } u.n = 0 if err := a.admitRead(ctx, "r1"); err != nil { t.Fatalf("once the orphan is gone: %v", err) } } // A finished read's reservation is held until no db-read unit is left, since // systemd removes the scratch only at unit stop. func TestAdmissionHoldsAReservationUntilItsUnitIsGone(t *testing.T) { t.Parallel() u := &unitsSeam{} // 12 GiB free of 20: r1 grown to 6 leaves room for r2's floor only once // r1's reservation is released. a := newAdmission(u.count, func() (uint64, uint64, error) { return gib(12), gib(20), nil }) ctx := context.Background() if err := a.admitRead(ctx, "r1"); err != nil { t.Fatal(err) } a.opened() if err := a.grow("r1", int64(gib(6))); err != nil { t.Fatal(err) } u.n = 1 a.closed() a.finish("r1") // The unit is still deactivating: one unit, no connection. Refused. if err := a.admitRead(ctx, "r2"); !errors.Is(err, errOrphans) { t.Fatalf("while the unit deactivates: %v", err) } u.n = 0 if err := a.admitRead(ctx, "r2"); err != nil { t.Fatalf("after the unit is gone the reservation is free: %v", err) } } // Review Focus 5: a unit listing that fails admits nothing and skips the // probe rather than guessing. func TestAdmissionFailsClosedWhenUnitsCannotBeListed(t *testing.T) { t.Parallel() u := &unitsSeam{err: errors.New("systemctl: not found")} a := newAdmission(u.count, func() (uint64, uint64, error) { return gib(100), gib(200), nil }) if err := a.admitRead(context.Background(), "r1"); err == nil { t.Fatal("admitted with no unit listing") } if a.admitProbe(context.Background()) { t.Fatal("probed with no unit listing") } } // A read admitted and then lost in the election started no worker: its // floor goes at once, even while another read's unit is running. func TestAdmissionAbandonFreesAtOnce(t *testing.T) { t.Parallel() u := &unitsSeam{} // 17 GiB of 20: room for two floors (17-10 = 7), not three. a := newAdmission(u.count, func() (uint64, uint64, error) { return gib(17), gib(20), nil }) ctx := context.Background() if err := a.admitRead(ctx, "running"); err != nil { t.Fatal(err) } a.opened() u.n = 1 // its worker unit is up, so finished reservations stay held if err := a.admitRead(ctx, "lost"); err != nil { t.Fatal(err) } a.abandon("lost") // it lost the election; no worker started if err := a.admitRead(ctx, "next"); err != nil { t.Fatalf("the lost read's floor is still held: %v", err) } } // Plan review finding 10: a retried read keeps its first attempt's // reservation while that worker's unit is still stopping, and settle waits // for the unit. Mutation: retry leaves held[id] in place instead of moving // it to a retired key; the grow below then fits and this test is red. func TestAdmissionRetryHoldsTheOldReservation(t *testing.T) { t.Parallel() u := &unitsSeam{} a := newAdmission(u.count, func() (uint64, uint64, error) { return gib(12), gib(20), nil }) ctx := context.Background() if err := a.admitRead(ctx, "r1"); err != nil { t.Fatal(err) } if err := a.grow("r1", int64(gib(6))); err != nil { t.Fatal(err) } u.n = 1 // the first worker's connection is closed; its unit is stopping a.retry("r1", 1) if err := a.grow("r1", int64(gib(5))); !errors.Is(err, errNoRoom) { t.Fatalf("the stopping worker's scratch was not held: %v", err) } short, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() if err := a.settle(short, 10*time.Millisecond); !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("settle returned beside a live unit: %v", err) } u.n = 0 if err := a.settle(ctx, 10*time.Millisecond); err != nil { t.Fatal(err) } if err := a.grow("r1", int64(gib(5))); err != nil { t.Fatalf("after the unit stopped: %v", err) } } func TestAdmissionFailsClosedWhenTheDiskCannotBeRead(t *testing.T) { t.Parallel() a := newAdmission((&unitsSeam{}).count, func() (uint64, uint64, error) { return 0, 0, errors.New("statfs") }) if err := a.admitRead(context.Background(), "r1"); !errors.Is(err, errNoRoom) { t.Fatalf("admitted with no disk reading: %v", err) } }