mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-06 22:41:56 +02:00
* s3: return 403, not 500, when an over-quota bucket refuses a write Filer AssignVolume flattened ErrReadOnly into the free-text AssignVolumeResponse.Error string, so S3 PutObject / PutObjectPart via UploadReaderInChunks could not match it with errors.Is and fell through to 500 InternalError: retryable, and it hides the quota. Add FilerError READ_ONLY and AssignVolumeResponse.error_code, set it alongside the unchanged error text, and rebuild the sentinel with filer_pb.AssignVolumeResponseError. weed_server.ErrReadOnly now aliases filer_pb.ErrReadOnly so errors.Is matches on both sides, and mapChunkedUploadErrorToS3Error maps it to ErrAccessDenied. There is no "read only" substring matching, so a volume server's "volume N is read only" stays retryable. Carrying the verdict as a response code rather than a gRPC status keeps clients from treating it as a transport failure: the S3 gateway does not fail over across filers and the Java client does not retry it. Wrap per-chunk copy errors with %w so CopyObject keeps the sentinel, and map UploadPartCopy chunk errors through mapCopyErrorToS3Error instead of always returning 500. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci: re-run integration tests (PyPI download timeout) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
97 lines
2.8 KiB
Go
97 lines
2.8 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
)
|
|
|
|
func newAssignTestServer(fc *filer.FilerConf, diskType string) *FilerServer {
|
|
return &FilerServer{
|
|
option: &FilerOption{
|
|
DiskType: diskType,
|
|
},
|
|
filer: &filer.Filer{
|
|
DirBucketsPath: "/buckets",
|
|
FilerConf: fc,
|
|
MaxFilenameLength: 255,
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestResolveAssignStorageOptionUsesBucketRuleBeforeFilerDiskDefault(t *testing.T) {
|
|
fc := filer.NewFilerConf()
|
|
if err := fc.SetLocationConf(&filer_pb.FilerConf_PathConf{
|
|
LocationPrefix: "/buckets/zot",
|
|
DiskType: "disk",
|
|
}); err != nil {
|
|
t.Fatalf("set location conf: %v", err)
|
|
}
|
|
|
|
fs := newAssignTestServer(fc, "hdd")
|
|
|
|
so, err := fs.resolveAssignStorageOption(context.Background(), &filer_pb.AssignVolumeRequest{
|
|
Path: "/buckets/zot/.uploads/upload-id/0001_part.part",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("resolve assign storage option: %v", err)
|
|
}
|
|
|
|
if got, want := so.Collection, "zot"; got != want {
|
|
t.Fatalf("collection = %q, want %q", got, want)
|
|
}
|
|
if got, want := so.DiskType, "disk"; got != want {
|
|
t.Fatalf("disk type = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestResolveAssignStorageOptionFallsBackToFilerDiskDefault(t *testing.T) {
|
|
fs := newAssignTestServer(filer.NewFilerConf(), "hdd")
|
|
|
|
so, err := fs.resolveAssignStorageOption(context.Background(), &filer_pb.AssignVolumeRequest{
|
|
Path: "/tmp/unmatched/file.bin",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("resolve assign storage option: %v", err)
|
|
}
|
|
|
|
if got, want := so.DiskType, "hdd"; got != want {
|
|
t.Fatalf("disk type = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// A read-only path is a verdict, not a transport failure: it must come back as a
|
|
// successful RPC (so clients neither retry nor fail over) carrying READ_ONLY.
|
|
func TestAssignVolumeReadOnlyReturnsErrorCode(t *testing.T) {
|
|
fc := filer.NewFilerConf()
|
|
if err := fc.SetLocationConf(&filer_pb.FilerConf_PathConf{
|
|
LocationPrefix: "/buckets/overquota",
|
|
ReadOnly: true,
|
|
}); err != nil {
|
|
t.Fatalf("set location conf: %v", err)
|
|
}
|
|
|
|
fs := newAssignTestServer(fc, "")
|
|
|
|
resp, err := fs.AssignVolume(context.Background(), &filer_pb.AssignVolumeRequest{
|
|
Path: "/buckets/overquota/x",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AssignVolume err = %v, want a response", err)
|
|
}
|
|
if resp.ErrorCode != filer_pb.FilerError_READ_ONLY {
|
|
t.Fatalf("AssignVolume error_code = %v, want %v", resp.ErrorCode, filer_pb.FilerError_READ_ONLY)
|
|
}
|
|
// Clients that predate error_code still get the old message.
|
|
if want := "assign volume: read only: /buckets/overquota"; !strings.HasPrefix(resp.Error, want) {
|
|
t.Fatalf("AssignVolume error = %q, want prefix %q", resp.Error, want)
|
|
}
|
|
if err := filer_pb.AssignVolumeResponseError(resp); !errors.Is(err, ErrReadOnly) {
|
|
t.Fatalf("AssignVolumeResponseError = %v, want ErrReadOnly", err)
|
|
}
|
|
}
|