Skip to content

Commit 737a6f5

Browse files
author
gluck
committed
readers while it folds: the case the fleet cannot produce
Every observer of a studied form now renders from ONE shared Libretto instance, because a second Form over that channel is an orphaned reader -- refused by construction since 7bf3a6a. That makes concurrent reads of a copy WHILE its own fold writes it the normal case, and nothing exercised it: the fleet applies its 300 patches and only then studies, so twelve arias never once raced the writer. I listed it as a gap when Gluck asked what was not tested; this closes it. Eight readers taking the renderer's exact call -- an absolute window ending at a version they observed -- against a writer patching the source in a loop. It asserts what a renderer depends on and not more: versions do not go backwards, no patch is answered outside the window that asked for it, and the sweep is silent afterwards, which is the one-writer invariant restated from the read side.
1 parent a75104b commit 737a6f5

1 file changed

Lines changed: 87 additions & 0 deletions

File tree

‎internal/store/libretto_test.go‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,3 +553,90 @@ func TestRefcountSurvivesConcurrentMovers(t *testing.T) {
553553
got, before, movers*rounds, before-got)
554554
}
555555
}
556+
557+
// READERS WHILE IT FOLDS. Every observer of a form now renders from ONE
558+
// shared Libretto instance (a second Form over that channel is an orphaned
559+
// reader, refused by construction elsewhere), so the copy is read by N
560+
// goroutines while its own fold writes it.
561+
//
562+
// That case had never been exercised: the fleet studies AFTER its patches, so
563+
// twelve arias never raced the writer. This is the gap, closed.
564+
//
565+
// What it asserts is what a renderer depends on: a range asked for below the
566+
// version the reader observed is answered without tearing, the answers are
567+
// monotone in version, and nothing panics under -race.
568+
func TestLibrettoReadersWhileItFolds(t *testing.T) {
569+
be, src, _ := librettoFixture(t)
570+
aria, _, err := be.ForkWith("", 0, patchOf(t, map[string]string{"aria_id": `"a1"`}))
571+
if err != nil {
572+
t.Fatal(err)
573+
}
574+
if _, _, err := be.StudyForm(aria, src); err != nil {
575+
t.Fatal(err)
576+
}
577+
lib, err := be.Libretto(src)
578+
if err != nil {
579+
t.Fatal(err)
580+
}
581+
582+
stop := make(chan struct{})
583+
var writer sync.WaitGroup
584+
writer.Add(1)
585+
go func() {
586+
defer writer.Done()
587+
for i := 0; ; i++ {
588+
select {
589+
case <-stop:
590+
return
591+
default:
592+
}
593+
if _, err := be.ApplyForm(src, patchOf(t, map[string]string{
594+
"n": fmt.Sprintf("%d", i)})); err != nil {
595+
return
596+
}
597+
}
598+
}()
599+
600+
const readers = 8
601+
var rg sync.WaitGroup
602+
bad := make(chan string, readers)
603+
for r := 0; r < readers; r++ {
604+
rg.Add(1)
605+
go func() {
606+
defer rg.Done()
607+
var last uint64
608+
for i := 0; i < 200; i++ {
609+
at := lib.Version()
610+
if at < last {
611+
bad <- fmt.Sprintf("version went backwards: %d then %d", last, at)
612+
return
613+
}
614+
last = at
615+
// The renderer's exact call: an absolute window ending at a
616+
// version this reader has observed.
617+
for _, p := range lib.PatchesBetween(0, at) {
618+
if p.Version > at {
619+
bad <- fmt.Sprintf("a patch at %d came back for a window ending %d", p.Version, at)
620+
return
621+
}
622+
}
623+
}
624+
}()
625+
}
626+
rg.Wait()
627+
close(stop)
628+
writer.Wait()
629+
close(bad)
630+
for msg := range bad {
631+
t.Fatal(msg)
632+
}
633+
634+
// And the copy is still coherent afterwards: one writer, one instance.
635+
audit, err := be.ReconcileLibrettos()
636+
if err != nil {
637+
t.Fatal(err)
638+
}
639+
if audit.Corrected != 0 {
640+
t.Fatalf("the sweep had to repair after concurrent reads: %+v", audit)
641+
}
642+
}

0 commit comments

Comments
 (0)