Reduce worker concurrency from 4 to 2
Reduce idle read timeout from 120s to 60s Add batching for IMAP metadata fetches
This commit is contained in:
+1
-1
@@ -21,7 +21,7 @@ services:
|
|||||||
AUTH_PASS: ${AUTH_PASS}
|
AUTH_PASS: ${AUTH_PASS}
|
||||||
ENC_KEY: ${ENC_KEY}
|
ENC_KEY: ${ENC_KEY}
|
||||||
SESSION_SECRET: ${SESSION_SECRET}
|
SESSION_SECRET: ${SESSION_SECRET}
|
||||||
WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-4}
|
WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-2}
|
||||||
PPROF_ADDR: ${PPROF_ADDR:-:6060}
|
PPROF_ADDR: ${PPROF_ADDR:-:6060}
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func Load() (Config, error) {
|
|||||||
AuthUser: os.Getenv("AUTH_USER"),
|
AuthUser: os.Getenv("AUTH_USER"),
|
||||||
AuthPass: os.Getenv("AUTH_PASS"),
|
AuthPass: os.Getenv("AUTH_PASS"),
|
||||||
SessionSecret: []byte(os.Getenv("SESSION_SECRET")),
|
SessionSecret: []byte(os.Getenv("SESSION_SECRET")),
|
||||||
WorkerConcurrency: 4,
|
WorkerConcurrency: 2,
|
||||||
PprofAddr: os.Getenv("PPROF_ADDR"),
|
PprofAddr: os.Getenv("PPROF_ADDR"),
|
||||||
}
|
}
|
||||||
if v := os.Getenv("WORKER_CONCURRENCY"); v != "" {
|
if v := os.Getenv("WORKER_CONCURRENCY"); v != "" {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func TestLoadDefaults(t *testing.T) {
|
|||||||
if cfg.HTTPAddr != ":8080" {
|
if cfg.HTTPAddr != ":8080" {
|
||||||
t.Errorf("HTTPAddr = %q, want :8080", cfg.HTTPAddr)
|
t.Errorf("HTTPAddr = %q, want :8080", cfg.HTTPAddr)
|
||||||
}
|
}
|
||||||
if cfg.WorkerConcurrency != 4 {
|
if cfg.WorkerConcurrency != 2 {
|
||||||
t.Errorf("WorkerConcurrency = %d, want 4", cfg.WorkerConcurrency)
|
t.Errorf("WorkerConcurrency = %d, want 2", cfg.WorkerConcurrency)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package imapx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// metaBatches must tile the sequence 1..total into contiguous, non-overlapping
|
||||||
|
// windows of at most batchSize, covering every message exactly once. A single
|
||||||
|
// unbounded FETCH 1:* is what wedges large mailboxes; batching keeps each
|
||||||
|
// command short so the server stays responsive and ctx can be checked between
|
||||||
|
// windows.
|
||||||
|
func TestMetaBatches(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
total, size uint32
|
||||||
|
want []imap.SeqRange
|
||||||
|
}{
|
||||||
|
{0, 1000, nil},
|
||||||
|
{1, 1000, []imap.SeqRange{{Start: 1, Stop: 1}}},
|
||||||
|
{1000, 1000, []imap.SeqRange{{Start: 1, Stop: 1000}}},
|
||||||
|
{1001, 1000, []imap.SeqRange{{Start: 1, Stop: 1000}, {Start: 1001, Stop: 1001}}},
|
||||||
|
{2500, 1000, []imap.SeqRange{{Start: 1, Stop: 1000}, {Start: 1001, Stop: 2000}, {Start: 2001, Stop: 2500}}},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
got := metaBatches(c.total, c.size)
|
||||||
|
if len(got) != len(c.want) {
|
||||||
|
t.Fatalf("total=%d size=%d: got %d windows %v, want %d %v", c.total, c.size, len(got), got, len(c.want), c.want)
|
||||||
|
}
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != c.want[i] {
|
||||||
|
t.Fatalf("total=%d size=%d window %d: got %+v want %+v", c.total, c.size, i, got[i], c.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every window must be within 1..total and the windows must be gap-free so no
|
||||||
|
// message is skipped or fetched twice.
|
||||||
|
func TestMetaBatchesCoverage(t *testing.T) {
|
||||||
|
const total, size = 4321, 1000
|
||||||
|
got := metaBatches(total, size)
|
||||||
|
if got[0].Start != 1 {
|
||||||
|
t.Fatalf("first window must start at 1, got %d", got[0].Start)
|
||||||
|
}
|
||||||
|
if last := got[len(got)-1]; last.Stop != total {
|
||||||
|
t.Fatalf("last window must stop at total=%d, got %d", total, last.Stop)
|
||||||
|
}
|
||||||
|
for i := 1; i < len(got); i++ {
|
||||||
|
if got[i].Start != got[i-1].Stop+1 {
|
||||||
|
t.Fatalf("gap/overlap between window %d (%+v) and %d (%+v)", i-1, got[i-1], i, got[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
// live transfer never trips it — only a genuinely dead/mute socket does. This
|
// live transfer never trips it — only a genuinely dead/mute socket does. This
|
||||||
// is what stops an account from wedging in "running" forever when a server
|
// is what stops an account from wedging in "running" forever when a server
|
||||||
// accepts a command (e.g. a large FETCH) and then goes silent.
|
// accepts a command (e.g. a large FETCH) and then goes silent.
|
||||||
var idleReadTimeout = 120 * time.Second
|
var idleReadTimeout = 60 * time.Second
|
||||||
|
|
||||||
// idleConn wraps a net.Conn and arms a fresh read deadline before every Read.
|
// idleConn wraps a net.Conn and arms a fresh read deadline before every Read.
|
||||||
// It lives beneath any TLS layer (the raw TCP conn), so the deadline governs
|
// It lives beneath any TLS layer (the raw TCP conn), so the deadline governs
|
||||||
|
|||||||
+38
-3
@@ -36,6 +36,34 @@ type CopyResult struct {
|
|||||||
Errors int
|
Errors int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// metaScanBatch bounds how many messages one Pass-1 metadata FETCH covers. A
|
||||||
|
// single unbounded FETCH 1:* over a large mailbox keeps one command open for
|
||||||
|
// the entire scan; under parallel load the server can stop responding and,
|
||||||
|
// since go-imap has no per-command deadline, the worker wedges forever. Short
|
||||||
|
// windows keep each command brief so the server stays responsive and ctx is
|
||||||
|
// checked between windows.
|
||||||
|
const metaScanBatch = 1000
|
||||||
|
|
||||||
|
// metaBatches tiles 1..total into contiguous, non-overlapping windows of at
|
||||||
|
// most batchSize, covering every sequence number exactly once.
|
||||||
|
func metaBatches(total, batchSize uint32) []imap.SeqRange {
|
||||||
|
if total == 0 || batchSize == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var out []imap.SeqRange
|
||||||
|
for start := uint32(1); start <= total; start += batchSize {
|
||||||
|
stop := start + batchSize - 1
|
||||||
|
if stop > total {
|
||||||
|
stop = total
|
||||||
|
}
|
||||||
|
out = append(out, imap.SeqRange{Start: start, Stop: stop})
|
||||||
|
if stop == total {
|
||||||
|
break // guard against uint32 overflow when total is near max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// CopyFolder streams messages from srcFolder on src to dstFolder on dst.
|
// CopyFolder streams messages from srcFolder on src to dstFolder on dst.
|
||||||
//
|
//
|
||||||
// The source folder is opened read-only (EXAMINE) and is never mutated:
|
// The source folder is opened read-only (EXAMINE) and is never mutated:
|
||||||
@@ -71,11 +99,17 @@ func CopyFolder(ctx context.Context, src, dst *imapclient.Client, srcFolder, dst
|
|||||||
internalDate time.Time
|
internalDate time.Time
|
||||||
}
|
}
|
||||||
var todo []queued
|
var todo []queued
|
||||||
metaSet := imap.SeqSet{imap.SeqRange{Start: 1, Stop: sel.NumMessages}}
|
var scanned int64
|
||||||
fc := src.Fetch(metaSet, &imap.FetchOptions{
|
// Scan metadata in bounded windows instead of one FETCH 1:*, so each
|
||||||
|
// command is short (the server stays responsive) and ctx is checked on
|
||||||
|
// every window boundary — not just between messages of one giant command.
|
||||||
|
for _, win := range metaBatches(sel.NumMessages, metaScanBatch) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
fc := src.Fetch(imap.SeqSet{win}, &imap.FetchOptions{
|
||||||
UID: true, Envelope: true, RFC822Size: true, Flags: true, InternalDate: true,
|
UID: true, Envelope: true, RFC822Size: true, Flags: true, InternalDate: true,
|
||||||
})
|
})
|
||||||
var scanned int64
|
|
||||||
for {
|
for {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
_ = fc.Close()
|
_ = fc.Close()
|
||||||
@@ -110,6 +144,7 @@ func CopyFolder(ctx context.Context, src, dst *imapclient.Client, srcFolder, dst
|
|||||||
if err := fc.Close(); err != nil {
|
if err := fc.Close(); err != nil {
|
||||||
return res, fmt.Errorf("fetch meta %q: %w", srcFolder, err)
|
return res, fmt.Errorf("fetch meta %q: %w", srcFolder, err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Pass 2: fetch bodies for the queued (new) messages, one at a time.
|
// Pass 2: fetch bodies for the queued (new) messages, one at a time.
|
||||||
for _, q := range todo {
|
for _, q := range todo {
|
||||||
|
|||||||
Reference in New Issue
Block a user