Skip to content

Update error handling to prevent partial / failed downloads from being marked complete#263

Open
CDRayn wants to merge 21 commits intomainfrom
clay/s3-package-download-fix
Open

Update error handling to prevent partial / failed downloads from being marked complete#263
CDRayn wants to merge 21 commits intomainfrom
clay/s3-package-download-fix

Conversation

@CDRayn
Copy link
Contributor

@CDRayn CDRayn commented Mar 13, 2026

About

This PR updates the logic in the rsstorage package to better handle errors when performing chunked reads or writes. Certain errors were not being handled or propagated before and could result in packages being marked as complete when they were actually just partially downloaded or broken. This addresses https://github.com/rstudio/package-manager/issues/16438.

This fix involved checking for errors during chunk reads and writes, and making sure the checks were used to signal to callers that the operation failed and the package isn't ready. Additionally, checks were added to verify the number of bytes read or written equals the file size and returning an error when they don't match. Instances of ignored errors were also updated to check for error conditions in the event that they were also causing unexpected behavior.

@CDRayn CDRayn requested review from a team, jonyoder and jstruzik March 13, 2026 16:04
@CDRayn CDRayn added the bug Something isn't working label Mar 13, 2026
go func() {
err = w.readChunks(ctx, address, chunkDir, info.NumChunks, info.Complete, info.FileSize, pW)
}()
if err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, didn't mean to delete my comment above, but I see it was fixed. Another issue here- the err var happens right away and won't get any async info.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I was going to comment here, too.

CDRayn and others added 2 commits March 13, 2026 15:43
Co-authored-by: Brian Deitte <bdeitte@gmail.com>
Co-authored-by: Brian Deitte <bdeitte@gmail.com>
Copy link
Contributor

@jstruzik jstruzik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM other than Brian's comments

Comment on lines +272 to +282
for i := uint64(0); i <= numChunks; i++ {
wg.Add(1)
go func() {
defer wg.Done()
bytesRead, err = w.retryingChunkRead(ctx, i, address, chunkDir, complete, writer)
if err != nil {
errs <- writer.CloseWithError(err)
return
}
totalBytesWritten += bytesRead
}()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop needs to be sequential. Each call to retryingChunkRead will copy the bytes for the chunk to the writer we pass to it. If we're using async goroutines, the file could be assembled in a random order.

go func() {
err = w.readChunks(ctx, address, chunkDir, info.NumChunks, info.Complete, info.FileSize, pW)
}()
if err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I was going to comment here, too.

Copy link
Collaborator

@jonyoder jonyoder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few additional comments beyond what Jon already flagged on the readChunks parallelization, the ReadChunked goroutine error check, and the writeChunks defer panic.

Edit: Claude posted these last four for me. I thought they were worth calling out.

// Return normal file info
return true, nil, stat.Size(), stat.ModTime(), nil
found = true
chunkInfo = &info
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: with named returns, err is already zero-valued (nil) by default. The explicit err = nil assignments here and on line 128 are redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like setting named returned explicitly for clarity / readability. I'll remove them.

defer func(path string) {
removeErr := os.RemoveAll(path)
if removeErr != nil {
c.Logf("unbale to remove temp directory: %s", removeErr.Error())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: "unbale" → "unable"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants