mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-02 18:22:40 +01:00
* s3 tier backend: bound connect and response-header waits The tier client used the SDK default transport: a 30s dial timeout and no limit on waiting for response headers. A blackholed endpoint cost about two minutes per request with the SDK's retries, and an endpoint that accepts connections but never answers held the request forever. Dial with a 10s timeout and give up on a response whose headers do not arrive within a minute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * volume: stop the disk scan from waiting on a stalled tier backend A volume server registers with the master only after every volume on its disks has loaded, and loading a tiered volume reads its superblock from the remote object. With the S3 endpoint unreachable, each read waited out the client's timeouts and retries on one of the loader workers, so start-up grew with the number of tiered volumes and the local volumes were not served until it finished. A tiered volume already loads without its superblock when the read fails. Give that read 10s, and once it stalls, let the backend's other volumes skip it for a minute instead of each waiting out the timeout. The volume reads from the backend as before once the endpoint answers again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * seaweed-volume: bound connect and read waits of the S3 tier client Mirror the Go tier client: a 10s connect timeout instead of the SDK's 3.1s default, and a minute to receive a response, so an endpoint that accepts connections but never answers cannot hold a read forever. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * seaweed-volume: stop volume loading from waiting on a stalled tier backend Mirror the Go loader: the superblock read of a tiered volume gets 10s, and once it stalls the backend's other volumes skip it for a minute. The volume loads with its .vif version as before when the read fails, and reads from the backend once the endpoint answers again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * seaweed-volume: keep the .vif replication when a remote superblock read fails Go applies the .vif replication to a tiered volume whether or not its superblock read succeeded, so a volume loaded while its backend is down still reports its replica placement. The Rust loader applied it only on success and reported the default placement instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * volume: skip the .idx head repair while the tier backend is unreachable The repair reads needle headers from the remote .dat, so after the superblock read failed it would wait on the same backend again and hold up the load. It runs on the next load that reaches the backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * seaweed-volume: skip the .idx head repair while the tier backend is unreachable Mirrors the Go load: after the remote superblock read failed, the repair's remote header reads would wait on the same backend again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3 tier backend: give up a ranged read whose body stops arriving The response-header timeout ends once headers arrive, so an endpoint that answers and then sends no body held ReadAt, and a superblock read's worker, forever. Cancel the request once the body makes no progress for s3ReadTimeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * seaweed-volume: test that a tier read whose body stalls gives up The SDK's stalled-stream protection already fails it, matching the Go read's no-progress cancel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * volume(rust): keep the S3 read timeout off upload_part The SDK's read timeout runs from the start of the request, so a 64 MiB part on a slow link would time out while its body is still being sent. Go's ResponseHeaderTimeout only starts after the body is written. upload_part now keeps the connect timeout and drops the read timeout; stalled-stream protection still fails an upload that stops moving. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * volume(rust): bound the upload_part response wait from when the part is sent Without a read timeout, an upload_part whose body was accepted but never answered waited forever. The part body now goes to the connection in 64 KiB chunks, and once the last chunk is taken the response must arrive within S3_READ_TIMEOUT, like Go's ResponseHeaderTimeout. A retry rebuilds the body and restarts that clock. The body keeps its in-memory contents, so signing and the checksum header are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * volume(rust): keep a retried part's response clock from an earlier attempt The send state was a single flag, and a retry body could be rebuilt and fully sent before the waiter saw the flag drop, so the waiter kept the earlier attempt's deadline. Each body now carries its attempt number, only the latest attempt marks the part sent, and the deadline holds only while no later attempt has started. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
6.7 KiB
Go
186 lines
6.7 KiB
Go
package storage
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// stallingBackend serves localDirBackend's objects, but every read waits
|
|
// until release is closed: an endpoint that accepts requests and never answers.
|
|
type stallingBackend struct {
|
|
*localDirBackend
|
|
release chan struct{}
|
|
reads atomic.Int32
|
|
}
|
|
|
|
func (b *stallingBackend) NewStorageFile(key string, tierInfo *volume_server_pb.VolumeInfo) backend.BackendStorageFile {
|
|
return &stallingBackendFile{BackendStorageFile: b.localDirBackend.NewStorageFile(key, tierInfo), backend: b}
|
|
}
|
|
|
|
type stallingBackendFile struct {
|
|
backend.BackendStorageFile
|
|
backend *stallingBackend
|
|
}
|
|
|
|
func (f *stallingBackendFile) ReadAt(p []byte, off int64) (int, error) {
|
|
f.backend.reads.Add(1)
|
|
<-f.backend.release
|
|
return f.BackendStorageFile.ReadAt(p, off)
|
|
}
|
|
|
|
func shortenRemoteSuperBlockWait(t *testing.T, timeout, stall time.Duration) {
|
|
t.Helper()
|
|
oldTimeout, oldStall := remoteSuperBlockTimeout, remoteSuperBlockStallPeriod
|
|
remoteSuperBlockTimeout, remoteSuperBlockStallPeriod = timeout, stall
|
|
remoteSuperBlockStalls.Delete(testBackendName)
|
|
t.Cleanup(func() {
|
|
remoteSuperBlockTimeout, remoteSuperBlockStallPeriod = oldTimeout, oldStall
|
|
remoteSuperBlockStalls.Delete(testBackendName)
|
|
})
|
|
}
|
|
|
|
func TestReadRemoteSuperBlockStallSkipsBackend(t *testing.T) {
|
|
shortenRemoteSuperBlockWait(t, 50*time.Millisecond, time.Hour)
|
|
b := &stallingBackend{localDirBackend: newLocalDirBackend(t), release: make(chan struct{})}
|
|
key, _, err := b.CopyFile(writeSuperBlockFile(t), nil, 0)
|
|
require.NoError(t, err)
|
|
f := b.NewStorageFile(key, &volume_server_pb.VolumeInfo{Files: []*volume_server_pb.RemoteFile{{FileSize: super_block.SuperBlockSize}}})
|
|
|
|
start := time.Now()
|
|
_, err = readRemoteSuperBlock(testBackendName, f)
|
|
require.Error(t, err)
|
|
require.Less(t, time.Since(start), 5*time.Second)
|
|
require.EqualValues(t, 1, b.reads.Load())
|
|
|
|
_, err = readRemoteSuperBlock(testBackendName, f)
|
|
require.Error(t, err)
|
|
require.EqualValues(t, 1, b.reads.Load(), "a stalled backend must not be asked again within the stall period")
|
|
|
|
close(b.release)
|
|
remoteSuperBlockStalls.Store(testBackendName, time.Now().Add(-time.Second))
|
|
sb, err := readRemoteSuperBlock(testBackendName, f)
|
|
require.NoError(t, err)
|
|
require.Equal(t, needle.GetCurrentVersion(), sb.Version)
|
|
_, stalled := remoteSuperBlockStalls.Load(testBackendName)
|
|
require.False(t, stalled, "a successful read clears the stall")
|
|
}
|
|
|
|
func writeSuperBlockFile(t *testing.T) *os.File {
|
|
t.Helper()
|
|
f, err := os.Create(filepath.Join(t.TempDir(), "sb.dat"))
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { f.Close() })
|
|
sb := super_block.SuperBlock{Version: needle.GetCurrentVersion(), ReplicaPlacement: &super_block.ReplicaPlacement{}, Ttl: &needle.TTL{}}
|
|
_, err = f.Write(sb.Bytes())
|
|
require.NoError(t, err)
|
|
return f
|
|
}
|
|
|
|
// A backend that never answers must not hold up the disk scan, local volumes
|
|
// included, and the tiered volumes must serve reads once it answers again.
|
|
func TestRemoteTier_DiskScanDoesNotWaitOnStalledBackend(t *testing.T) {
|
|
shortenRemoteSuperBlockWait(t, 200*time.Millisecond, time.Hour)
|
|
local := newLocalDirBackend(t)
|
|
registerTestBackend(t, local)
|
|
|
|
dir := t.TempDir()
|
|
remoteVids := []needle.VolumeId{61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72}
|
|
for _, vid := range remoteVids {
|
|
tierUpVolume(t, dir, vid, local)
|
|
}
|
|
lv, err := NewVolume(dir, dir, "", 60, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
require.NoError(t, err)
|
|
lv.Close()
|
|
|
|
stalling := &stallingBackend{localDirBackend: local, release: make(chan struct{})}
|
|
backend.BackendStorages[testBackendName] = stalling
|
|
var releaseOnce sync.Once
|
|
release := func() { releaseOnce.Do(func() { close(stalling.release) }) }
|
|
t.Cleanup(release)
|
|
|
|
loc := &DiskLocation{
|
|
Directory: dir,
|
|
DirectoryUuid: "test-uuid",
|
|
IdxDirectory: dir,
|
|
DiskType: types.HddType,
|
|
MaxVolumeCount: 100,
|
|
OriginalMaxVolumeCount: 100,
|
|
MinFreeSpace: util.MinFreeSpace{Type: util.AsPercent, Percent: 1, Raw: "1"},
|
|
}
|
|
loc.volumes = make(map[needle.VolumeId]*Volume)
|
|
loc.ecVolumes = make(map[needle.VolumeId]*erasure_coding.EcVolume)
|
|
|
|
start := time.Now()
|
|
loc.concurrentLoadingVolumes(NeedleMapInMemory, 2, 0, 0)
|
|
require.Less(t, time.Since(start), 5*time.Second, "the disk scan waited on the stalled backend")
|
|
defer func() {
|
|
for _, v := range loc.volumes {
|
|
v.Close()
|
|
}
|
|
}()
|
|
|
|
_, ok := loc.FindVolume(60)
|
|
require.True(t, ok, "local volume not loaded")
|
|
for _, vid := range remoteVids {
|
|
v, ok := loc.FindVolume(vid)
|
|
require.True(t, ok, "tiered volume %d not loaded", vid)
|
|
require.True(t, v.HasRemoteFile())
|
|
}
|
|
require.Less(t, int(stalling.reads.Load()), len(remoteVids), "every tiered volume waited on the stalled backend")
|
|
|
|
release()
|
|
v, _ := loc.FindVolume(remoteVids[0])
|
|
_, err = v.readNeedle(newEmptyNeedle(1), nil, nil)
|
|
require.NoError(t, err, "a tiered volume must serve reads once the backend answers")
|
|
}
|
|
|
|
// The .idx head repair reads the remote .dat, so it must wait for a load that
|
|
// can reach the backend instead of stalling this one.
|
|
func TestRemoteTier_IdxHeadRepairWaitsForReachableBackend(t *testing.T) {
|
|
shortenRemoteSuperBlockWait(t, 200*time.Millisecond, time.Hour)
|
|
local := newLocalDirBackend(t)
|
|
registerTestBackend(t, local)
|
|
|
|
dir := t.TempDir()
|
|
const vid = needle.VolumeId(80)
|
|
tierUpVolume(t, dir, vid, local)
|
|
idxPath := filepath.Join(dir, "80.idx")
|
|
clobberIdxHead(t, idxPath, []uint64{4, 5})
|
|
sizeBefore := fileSize(t, idxPath)
|
|
|
|
stalling := &stallingBackend{localDirBackend: local, release: make(chan struct{})}
|
|
backend.BackendStorages[testBackendName] = stalling
|
|
var releaseOnce sync.Once
|
|
release := func() { releaseOnce.Do(func() { close(stalling.release) }) }
|
|
time.AfterFunc(10*time.Second, release)
|
|
t.Cleanup(release)
|
|
|
|
start := time.Now()
|
|
v := reloadVolume(t, dir, vid)
|
|
require.Less(t, time.Since(start), 5*time.Second, "the load waited on the stalled backend")
|
|
v.Close()
|
|
require.Equal(t, sizeBefore, fileSize(t, idxPath), "the repair ran against an unreachable backend")
|
|
|
|
backend.BackendStorages[testBackendName] = local
|
|
remoteSuperBlockStalls.Delete(testBackendName)
|
|
v = reloadVolume(t, dir, vid)
|
|
defer v.Close()
|
|
require.Greater(t, fileSize(t, idxPath), sizeBefore, "the repair did not run once the backend answered")
|
|
_, err := v.readNeedle(newEmptyNeedle(1), nil, nil)
|
|
require.NoError(t, err)
|
|
}
|