Files
acp/read_ahead_test.go
Samuel CUI 159d8ea4ea Decouple bounded copy pipeline and preserve ordered completion
Overlap source reads, hashing, ordered linear writes, and asynchronous cleanup with bounded buffers. Preserve conflicting output order, API behavior, and no-space diagnostics. Add an LTFS drive-buffer model and reproducible performance checks.
2026-10-09 00:33:14 +08:00

102 lines
3.1 KiB
Go

package acp
import (
"context"
"fmt"
"sync/atomic"
"testing"
"testing/synctest"
)
func TestPipelineSingleReaderPrefetchesAfterSourceEOF(t *testing.T) {
// Both random and linear sources can read sequentially ahead of a blocked linear writer.
for _, linear := range []bool{false, true} {
t.Run(fmt.Sprintf("linear-source=%t", linear), func(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
c := newTestStream(t, WithHashPolicy(HashRead),
SetFromDevice(DeviceThreads(1), LinearDevice(linear)), SetToDevice(LinearDevice(true)))
gate, writing := make(chan struct{}), make(chan struct{})
release := onceRelease(gate)
defer release()
var written atomic.Int64
c.filesystem = pipelineFilesystem{create: func(job *writeJob, _ targetSpec) (transferOutput, error) {
return &pipelineOutput{write: func(p []byte) (int, error) {
if job.order == 0 {
close(writing)
<-gate
}
written.Add(int64(len(p)))
return len(p), nil
}}, nil
}}
const size = 4096
var reads [3]atomic.Int64
var readers [3]*generatedReader
prepared := make(chan *writeJob, len(readers))
for i := range readers {
readers[i] = &generatedReader{remaining: size, read: func(n int) { reads[i].Add(int64(n)) }}
prepared <- preparedFixture(c, uint64(i), size, fmt.Sprintf("target-%d", i), readers[i])
}
close(prepared)
out := c.copy(context.Background(), prepared)
<-writing
synctest.Wait()
// EOF moves the read reservation while Write remains blocked and the byte window has room.
for i, reader := range readers {
if reads[i].Load() != size || !reader.closed.Load() {
t.Errorf("source %d still waits for Write: bytes=%d closed=%t", i, reads[i].Load(), reader.closed.Load())
}
}
// Releasing the writer settles every hash and target without changing output accounting.
release()
count := 0
for job := range out {
count++
result := job.result()
if result.Err != nil || result.Targets[0].Err != nil || len(result.SHA256) != 32 {
t.Errorf("incomplete result: %+v", result)
}
}
if count != len(readers) || written.Load() != int64(len(readers)*size) {
t.Fatalf("incomplete transfer: files=%d bytes=%d", count, written.Load())
}
})
})
}
}
func TestReadAheadPrioritySkipsCompletedSourcesInOrder(t *testing.T) {
// Later empty or failed sources may finish before the foreground source has read its data.
synctest.Test(t, func(t *testing.T) {
c := newTestStream(t, SetFromDevice(DeviceThreads(1)), SetToDevice(LinearDevice(true)))
ahead := newReadAhead(c)
future := make(chan struct{})
go func() {
release, err := ahead.readSlot(3, nil)
if err == nil {
release()
}
close(future)
}()
ahead.finishRead(2)
ahead.finishRead(1)
synctest.Wait()
select {
case <-future:
t.Fatal("completed later sources skipped the unread foreground source")
default:
}
// Only completion of the earliest source releases the next remaining source's Read.
release, err := ahead.readSlot(0, nil)
if err != nil {
t.Fatal(err)
}
release()
ahead.finishRead(0)
<-future
})
}