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) } } // The reserve a cut leaves behind is the same fact as UploadSettle's own composition, reached the // other way round. // // ⛔ Why this exists at all: `cutTailReserve` counts the writes after a cut BY HAND (a literal three), // and a hand count in code is what this intake's budgets were rewritten to stop relying on — the // class that made UploadSettle short three editions running. The count cannot be replaced by a // derivation without giving the walk a list of its remaining steps, which is the hand-written list // again one level down. So it is CROSS-CHECKED instead: UploadSettle composes the walk out of one // cut and four writes, exactly one of which (StartParsing) runs before the cut, so the reserve must // be what is left after removing the cut and that one write. // // The two expressions share the constants and NOT the route, which is what makes this a check rather // than a tautology: add a fifth write to UploadSettle and the derived side grows while the literal // three does not, so this test — and only this test — turns red. func TestTheCutsReserveIsTheWalkMinusTheCutAndTheWriteBeforeIt(t *testing.T) { s := &Service{} walk := UploadSettle - ReceiptBudget want := walk - CutBudget - writeBudget if got := s.cutTailReserve(); got != want { t.Errorf("the cut reserves %s for the writes after it, but the walk (%s) has %s left once the cut (%s) "+ "and the write before it (%s) are taken out.\n"+ "The two disagree, which means UploadSettle and cutTailReserve no longer count the same steps: "+ "a write was added to one of them and not to the other. Whichever grew, the reserve is now wrong — "+ "too small truncates the write that records what the cut found, too large starves the cut itself.", got, walk, want, CutBudget, writeBudget) } // And the shortened form a test uses stays proportional, or every short-walk fixture silently // models a walk with no room for a cut at all (the hang this pack's own repair introduced). short := &Service{writeBudget: 25 * time.Millisecond} if got, want := short.cutTailReserve(), 3*25*time.Millisecond; got != want { t.Errorf("with a shortened write budget the reserve is %s, want %s", got, want) } }