Add body read timeout recovery to IMAP client
Introduce Client wrapper with socket deadline support Add reconnection logic for body read timeouts Implement test cases for underflow scenarios Update orchestrator to handle reconnections
This commit is contained in:
@@ -333,10 +333,19 @@ func (o *Orchestrator) runAccount(ctx context.Context, task store.Task, runID in
|
||||
if err != nil {
|
||||
return o.accountFailed(ctx, task.ID, runID, a, srcEP, dstEP, "src", err)
|
||||
}
|
||||
defer func() { _ = src.Logout().Wait() }()
|
||||
if err := src.Login(a.SrcLogin, string(srcPass)).Wait(); err != nil {
|
||||
_ = src.Logout().Wait()
|
||||
return o.accountFailed(ctx, task.ID, runID, a, srcEP, dstEP, "src", err)
|
||||
}
|
||||
// srcClient holds the LIVE source connection. A body-read timeout (server
|
||||
// under-delivering a literal) forces a mid-run reconnect via reconnectSrc,
|
||||
// which swaps this pointer. The cancel goroutine and the deferred logout
|
||||
// below both read through it, so they always act on the current connection
|
||||
// rather than a stale one that was already replaced and logged out.
|
||||
var srcClient atomic.Pointer[imapx.Client]
|
||||
srcClient.Store(src)
|
||||
defer func() { _ = srcClient.Load().Logout().Wait() }()
|
||||
|
||||
dst, err := imapx.Connect(actx, dstEP)
|
||||
if err != nil {
|
||||
return o.accountFailed(ctx, task.ID, runID, a, srcEP, dstEP, "dst", err)
|
||||
@@ -346,11 +355,32 @@ func (o *Orchestrator) runAccount(ctx context.Context, task store.Task, runID in
|
||||
return o.accountFailed(ctx, task.ID, runID, a, srcEP, dstEP, "dst", err)
|
||||
}
|
||||
|
||||
// reconnectSrc dials and logs in a fresh source client, swaps it in as the
|
||||
// live connection, and logs the old (desynced) one out. CopyFolder calls it
|
||||
// to recover after a message body times out: the server left the connection
|
||||
// mid-literal, so it can't be reused. Bound to actx, so a cancelled account
|
||||
// fails the dial instead of reconnecting.
|
||||
reconnectSrc := func() (*imapx.Client, error) {
|
||||
nc, err := imapx.Connect(actx, srcEP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := nc.Login(a.SrcLogin, string(srcPass)).Wait(); err != nil {
|
||||
_ = nc.Logout().Wait()
|
||||
return nil, err
|
||||
}
|
||||
if old := srcClient.Swap(nc); old != nil {
|
||||
_ = old.Logout().Wait()
|
||||
}
|
||||
slog.Info("reconnected src after message body timeout", "account", a.ID, "src_login", a.SrcLogin)
|
||||
return nc, nil
|
||||
}
|
||||
|
||||
// On cancel, close the connections so any in-flight network read (a slow
|
||||
// FETCH/Collect that ctx.Err() checks can't interrupt) unblocks immediately.
|
||||
go func() {
|
||||
<-actx.Done()
|
||||
_ = src.Close()
|
||||
_ = srcClient.Load().Close()
|
||||
_ = dst.Close()
|
||||
}()
|
||||
|
||||
@@ -445,6 +475,10 @@ func (o *Orchestrator) runAccount(ctx context.Context, task store.Task, runID in
|
||||
// stall watchdog sees a large-but-live transfer as progress instead of
|
||||
// cancelling it as a wedged connection.
|
||||
OnActivity: touch,
|
||||
// Recovers from a message body timeout (server under-delivering a
|
||||
// literal) by swapping in a fresh source connection so the folder can
|
||||
// resume with the remaining messages.
|
||||
ReconnectSrc: reconnectSrc,
|
||||
OnProgress: func(c, s int) {
|
||||
touch()
|
||||
now := time.Now()
|
||||
@@ -491,7 +525,7 @@ func (o *Orchestrator) runAccount(ctx context.Context, task store.Task, runID in
|
||||
if actx.Err() != nil {
|
||||
break // cancelled — stop scheduling more folders
|
||||
}
|
||||
res, err := imapx.CopyFolder(actx, src, dst, fp.src, fp.dst, deps)
|
||||
res, err := imapx.CopyFolder(actx, srcClient.Load(), dst, fp.src, fp.dst, deps)
|
||||
folderErr := int64(0)
|
||||
if err != nil && actx.Err() == nil {
|
||||
slog.Warn("folder copy error", "account", a.ID, "src_login", a.SrcLogin, "folder", fp.src, "err", err)
|
||||
|
||||
Reference in New Issue
Block a user