Skip to content

Commit 44752f3

Browse files
protondrive: retry transient block upload failures
Retry only failed encrypted blocks with fresh upload links and bounded context-aware backoff. Preserve successful blocks and return terminal or exhausted errors without replaying the complete file stream. Refs oss-singularity/proton-drive-linux#42
1 parent 1d2e00d commit 44752f3

2 files changed

Lines changed: 374 additions & 56 deletions

File tree

file_upload.go

Lines changed: 170 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"crypto/sha256"
99
"encoding/base64"
1010
"encoding/hex"
11+
"errors"
12+
"fmt"
1113
"io"
1214
"mime"
1315
"os"
@@ -19,14 +21,154 @@ import (
1921
"github.com/rclone/go-proton-api"
2022
)
2123

22-
func collectUploadErrors(errChan <-chan error, count int) error {
23-
var firstErr error
24-
for range count {
25-
if err := <-errChan; err != nil && firstErr == nil {
26-
firstErr = err
24+
const (
25+
blockUploadMaxAttempts = 5
26+
blockUploadRetryBaseDelay = time.Second
27+
blockUploadRetryMaxDelay = 15 * time.Second
28+
)
29+
30+
type pendingUploadBlock struct {
31+
blockUploadInfo proton.BlockUploadInfo
32+
encData []byte
33+
}
34+
35+
type blockUploadResult struct {
36+
index int
37+
err error
38+
}
39+
40+
type blockUploadRetryLogger interface {
41+
Warnf(format string, v ...interface{})
42+
}
43+
44+
func retryableBlockUploadError(err error) bool {
45+
if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
46+
return false
47+
}
48+
49+
var apiErr *proton.APIError
50+
if errors.As(err, &apiErr) {
51+
return apiErr.Status >= 500 && apiErr.Status <= 599
52+
}
53+
54+
var protonNetErr *proton.NetError
55+
return errors.As(err, &protonNetErr)
56+
}
57+
58+
func blockUploadRetryDelay(failedAttempt int) time.Duration {
59+
delay := blockUploadRetryBaseDelay
60+
for i := 1; i < failedAttempt && delay < blockUploadRetryMaxDelay; i++ {
61+
delay *= 2
62+
}
63+
if delay > blockUploadRetryMaxDelay {
64+
return blockUploadRetryMaxDelay
65+
}
66+
return delay
67+
}
68+
69+
func waitForBlockUploadRetry(ctx context.Context, delay time.Duration) error {
70+
timer := time.NewTimer(delay)
71+
defer timer.Stop()
72+
select {
73+
case <-ctx.Done():
74+
return ctx.Err()
75+
case <-timer.C:
76+
return nil
77+
}
78+
}
79+
80+
func uploadBlockBatchWithRetry(
81+
ctx context.Context,
82+
blocks []pendingUploadBlock,
83+
maxAttempts int,
84+
requestLinks func(context.Context, []proton.BlockUploadInfo) ([]proton.BlockUploadLink, error),
85+
uploadBlock func(context.Context, proton.BlockUploadLink, []byte) error,
86+
wait func(context.Context, time.Duration) error,
87+
logger blockUploadRetryLogger,
88+
) error {
89+
remaining := append([]pendingUploadBlock(nil), blocks...)
90+
var lastErr error
91+
92+
for attempt := 1; attempt <= maxAttempts; attempt++ {
93+
blockList := make([]proton.BlockUploadInfo, len(remaining))
94+
for i := range remaining {
95+
blockList[i] = remaining[i].blockUploadInfo
96+
}
97+
98+
links, err := requestLinks(ctx, blockList)
99+
if err != nil {
100+
lastErr = err
101+
if !retryableBlockUploadError(err) || attempt == maxAttempts {
102+
return err
103+
}
104+
} else {
105+
if len(links) != len(remaining) {
106+
return fmt.Errorf(
107+
"requested %d Proton block upload links, received %d",
108+
len(remaining),
109+
len(links),
110+
)
111+
}
112+
113+
results := make(chan blockUploadResult, len(remaining))
114+
for i := range remaining {
115+
go func(index int) {
116+
results <- blockUploadResult{
117+
index: index,
118+
err: uploadBlock(ctx, links[index], remaining[index].encData),
119+
}
120+
}(i)
121+
}
122+
123+
errorsByIndex := make([]error, len(remaining))
124+
for range remaining {
125+
result := <-results
126+
errorsByIndex[result.index] = result.err
127+
}
128+
129+
failed := make([]pendingUploadBlock, 0, len(remaining))
130+
var terminalErr error
131+
lastErr = nil
132+
for i, uploadErr := range errorsByIndex {
133+
if uploadErr == nil {
134+
continue
135+
}
136+
if !retryableBlockUploadError(uploadErr) && terminalErr == nil {
137+
terminalErr = uploadErr
138+
}
139+
if lastErr == nil {
140+
lastErr = uploadErr
141+
}
142+
failed = append(failed, remaining[i])
143+
}
144+
if terminalErr != nil {
145+
return terminalErr
146+
}
147+
if len(failed) == 0 {
148+
return nil
149+
}
150+
if attempt == maxAttempts {
151+
return lastErr
152+
}
153+
remaining = failed
154+
}
155+
156+
delay := blockUploadRetryDelay(attempt)
157+
if logger != nil {
158+
logger.Warnf(
159+
"Retrying %d transient Proton block upload(s) after %s (attempt %d/%d)",
160+
len(remaining),
161+
delay,
162+
attempt+1,
163+
maxAttempts,
164+
)
165+
}
166+
if err := wait(ctx, delay); err != nil {
167+
return err
27168
}
28169
}
29-
return firstErr
170+
171+
return lastErr
30172
}
31173

32174
func (protonDrive *ProtonDrive) handleRevisionConflict(ctx context.Context, link *proton.Link, createFileResp *proton.CreateFileRes) (string, bool, error) {
@@ -267,56 +409,45 @@ func (protonDrive *ProtonDrive) createFileUploadDraft(ctx context.Context, paren
267409
}
268410

269411
func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, newSessionKey *crypto.SessionKey, newNodeKR *crypto.KeyRing, file io.Reader, linkID, revisionID string) ([]byte, int64, []int64, string, error) {
270-
type PendingUploadBlocks struct {
271-
blockUploadInfo proton.BlockUploadInfo
272-
encData []byte
273-
}
274-
275412
if newSessionKey == nil || newNodeKR == nil {
276413
return nil, 0, nil, "", ErrMissingInputUploadAndCollectBlockData
277414
}
278415

279416
totalFileSize := int64(0)
280417

281-
pendingUploadBlocks := make([]PendingUploadBlocks, 0)
418+
pendingUploadBlocks := make([]pendingUploadBlock, 0)
282419
manifestSignatureData := make([]byte, 0)
283420
uploadPendingBlocks := func() error {
284421
if len(pendingUploadBlocks) == 0 {
285422
return nil
286423
}
287424

288-
blockList := make([]proton.BlockUploadInfo, 0)
289-
for i := range pendingUploadBlocks {
290-
blockList = append(blockList, pendingUploadBlocks[i].blockUploadInfo)
291-
}
292-
blockUploadReq := proton.BlockUploadReq{
293-
AddressID: protonDrive.MainShare.AddressID,
294-
ShareID: protonDrive.MainShare.ShareID,
295-
LinkID: linkID,
296-
RevisionID: revisionID,
297-
298-
BlockList: blockList,
425+
requestLinks := func(ctx context.Context, blockList []proton.BlockUploadInfo) ([]proton.BlockUploadLink, error) {
426+
return protonDrive.c.RequestBlockUpload(ctx, proton.BlockUploadReq{
427+
AddressID: protonDrive.MainShare.AddressID,
428+
ShareID: protonDrive.MainShare.ShareID,
429+
LinkID: linkID,
430+
RevisionID: revisionID,
431+
BlockList: blockList,
432+
})
299433
}
300-
blockUploadResp, err := protonDrive.c.RequestBlockUpload(ctx, blockUploadReq)
301-
if err != nil {
302-
return err
303-
}
304-
305-
errChan := make(chan error, len(blockUploadResp))
306-
uploadBlockWrapper := func(ctx context.Context, errChan chan error, bareURL, token string, block io.Reader) {
434+
uploadBlock := func(ctx context.Context, link proton.BlockUploadLink, block []byte) error {
307435
if err := protonDrive.blockUploadSemaphore.Acquire(ctx, 1); err != nil {
308-
errChan <- err
309-
return
436+
return err
310437
}
311438
defer protonDrive.blockUploadSemaphore.Release(1)
312439

313-
errChan <- protonDrive.c.UploadBlock(ctx, bareURL, token, block)
314-
}
315-
for i := range blockUploadResp {
316-
go uploadBlockWrapper(ctx, errChan, blockUploadResp[i].BareURL, blockUploadResp[i].Token, bytes.NewReader(pendingUploadBlocks[i].encData))
440+
return protonDrive.c.UploadBlock(ctx, link.BareURL, link.Token, bytes.NewReader(block))
317441
}
318-
319-
if err := collectUploadErrors(errChan, len(blockUploadResp)); err != nil {
442+
if err := uploadBlockBatchWithRetry(
443+
ctx,
444+
pendingUploadBlocks,
445+
blockUploadMaxAttempts,
446+
requestLinks,
447+
uploadBlock,
448+
waitForBlockUploadRetry,
449+
protonDrive.Config.GetLogger(),
450+
); err != nil {
320451
return err
321452
}
322453

@@ -410,7 +541,7 @@ func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, n
410541
}
411542
manifestSignatureData = append(manifestSignatureData, hash...)
412543

413-
pendingUploadBlocks = append(pendingUploadBlocks, PendingUploadBlocks{
544+
pendingUploadBlocks = append(pendingUploadBlocks, pendingUploadBlock{
414545
blockUploadInfo: proton.BlockUploadInfo{
415546
Index: i, // iOS drive: BE starts with 1
416547
Size: int64(len(encData)),

0 commit comments

Comments
 (0)