154 lines
7 KiB
Go
154 lines
7 KiB
Go
package books
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// The tail of an upload cannot outlive UploadSettle, whatever it does on the way there.
|
||
//
|
||
// The PROPERTY and not a sum of the steps, and that is the whole of the form. The sum was derived by
|
||
// hand three times and was short all three, each time for a different reason (register row PD-464); a
|
||
// test that re-listed the steps would be the fourth hand-derivation and would hold for exactly as
|
||
// long as somebody remembered to update it. What is asserted here survives steps nobody has written
|
||
// yet, which is why the fourth case takes fifty of them.
|
||
func TestNoStepOfAnUploadsTailOutlivesTheWalk(t *testing.T) {
|
||
// THREE different numbers on purpose: in a fixture where the walk, a step's own budget and what a
|
||
// step asks for coincide, "capped by the walk" and "given what it asked for" are the same
|
||
// observation, and the whole class of differences between them goes invisible (D39.208 §5).
|
||
const walkBudget, writeShort, asksForever = 400 * time.Millisecond, 90 * time.Millisecond, time.Hour
|
||
s := &Service{writeBudget: writeShort, uploadSettle: walkBudget}
|
||
walk, cancel := s.walk(context.Background())
|
||
defer cancel()
|
||
end, ok := walk.Deadline()
|
||
if !ok {
|
||
t.Fatal("the walk carries no deadline, so nothing taken under it is bounded by anything")
|
||
}
|
||
|
||
// A step that asks for more than the walk has left gets the walk's end and not what it asked for.
|
||
long, cancelLong := s.step(walk, asksForever)
|
||
defer cancelLong()
|
||
if d, _ := long.Deadline(); !d.Equal(end) {
|
||
t.Errorf("a step that asked for %s ends at %s, want the walk's own end %s: the walk is not capping it",
|
||
asksForever, d.Format(time.StampMilli), end.Format(time.StampMilli))
|
||
}
|
||
|
||
// A step that asks for less keeps its own budget: the cap is a ceiling, not a replacement.
|
||
short, cancelShort := s.step(walk, writeShort)
|
||
defer cancelShort()
|
||
if d, _ := short.Deadline(); !d.Before(end) {
|
||
t.Errorf("a step that asked for %s ends at %s, at or past the walk's %s: it was given the ceiling instead of its budget",
|
||
writeShort, d.Format(time.StampMilli), end.Format(time.StampMilli))
|
||
}
|
||
|
||
// FIFTY steps, each asking for an hour, each taken under the one before it. This is the property
|
||
// the form exists for: the number of steps in the tail is not a term of where the tail ends.
|
||
under := walk
|
||
for i := range 50 {
|
||
next, cancelNext := s.step(under, asksForever)
|
||
defer cancelNext()
|
||
if d, _ := next.Deadline(); d.After(end) {
|
||
t.Fatalf("step %d of the tail ends at %s, past the walk's %s: adding a step moves the boundary",
|
||
i, d.Format(time.StampMilli), end.Format(time.StampMilli))
|
||
}
|
||
under = next
|
||
}
|
||
|
||
// A terminal write taken after the step before it is SPENT is still usable — that is the whole
|
||
// reason a step detaches at all: the engine call may legitimately consume everything, and the
|
||
// record of what happened must still be written. It is capped by the walk all the same.
|
||
spent, cancelSpent := s.step(walk, time.Nanosecond)
|
||
defer cancelSpent()
|
||
<-spent.Done()
|
||
after, cancelAfter := s.step(spent, writeShort)
|
||
defer cancelAfter()
|
||
if err := after.Err(); err != nil {
|
||
t.Errorf("a write taken after an exhausted step was born cancelled (%v): what happened would go unrecorded", err)
|
||
}
|
||
if d, _ := after.Deadline(); d.After(end) {
|
||
t.Errorf("a write taken after an exhausted step ends at %s, past the walk's %s",
|
||
d.Format(time.StampMilli), end.Format(time.StampMilli))
|
||
}
|
||
}
|
||
|
||
// Outside a walk — the queue's worker and the backstop sweep — a step is the detached, self-bounded
|
||
// write it has always been: their budgets are their own, and there is no upload waiting on them.
|
||
func TestAStepOutsideAWalkKeepsItsOwnBudgetAndSurvivesItsCaller(t *testing.T) {
|
||
const writeShort = 90 * time.Millisecond
|
||
s := &Service{writeBudget: writeShort}
|
||
parent, cancelParent := context.WithCancel(context.Background())
|
||
free, cancelFree := s.step(parent, writeShort)
|
||
defer cancelFree()
|
||
cancelParent()
|
||
if err := free.Err(); err != nil {
|
||
t.Errorf("a write outside a walk died with its caller (%v): a job whose deadline is spent would lose its record", err)
|
||
}
|
||
d, ok := free.Deadline()
|
||
if !ok {
|
||
t.Fatal("a write outside a walk got no deadline: detached is not the same as unlimited")
|
||
}
|
||
if left := time.Until(d); left > writeShort {
|
||
t.Errorf("a write outside a walk got %s, want no more than its own %s", left, writeShort)
|
||
}
|
||
}
|
||
|
||
// UploadSettle covers the tail END TO END, and part of it is spent after this package is done: the
|
||
// idempotency receipt is written once Accept has returned. So the walk takes the settle budget MINUS
|
||
// the receipt's share — otherwise the tail ends exactly one receipt past the number the boot compared
|
||
// against the windows an upload has to finish inside, which is the shape of every earlier miss.
|
||
func TestTheWalkLeavesTheReceiptItsShareOfTheSettleBudget(t *testing.T) {
|
||
s := &Service{}
|
||
before := time.Now()
|
||
walk, cancel := s.walk(context.Background())
|
||
defer cancel()
|
||
end, ok := walk.Deadline()
|
||
if !ok {
|
||
t.Fatal("the walk carries no deadline")
|
||
}
|
||
// `before` is read a hair earlier than the walk stamps its deadline, so the reading is the budget
|
||
// plus scheduling slack and never less than it.
|
||
got, want := end.Sub(before), UploadSettle-ReceiptBudget
|
||
if got < want || got > want+time.Second {
|
||
t.Errorf("the walk runs for about %s, want UploadSettle (%s) less the receipt's share (%s) = %s",
|
||
got, UploadSettle, ReceiptBudget, want)
|
||
}
|
||
}
|
||
|
||
// The cut an upload waits for runs on the WALK's deadline, not on a budget of its own.
|
||
//
|
||
// Asserted at the real call site and by the deadline the engine is HANDED, rather than by a stopwatch
|
||
// around the request: the question is which budget bounds the cut, and a clock answers it only on a
|
||
// machine that happened to be slow enough. A cut detached from the walk — the shape this replaced —
|
||
// hands the engine CutBudget here, three orders of magnitude past the walk it is inside.
|
||
func TestTheCutOfAnUploadIsBoundedByTheWalkAndNotByItsOwnBudget(t *testing.T) {
|
||
f := newFixture(t)
|
||
templated(t, f)
|
||
const tail, write = 700 * time.Millisecond, 20 * time.Millisecond
|
||
f.svc.uploadSettle = tail
|
||
// Shortened with the walk, because the reserve a cut leaves for the writes after it is three of
|
||
// these: against the real thirty seconds a 700 ms walk holds no cut at all, and the fixture would
|
||
// then be asserting about a cut that never ran (stepLeaving).
|
||
f.svc.writeBudget = write
|
||
var granted time.Duration
|
||
var bounded bool
|
||
f.engine.onManifest = func(ctx context.Context) {
|
||
d, ok := ctx.Deadline()
|
||
bounded = ok
|
||
granted = time.Until(d)
|
||
}
|
||
f.accept(t, "蛊真人.txt", "первая глава\fвторая глава")
|
||
if f.engine.called() != 1 {
|
||
t.Fatalf("the engine was asked %d times, so there is no granted deadline to judge", f.engine.called())
|
||
}
|
||
if !bounded {
|
||
t.Fatal("the cut ran with no deadline at all")
|
||
}
|
||
if granted <= 0 || granted > tail {
|
||
t.Errorf("the cut was granted %s inside a %s walk, want a slice of the walk", granted, tail)
|
||
}
|
||
if granted >= CutBudget {
|
||
t.Errorf("the cut was granted %s, its own CutBudget (%s): it is detached from the walk it runs inside",
|
||
granted, CutBudget)
|
||
}
|
||
}
|