From dbbe148fdb3a81b1a25e404811c4362fa9da1ef2 Mon Sep 17 00:00:00 2001 From: ValentaTomas Date: Sat, 30 May 2026 14:09:51 -0700 Subject: [PATCH 1/6] perf(build): parallelize fragmented backing reads --- .../orchestrator/pkg/sandbox/build/build.go | 145 ++++++++++++++++++ packages/shared/pkg/featureflags/flags.go | 4 + 2 files changed, 149 insertions(+) diff --git a/packages/orchestrator/pkg/sandbox/build/build.go b/packages/orchestrator/pkg/sandbox/build/build.go index 605a103b21..0baeca710e 100644 --- a/packages/orchestrator/pkg/sandbox/build/build.go +++ b/packages/orchestrator/pkg/sandbox/build/build.go @@ -12,9 +12,11 @@ import ( "github.com/google/uuid" "go.uber.org/zap" + "golang.org/x/sync/errgroup" "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/block" blockmetrics "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/block/metrics" + "github.com/e2b-dev/infra/packages/shared/pkg/featureflags" "github.com/e2b-dev/infra/packages/shared/pkg/logger" "github.com/e2b-dev/infra/packages/shared/pkg/storage" "github.com/e2b-dev/infra/packages/shared/pkg/storage/header" @@ -55,6 +57,28 @@ func (b *File) SwapHeader(h *header.Header) { } func (b *File) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) { + maxParallel := 1 + if b.store != nil && b.store.flags != nil { + maxParallel = b.store.flags.IntFlag(ctx, featureflags.MaxParallelBuildReadSegments) + } + if maxParallel > 1 && len(p) > 0 { + if err := b.readAtParallel(ctx, p, off, maxParallel); err == nil { + return len(p), nil + } else if shouldRetrySerial(err) { + if retry, swapErr := b.retryOnTransition(ctx, err); !retry && swapErr != nil { + return 0, swapErr + } + + return b.readAtSerial(ctx, p, off) + } + + return 0, err + } + + return b.readAtSerial(ctx, p, off) +} + +func (b *File) readAtSerial(ctx context.Context, p []byte, off int64) (n int, err error) { // Cache some resolved Diffs per BuildId for the duration of one ReadAt to avoid // hitting the DiffStore TTL cache (and its mutex) on every iteration. const buildCacheSize = 16 @@ -165,6 +189,127 @@ func (b *File) ReadAt(ctx context.Context, p []byte, off int64) (n int, err erro return n, nil } +type readSegment struct { + dstOff int + srcOff int64 + length int64 + diff Diff + ft *storage.FrameTable +} + +func (b *File) readAtParallel(ctx context.Context, p []byte, off int64, maxParallel int) error { + segments, err := b.planRead(ctx, p, off) + if err != nil { + return err + } + if len(segments) <= 1 { + for _, s := range segments { + n, err := s.diff.ReadAt(ctx, p[s.dstOff:s.dstOff+int(s.length)], s.srcOff, s.ft) + if err != nil { + return err + } + if int64(n) != s.length { + return io.ErrUnexpectedEOF + } + } + + return nil + } + + g, gctx := errgroup.WithContext(ctx) + g.SetLimit(maxParallel) + for _, s := range segments { + seg := s + g.Go(func() error { + n, err := seg.diff.ReadAt(gctx, p[seg.dstOff:seg.dstOff+int(seg.length)], seg.srcOff, seg.ft) + if err != nil { + return err + } + if int64(n) != seg.length { + return io.ErrUnexpectedEOF + } + + return nil + }) + } + + return g.Wait() +} + +func (b *File) planRead(ctx context.Context, p []byte, off int64) ([]readSegment, error) { + const buildCacheSize = 16 + var ( + underlyingIDs [buildCacheSize]uuid.UUID + underlyingDiffs [buildCacheSize]Diff + cacheIDs = underlyingIDs[:0] + cacheDiffs = underlyingDiffs[:0] + segments []readSegment + ) + + var n int + for n < len(p) { + h := b.Header() + mappedToBuild, err := h.GetShiftedMapping(ctx, off+int64(n)) + if err != nil { + return nil, fmt.Errorf("failed to get mapping: %w", err) + } + readLength := min(int64(mappedToBuild.Length), int64(len(p)-n)) + if readLength <= 0 { + return nil, io.EOF + } + if mappedToBuild.BuildId == uuid.Nil { + clear(p[n : n+int(readLength)]) + n += int(readLength) + + continue + } + + diff, err := b.cachedBuild(ctx, h, mappedToBuild.BuildId, &cacheIDs, &cacheDiffs) + if err != nil { + return nil, err + } + segments = append(segments, readSegment{ + dstOff: n, + srcOff: int64(mappedToBuild.Offset), + length: readLength, + diff: diff, + ft: h.GetBuildFrameData(mappedToBuild.BuildId), + }) + n += int(readLength) + } + + return segments, nil +} + +func (b *File) cachedBuild(ctx context.Context, h *header.Header, buildID uuid.UUID, ids *[]uuid.UUID, diffs *[]Diff) (Diff, error) { + for i, id := range *ids { + if id == buildID { + return (*diffs)[i], nil + } + } + + diff, err := b.getBuild(ctx, buildID, b.buildFileSize(h, buildID), h.GetBuildFrameData(buildID).CompressionType()) + if err != nil { + return nil, fmt.Errorf("failed to get build: %w", err) + } + if len(*ids) < cap(*ids) { + *ids = append(*ids, buildID) + *diffs = append(*diffs, diff) + } + + return diff, nil +} + +func shouldRetrySerial(err error) bool { + var closed *block.CacheClosedError + if errors.As(err, &closed) { + return true + } + var transErr *storage.PeerTransitionedError + + return errors.As(err, &transErr) +} + // Slice returns [off, off+length). Zero-copy when the range fits in a // single mapping; otherwise composes via ReadAt. func (b *File) Slice(ctx context.Context, off, length int64) ([]byte, error) { diff --git a/packages/shared/pkg/featureflags/flags.go b/packages/shared/pkg/featureflags/flags.go index 28b6f8e370..1d5b27bacc 100644 --- a/packages/shared/pkg/featureflags/flags.go +++ b/packages/shared/pkg/featureflags/flags.go @@ -268,6 +268,10 @@ var ( MaxConcurrentSnapshotBuildQueries = NewIntFlag("max-concurrent-snapshot-build-queries", 0) MinChunkerReadSizeKB = NewIntFlag("min-chunker-read-size-kb", 16) + + // MaxParallelBuildReadSegments limits concurrent backing reads within one fragmented build read. + // 1 or lower keeps the existing serial path. + MaxParallelBuildReadSegments = NewIntFlag("max-parallel-build-read-segments", 1) ) // ReclaimConfigFlag holds per-step caps in milliseconds for the pre-pause From c47cc302b76ffd0b1d9bb5f03f0e008bfb87307d Mon Sep 17 00:00:00 2001 From: ValentaTomas Date: Sat, 30 May 2026 14:30:17 -0700 Subject: [PATCH 2/6] fix(build): fall back from parallel reads on EOF planning --- packages/orchestrator/pkg/sandbox/build/build.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/orchestrator/pkg/sandbox/build/build.go b/packages/orchestrator/pkg/sandbox/build/build.go index 0baeca710e..98fbf4768d 100644 --- a/packages/orchestrator/pkg/sandbox/build/build.go +++ b/packages/orchestrator/pkg/sandbox/build/build.go @@ -288,7 +288,11 @@ func (b *File) cachedBuild(ctx context.Context, h *header.Header, buildID uuid.U } } - diff, err := b.getBuild(ctx, buildID, b.buildFileSize(h, buildID), h.GetBuildFrameData(buildID).CompressionType()) + var ct storage.CompressionType + if ft := h.GetBuildFrameData(buildID); ft != nil { + ct = ft.CompressionType() + } + diff, err := b.getBuild(ctx, buildID, b.buildFileSize(h, buildID), ct) if err != nil { return nil, fmt.Errorf("failed to get build: %w", err) } @@ -301,6 +305,9 @@ func (b *File) cachedBuild(ctx context.Context, h *header.Header, buildID uuid.U } func shouldRetrySerial(err error) bool { + if errors.Is(err, io.EOF) { + return true + } var closed *block.CacheClosedError if errors.As(err, &closed) { return true From ad7c1b0fc2805738541d657302fbfba2da56dfba Mon Sep 17 00:00:00 2001 From: Tomas Valenta Date: Sat, 30 May 2026 15:25:35 -0700 Subject: [PATCH 3/6] fix(build): assume diff store flags are available Remove defensive nil handling around build read parallelism since build files are created with a DiffStore. --- packages/orchestrator/pkg/sandbox/build/build.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/orchestrator/pkg/sandbox/build/build.go b/packages/orchestrator/pkg/sandbox/build/build.go index 98fbf4768d..67a5ccc848 100644 --- a/packages/orchestrator/pkg/sandbox/build/build.go +++ b/packages/orchestrator/pkg/sandbox/build/build.go @@ -57,10 +57,7 @@ func (b *File) SwapHeader(h *header.Header) { } func (b *File) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) { - maxParallel := 1 - if b.store != nil && b.store.flags != nil { - maxParallel = b.store.flags.IntFlag(ctx, featureflags.MaxParallelBuildReadSegments) - } + maxParallel := b.store.flags.IntFlag(ctx, featureflags.MaxParallelBuildReadSegments) if maxParallel > 1 && len(p) > 0 { if err := b.readAtParallel(ctx, p, off, maxParallel); err == nil { return len(p), nil From 8e681995f3deb34eaee1a2351a0afce76c230936 Mon Sep 17 00:00:00 2001 From: ValentaTomas Date: Sat, 30 May 2026 20:00:51 -0700 Subject: [PATCH 4/6] refactor(build): unify serial and parallel read paths --- .../orchestrator/pkg/sandbox/build/build.go | 224 +++++------------- 1 file changed, 57 insertions(+), 167 deletions(-) diff --git a/packages/orchestrator/pkg/sandbox/build/build.go b/packages/orchestrator/pkg/sandbox/build/build.go index 67a5ccc848..2a7610b7e8 100644 --- a/packages/orchestrator/pkg/sandbox/build/build.go +++ b/packages/orchestrator/pkg/sandbox/build/build.go @@ -56,134 +56,37 @@ func (b *File) SwapHeader(h *header.Header) { b.header.Store(h) } -func (b *File) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) { +// ReadAt resolves the mappings covering p (via planRead) and reads their +// segments, optionally in parallel. A Diff evicted between planning and reading +// or a peer transition re-resolves and retries; reads are idempotent. +func (b *File) ReadAt(ctx context.Context, p []byte, off int64) (int, error) { maxParallel := b.store.flags.IntFlag(ctx, featureflags.MaxParallelBuildReadSegments) - if maxParallel > 1 && len(p) > 0 { - if err := b.readAtParallel(ctx, p, off, maxParallel); err == nil { - return len(p), nil - } else if shouldRetrySerial(err) { - if retry, swapErr := b.retryOnTransition(ctx, err); !retry && swapErr != nil { - return 0, swapErr - } - - return b.readAtSerial(ctx, p, off) - } - - return 0, err - } - - return b.readAtSerial(ctx, p, off) -} - -func (b *File) readAtSerial(ctx context.Context, p []byte, off int64) (n int, err error) { - // Cache some resolved Diffs per BuildId for the duration of one ReadAt to avoid - // hitting the DiffStore TTL cache (and its mutex) on every iteration. - const buildCacheSize = 16 - var ( - underlyingIDs [buildCacheSize]uuid.UUID - underlyingDiffs [buildCacheSize]Diff - cacheIDs = underlyingIDs[:0] - cacheDiffs = underlyingDiffs[:0] - ) - - for n < len(p) { - h := b.Header() - // Find out what build and ranghe we need to read, from mappings. - mappedToBuild, err := h.GetShiftedMapping(ctx, off+int64(n)) - if err != nil { - return 0, fmt.Errorf("failed to get mapping: %w", err) + for { + segments, n, err := b.planRead(ctx, p, off) + if err == nil { + err = b.readSegments(ctx, p, segments, maxParallel) } + if err == nil { + if n < len(p) { + return n, io.EOF + } - remainingReadLength := int64(len(p)) - int64(n) - readLength := min(int64(mappedToBuild.Length), remainingReadLength) - - if readLength <= 0 { - logger.L().Error(ctx, fmt.Sprintf( - "(%d bytes left to read, off %d) reading %d bytes from %+v/%+v: [%d:] -> [%d:%d] <> %d (mapped length: %d, remaining read length: %d)\n>>> EOF\n", - len(p)-n, - off, - readLength, - mappedToBuild.BuildId, - b.fileType, - mappedToBuild.Offset, - n, - int64(n)+readLength, - n, - mappedToBuild.Length, - remainingReadLength, - )) - - return n, io.EOF + return n, nil } - if mappedToBuild.BuildId == uuid.Nil { - clear(p[n : int64(n)+readLength]) - n += int(readLength) - + var closed *block.CacheClosedError + if errors.As(err, &closed) { continue } - - size := b.buildFileSize(h, mappedToBuild.BuildId) - ft := h.GetBuildFrameData(mappedToBuild.BuildId) - - // Find the build in the caches. - var mappedBuild Diff - hitIdx := -1 - for i, id := range cacheIDs { - if id == mappedToBuild.BuildId { - mappedBuild = cacheDiffs[i] - hitIdx = i - - break - } - } - if mappedBuild == nil { - mappedBuild, err = b.getBuild(ctx, mappedToBuild.BuildId, size, ft.CompressionType()) - if err != nil { - return 0, fmt.Errorf("failed to get build: %w", err) - } - if len(cacheIDs) < cap(cacheIDs) { - cacheIDs = append(cacheIDs, mappedToBuild.BuildId) - cacheDiffs = append(cacheDiffs, mappedBuild) - hitIdx = len(cacheIDs) - 1 - } - } - - // Read from that build, and handle the various retries. - buildN, err := mappedBuild.ReadAt(ctx, - p[n:int64(n)+readLength], - int64(mappedToBuild.Offset), - ft, - ) - if err != nil { - // Cache may have evicted+closed the Diff between resolve and ReadAt; - // drop the stale entry and re-resolve on the next iteration. - var closed *block.CacheClosedError - if errors.As(err, &closed) { - if hitIdx >= 0 { - last := len(cacheIDs) - 1 - cacheIDs[hitIdx] = cacheIDs[last] - cacheDiffs[hitIdx] = cacheDiffs[last] - cacheIDs = cacheIDs[:last] - cacheDiffs = cacheDiffs[:last] - } - - continue - } - if retry, swapErr := b.retryOnTransition(ctx, err); retry { - continue - } else if swapErr != nil { - return 0, swapErr - } - - return 0, fmt.Errorf("failed to read from source: %w", err) + if retry, swapErr := b.retryOnTransition(ctx, err); retry { + continue + } else if swapErr != nil { + return 0, swapErr } - n += buildN + return 0, err } - - return n, nil } type readSegment struct { @@ -194,46 +97,47 @@ type readSegment struct { ft *storage.FrameTable } -func (b *File) readAtParallel(ctx context.Context, p []byte, off int64, maxParallel int) error { - segments, err := b.planRead(ctx, p, off) - if err != nil { - return err - } - if len(segments) <= 1 { +// readSegments reads each segment into p, in parallel when enabled and there is +// more than one segment, otherwise sequentially. +func (b *File) readSegments(ctx context.Context, p []byte, segments []readSegment, maxParallel int) error { + if maxParallel > 1 && len(segments) > 1 { + g, gctx := errgroup.WithContext(ctx) + g.SetLimit(maxParallel) for _, s := range segments { - n, err := s.diff.ReadAt(ctx, p[s.dstOff:s.dstOff+int(s.length)], s.srcOff, s.ft) - if err != nil { - return err - } - if int64(n) != s.length { - return io.ErrUnexpectedEOF - } + seg := s + g.Go(func() error { return b.readSegment(gctx, p, seg) }) } - return nil + return g.Wait() } - g, gctx := errgroup.WithContext(ctx) - g.SetLimit(maxParallel) for _, s := range segments { - seg := s - g.Go(func() error { - n, err := seg.diff.ReadAt(gctx, p[seg.dstOff:seg.dstOff+int(seg.length)], seg.srcOff, seg.ft) - if err != nil { - return err - } - if int64(n) != seg.length { - return io.ErrUnexpectedEOF - } + if err := b.readSegment(ctx, p, s); err != nil { + return err + } + } - return nil - }) + return nil +} + +func (b *File) readSegment(ctx context.Context, p []byte, s readSegment) error { + n, err := s.diff.ReadAt(ctx, p[s.dstOff:s.dstOff+int(s.length)], s.srcOff, s.ft) + if err != nil { + return err + } + if int64(n) != s.length { + return io.ErrUnexpectedEOF } - return g.Wait() + return nil } -func (b *File) planRead(ctx context.Context, p []byte, off int64) ([]readSegment, error) { +// planRead resolves the mappings covering p into read segments, zero-filling +// uuid.Nil regions in place. It returns the number of bytes covered; a value +// below len(p) means the mappings ran out, which the caller treats as EOF. +func (b *File) planRead(ctx context.Context, p []byte, off int64) ([]readSegment, int, error) { + // Cache resolved Diffs per BuildId for the duration of one read to avoid + // hitting the DiffStore TTL cache (and its mutex) on every mapping. const buildCacheSize = 16 var ( underlyingIDs [buildCacheSize]uuid.UUID @@ -248,11 +152,11 @@ func (b *File) planRead(ctx context.Context, p []byte, off int64) ([]readSegment h := b.Header() mappedToBuild, err := h.GetShiftedMapping(ctx, off+int64(n)) if err != nil { - return nil, fmt.Errorf("failed to get mapping: %w", err) + return nil, 0, fmt.Errorf("failed to get mapping: %w", err) } readLength := min(int64(mappedToBuild.Length), int64(len(p)-n)) if readLength <= 0 { - return nil, io.EOF + return segments, n, nil } if mappedToBuild.BuildId == uuid.Nil { clear(p[n : n+int(readLength)]) @@ -263,7 +167,7 @@ func (b *File) planRead(ctx context.Context, p []byte, off int64) ([]readSegment diff, err := b.cachedBuild(ctx, h, mappedToBuild.BuildId, &cacheIDs, &cacheDiffs) if err != nil { - return nil, err + return nil, 0, err } segments = append(segments, readSegment{ dstOff: n, @@ -275,7 +179,7 @@ func (b *File) planRead(ctx context.Context, p []byte, off int64) ([]readSegment n += int(readLength) } - return segments, nil + return segments, n, nil } func (b *File) cachedBuild(ctx context.Context, h *header.Header, buildID uuid.UUID, ids *[]uuid.UUID, diffs *[]Diff) (Diff, error) { @@ -285,10 +189,9 @@ func (b *File) cachedBuild(ctx context.Context, h *header.Header, buildID uuid.U } } - var ct storage.CompressionType - if ft := h.GetBuildFrameData(buildID); ft != nil { - ct = ft.CompressionType() - } + // CompressionType is nil-safe: an uncompressed build (nil frame table) + // resolves to CompressionNone. + ct := h.GetBuildFrameData(buildID).CompressionType() diff, err := b.getBuild(ctx, buildID, b.buildFileSize(h, buildID), ct) if err != nil { return nil, fmt.Errorf("failed to get build: %w", err) @@ -301,19 +204,6 @@ func (b *File) cachedBuild(ctx context.Context, h *header.Header, buildID uuid.U return diff, nil } -func shouldRetrySerial(err error) bool { - if errors.Is(err, io.EOF) { - return true - } - var closed *block.CacheClosedError - if errors.As(err, &closed) { - return true - } - var transErr *storage.PeerTransitionedError - - return errors.As(err, &transErr) -} - // Slice returns [off, off+length). Zero-copy when the range fits in a // single mapping; otherwise composes via ReadAt. func (b *File) Slice(ctx context.Context, off, length int64) ([]byte, error) { From 6bc548fa94ccfc65b01c9c3afc1cb2b0f1d93fb3 Mon Sep 17 00:00:00 2001 From: ValentaTomas Date: Sat, 30 May 2026 20:13:51 -0700 Subject: [PATCH 5/6] refactor(build): trim read-path comments --- .../orchestrator/pkg/sandbox/build/build.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/orchestrator/pkg/sandbox/build/build.go b/packages/orchestrator/pkg/sandbox/build/build.go index 2a7610b7e8..cb76ded06f 100644 --- a/packages/orchestrator/pkg/sandbox/build/build.go +++ b/packages/orchestrator/pkg/sandbox/build/build.go @@ -56,9 +56,8 @@ func (b *File) SwapHeader(h *header.Header) { b.header.Store(h) } -// ReadAt resolves the mappings covering p (via planRead) and reads their -// segments, optionally in parallel. A Diff evicted between planning and reading -// or a peer transition re-resolves and retries; reads are idempotent. +// ReadAt fills p from the mapped build segments, optionally in parallel. +// Cache eviction or a peer transition re-resolves and retries. func (b *File) ReadAt(ctx context.Context, p []byte, off int64) (int, error) { maxParallel := b.store.flags.IntFlag(ctx, featureflags.MaxParallelBuildReadSegments) @@ -97,8 +96,6 @@ type readSegment struct { ft *storage.FrameTable } -// readSegments reads each segment into p, in parallel when enabled and there is -// more than one segment, otherwise sequentially. func (b *File) readSegments(ctx context.Context, p []byte, segments []readSegment, maxParallel int) error { if maxParallel > 1 && len(segments) > 1 { g, gctx := errgroup.WithContext(ctx) @@ -132,12 +129,10 @@ func (b *File) readSegment(ctx context.Context, p []byte, s readSegment) error { return nil } -// planRead resolves the mappings covering p into read segments, zero-filling -// uuid.Nil regions in place. It returns the number of bytes covered; a value -// below len(p) means the mappings ran out, which the caller treats as EOF. +// planRead resolves the segments covering p, zero-filling uuid.Nil regions. +// A returned byte count below len(p) means the mappings ran out (EOF). func (b *File) planRead(ctx context.Context, p []byte, off int64) ([]readSegment, int, error) { - // Cache resolved Diffs per BuildId for the duration of one read to avoid - // hitting the DiffStore TTL cache (and its mutex) on every mapping. + // Per-read Diff cache: avoids the DiffStore TTL cache mutex on every mapping. const buildCacheSize = 16 var ( underlyingIDs [buildCacheSize]uuid.UUID @@ -189,8 +184,7 @@ func (b *File) cachedBuild(ctx context.Context, h *header.Header, buildID uuid.U } } - // CompressionType is nil-safe: an uncompressed build (nil frame table) - // resolves to CompressionNone. + // CompressionType is nil-safe (nil frame table -> CompressionNone). ct := h.GetBuildFrameData(buildID).CompressionType() diff, err := b.getBuild(ctx, buildID, b.buildFileSize(h, buildID), ct) if err != nil { From a607c1a3bdcf5b60483e51a97cc4df648ab3ddb4 Mon Sep 17 00:00:00 2001 From: ValentaTomas Date: Sat, 30 May 2026 22:09:58 -0700 Subject: [PATCH 6/6] docs(build): document read-path corner cases --- packages/orchestrator/pkg/sandbox/build/build.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/orchestrator/pkg/sandbox/build/build.go b/packages/orchestrator/pkg/sandbox/build/build.go index cb76ded06f..c4814e9558 100644 --- a/packages/orchestrator/pkg/sandbox/build/build.go +++ b/packages/orchestrator/pkg/sandbox/build/build.go @@ -67,6 +67,8 @@ func (b *File) ReadAt(ctx context.Context, p []byte, off int64) (int, error) { err = b.readSegments(ctx, p, segments, maxParallel) } if err == nil { + // Fewer bytes than requested means the mappings ran out: report the + // bytes filled so far with io.EOF, matching io.ReaderAt semantics. if n < len(p) { return n, io.EOF } @@ -74,10 +76,14 @@ func (b *File) ReadAt(ctx context.Context, p []byte, off int64) (int, error) { return n, nil } + // A Diff can be evicted and closed between planning and reading. Re-plan + // the whole read; reads are idempotent, so re-filling already-written + // regions is safe and getBuild re-resolves the closed Diff. var closed *block.CacheClosedError if errors.As(err, &closed) { continue } + // A peer transition swaps the header to the finalized one; retry against it. if retry, swapErr := b.retryOnTransition(ctx, err); retry { continue } else if swapErr != nil { @@ -150,9 +156,12 @@ func (b *File) planRead(ctx context.Context, p []byte, off int64) ([]readSegment return nil, 0, fmt.Errorf("failed to get mapping: %w", err) } readLength := min(int64(mappedToBuild.Length), int64(len(p)-n)) + // A zero-length mapping means off+n is past the last mapping (EOF); stop + // and let the caller surface io.EOF for the bytes covered so far. if readLength <= 0 { return segments, n, nil } + // uuid.Nil marks an unmapped/empty region; zero-fill it in place. if mappedToBuild.BuildId == uuid.Nil { clear(p[n : n+int(readLength)]) n += int(readLength)