mirror of
https://github.com/samuelncui/acp.git
synced 2026-11-08 04:21:16 +00:00
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.
102 lines
3.1 KiB
Go
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
|
|
})
|
|
}
|