mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-06 06:22:05 +02:00
s3: return 403, not 500, when an over-quota bucket refuses a write (#11552)
* 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>
This commit is contained in:
1 parent
6f9becaa37
commit
164c3db606
12 files changed
+390
-142
No files matched your search
@@ -325,6 +325,7 @@ enum FilerError {
|
||||
EXISTING_IS_FILE = 4; // cannot overwrite file with directory
|
||||
ENTRY_ALREADY_EXISTS = 5; // O_EXCL and entry already exists
|
||||
PRECONDITION_FAILED = 6; // WriteCondition not satisfied
|
||||
READ_ONLY = 7; // path matches a read-only storage rule (e.g. bucket over quota)
|
||||
}
|
||||
|
||||
// ObjectMutation is one entry-level change applied by ObjectTransaction. All
|
||||
@@ -572,6 +573,9 @@ message AssignVolumeResponse {
|
||||
// fsync is the storage rule's fsync decision for the assigned path, so the
|
||||
// client can carry it onto the volume server upload request.
|
||||
bool fsync = 11;
|
||||
// error_code classifies error for callers that must branch on it; error
|
||||
// keeps the human-readable text for clients that predate the field.
|
||||
FilerError error_code = 12;
|
||||
}
|
||||
|
||||
message LookupVolumeRequest {
|
||||
|
||||
+129
-112
@@ -85,6 +85,7 @@ const (
|
||||
FilerError_EXISTING_IS_FILE FilerError = 4 // cannot overwrite file with directory
|
||||
FilerError_ENTRY_ALREADY_EXISTS FilerError = 5 // O_EXCL and entry already exists
|
||||
FilerError_PRECONDITION_FAILED FilerError = 6 // WriteCondition not satisfied
|
||||
FilerError_READ_ONLY FilerError = 7 // path matches a read-only storage rule (e.g. bucket over quota)
|
||||
)
|
||||
|
||||
// Enum value maps for FilerError.
|
||||
@@ -97,6 +98,7 @@ var (
|
||||
4: "EXISTING_IS_FILE",
|
||||
5: "ENTRY_ALREADY_EXISTS",
|
||||
6: "PRECONDITION_FAILED",
|
||||
7: "READ_ONLY",
|
||||
}
|
||||
FilerError_value = map[string]int32{
|
||||
"OK": 0,
|
||||
@@ -106,6 +108,7 @@ var (
|
||||
"EXISTING_IS_FILE": 4,
|
||||
"ENTRY_ALREADY_EXISTS": 5,
|
||||
"PRECONDITION_FAILED": 6,
|
||||
"READ_ONLY": 7,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3261,7 +3264,10 @@ type AssignVolumeResponse struct {
|
||||
Replicas []*Location `protobuf:"bytes,10,rep,name=replicas,proto3" json:"replicas,omitempty"`
|
||||
// fsync is the storage rule's fsync decision for the assigned path, so the
|
||||
// client can carry it onto the volume server upload request.
|
||||
Fsync bool `protobuf:"varint,11,opt,name=fsync,proto3" json:"fsync,omitempty"`
|
||||
Fsync bool `protobuf:"varint,11,opt,name=fsync,proto3" json:"fsync,omitempty"`
|
||||
// error_code classifies error for callers that must branch on it; error
|
||||
// keeps the human-readable text for clients that predate the field.
|
||||
ErrorCode FilerError `protobuf:"varint,12,opt,name=error_code,json=errorCode,proto3,enum=filer_pb.FilerError" json:"error_code,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -3359,6 +3365,13 @@ func (x *AssignVolumeResponse) GetFsync() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *AssignVolumeResponse) GetErrorCode() FilerError {
|
||||
if x != nil {
|
||||
return x.ErrorCode
|
||||
}
|
||||
return FilerError_OK
|
||||
}
|
||||
|
||||
type LookupVolumeRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
VolumeIds []string `protobuf:"bytes,1,rep,name=volume_ids,json=volumeIds,proto3" json:"volume_ids,omitempty"`
|
||||
@@ -7517,7 +7530,7 @@ const file_filer_proto_rawDesc = "" +
|
||||
"\tdata_node\x18\t \x01(\tR\bdataNode\x12\x1b\n" +
|
||||
"\tdisk_type\x18\b \x01(\tR\bdiskType\x12,\n" +
|
||||
"\x12expected_data_size\x18\n" +
|
||||
" \x01(\x04R\x10expectedDataSize\"\xa7\x02\n" +
|
||||
" \x01(\x04R\x10expectedDataSize\"\xdc\x02\n" +
|
||||
"\x14AssignVolumeResponse\x12\x17\n" +
|
||||
"\afile_id\x18\x01 \x01(\tR\x06fileId\x12\x14\n" +
|
||||
"\x05count\x18\x04 \x01(\x05R\x05count\x12\x12\n" +
|
||||
@@ -7530,7 +7543,9 @@ const file_filer_proto_rawDesc = "" +
|
||||
"\blocation\x18\t \x01(\v2\x12.filer_pb.LocationR\blocation\x12.\n" +
|
||||
"\breplicas\x18\n" +
|
||||
" \x03(\v2\x12.filer_pb.LocationR\breplicas\x12\x14\n" +
|
||||
"\x05fsync\x18\v \x01(\bR\x05fsync\"4\n" +
|
||||
"\x05fsync\x18\v \x01(\bR\x05fsync\x123\n" +
|
||||
"\n" +
|
||||
"error_code\x18\f \x01(\x0e2\x14.filer_pb.FilerErrorR\terrorCode\"4\n" +
|
||||
"\x13LookupVolumeRequest\x12\x1d\n" +
|
||||
"\n" +
|
||||
"volume_ids\x18\x01 \x03(\tR\tvolumeIds\"=\n" +
|
||||
@@ -7840,7 +7855,7 @@ const file_filer_proto_rawDesc = "" +
|
||||
"\x05SSE_C\x10\x01\x12\v\n" +
|
||||
"\aSSE_KMS\x10\x02\x12\n" +
|
||||
"\n" +
|
||||
"\x06SSE_S3\x10\x03*\xa5\x01\n" +
|
||||
"\x06SSE_S3\x10\x03*\xb4\x01\n" +
|
||||
"\n" +
|
||||
"FilerError\x12\x06\n" +
|
||||
"\x02OK\x10\x00\x12\x17\n" +
|
||||
@@ -7849,7 +7864,8 @@ const file_filer_proto_rawDesc = "" +
|
||||
"\x15EXISTING_IS_DIRECTORY\x10\x03\x12\x14\n" +
|
||||
"\x10EXISTING_IS_FILE\x10\x04\x12\x18\n" +
|
||||
"\x14ENTRY_ALREADY_EXISTS\x10\x05\x12\x17\n" +
|
||||
"\x13PRECONDITION_FAILED\x10\x06*u\n" +
|
||||
"\x13PRECONDITION_FAILED\x10\x06\x12\r\n" +
|
||||
"\tREAD_ONLY\x10\a*u\n" +
|
||||
"\vPosixLockOp\x12\f\n" +
|
||||
"\bTRY_LOCK\x10\x00\x12\n" +
|
||||
"\n" +
|
||||
@@ -8067,113 +8083,114 @@ var file_filer_proto_depIdxs = []int32{
|
||||
12, // 38: filer_pb.StreamRenameEntryResponse.event_notification:type_name -> filer_pb.EventNotification
|
||||
45, // 39: filer_pb.AssignVolumeResponse.location:type_name -> filer_pb.Location
|
||||
45, // 40: filer_pb.AssignVolumeResponse.replicas:type_name -> filer_pb.Location
|
||||
45, // 41: filer_pb.Locations.locations:type_name -> filer_pb.Location
|
||||
104, // 42: filer_pb.LookupVolumeResponse.locations_map:type_name -> filer_pb.LookupVolumeResponse.LocationsMapEntry
|
||||
47, // 43: filer_pb.CollectionListResponse.collections:type_name -> filer_pb.Collection
|
||||
12, // 44: filer_pb.SubscribeMetadataResponse.event_notification:type_name -> filer_pb.EventNotification
|
||||
59, // 45: filer_pb.SubscribeMetadataResponse.events:type_name -> filer_pb.SubscribeMetadataResponse
|
||||
63, // 46: filer_pb.SubscribeMetadataResponse.log_file_refs:type_name -> filer_pb.LogFileChunkRef
|
||||
62, // 47: filer_pb.ListMetadataSubscribersResponse.subscribers:type_name -> filer_pb.MetadataSubscriber
|
||||
13, // 48: filer_pb.LogFileChunkRef.chunks:type_name -> filer_pb.FileChunk
|
||||
10, // 49: filer_pb.TraverseBfsMetadataResponse.entry:type_name -> filer_pb.Entry
|
||||
105, // 50: filer_pb.LocateBrokerResponse.resources:type_name -> filer_pb.LocateBrokerResponse.Resource
|
||||
106, // 51: filer_pb.FilerConf.locations:type_name -> filer_pb.FilerConf.PathConf
|
||||
10, // 52: filer_pb.CacheRemoteObjectToLocalClusterResponse.entry:type_name -> filer_pb.Entry
|
||||
59, // 53: filer_pb.CacheRemoteObjectToLocalClusterResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
|
||||
84, // 54: filer_pb.TransferLocksRequest.locks:type_name -> filer_pb.Lock
|
||||
17, // 55: filer_pb.StreamMutateEntryRequest.create_request:type_name -> filer_pb.CreateEntryRequest
|
||||
29, // 56: filer_pb.StreamMutateEntryRequest.update_request:type_name -> filer_pb.UpdateEntryRequest
|
||||
35, // 57: filer_pb.StreamMutateEntryRequest.delete_request:type_name -> filer_pb.DeleteEntryRequest
|
||||
39, // 58: filer_pb.StreamMutateEntryRequest.rename_request:type_name -> filer_pb.StreamRenameEntryRequest
|
||||
28, // 59: filer_pb.StreamMutateEntryResponse.create_response:type_name -> filer_pb.CreateEntryResponse
|
||||
30, // 60: filer_pb.StreamMutateEntryResponse.update_response:type_name -> filer_pb.UpdateEntryResponse
|
||||
36, // 61: filer_pb.StreamMutateEntryResponse.delete_response:type_name -> filer_pb.DeleteEntryResponse
|
||||
40, // 62: filer_pb.StreamMutateEntryResponse.rename_response:type_name -> filer_pb.StreamRenameEntryResponse
|
||||
95, // 63: filer_pb.MountListResponse.mounts:type_name -> filer_pb.MountInfo
|
||||
5, // 64: filer_pb.LookupDirectoryEntriesRequest.requests:type_name -> filer_pb.LookupDirectoryEntryRequest
|
||||
10, // 65: filer_pb.LookupDirectoryEntryResult.entry:type_name -> filer_pb.Entry
|
||||
97, // 66: filer_pb.LookupDirectoryEntriesResponse.results:type_name -> filer_pb.LookupDirectoryEntryResult
|
||||
107, // 67: filer_pb.LookupDirectoryEntriesResponse.locations_map:type_name -> filer_pb.LookupDirectoryEntriesResponse.LocationsMapEntry
|
||||
108, // 68: filer_pb.LookupDirectoryEntriesResponse.read_auth:type_name -> filer_pb.LookupDirectoryEntriesResponse.ReadAuthEntry
|
||||
3, // 69: filer_pb.WriteCondition.Clause.kind:type_name -> filer_pb.WriteCondition.Kind
|
||||
10, // 70: filer_pb.WriteCondition.Clause.expected_entry:type_name -> filer_pb.Entry
|
||||
44, // 71: filer_pb.LookupVolumeResponse.LocationsMapEntry.value:type_name -> filer_pb.Locations
|
||||
44, // 72: filer_pb.LookupDirectoryEntriesResponse.LocationsMapEntry.value:type_name -> filer_pb.Locations
|
||||
5, // 73: filer_pb.SeaweedFiler.LookupDirectoryEntry:input_type -> filer_pb.LookupDirectoryEntryRequest
|
||||
96, // 74: filer_pb.SeaweedFiler.LookupDirectoryEntries:input_type -> filer_pb.LookupDirectoryEntriesRequest
|
||||
7, // 75: filer_pb.SeaweedFiler.ListEntries:input_type -> filer_pb.ListEntriesRequest
|
||||
17, // 76: filer_pb.SeaweedFiler.CreateEntry:input_type -> filer_pb.CreateEntryRequest
|
||||
29, // 77: filer_pb.SeaweedFiler.UpdateEntry:input_type -> filer_pb.UpdateEntryRequest
|
||||
31, // 78: filer_pb.SeaweedFiler.TouchAccessTime:input_type -> filer_pb.TouchAccessTimeRequest
|
||||
33, // 79: filer_pb.SeaweedFiler.AppendToEntry:input_type -> filer_pb.AppendToEntryRequest
|
||||
35, // 80: filer_pb.SeaweedFiler.DeleteEntry:input_type -> filer_pb.DeleteEntryRequest
|
||||
21, // 81: filer_pb.SeaweedFiler.ObjectTransaction:input_type -> filer_pb.ObjectTransactionRequest
|
||||
26, // 82: filer_pb.SeaweedFiler.ObjectTransactionBatch:input_type -> filer_pb.ObjectTransactionBatchRequest
|
||||
24, // 83: filer_pb.SeaweedFiler.PosixLock:input_type -> filer_pb.PosixLockRequest
|
||||
37, // 84: filer_pb.SeaweedFiler.AtomicRenameEntry:input_type -> filer_pb.AtomicRenameEntryRequest
|
||||
39, // 85: filer_pb.SeaweedFiler.StreamRenameEntry:input_type -> filer_pb.StreamRenameEntryRequest
|
||||
89, // 86: filer_pb.SeaweedFiler.StreamMutateEntry:input_type -> filer_pb.StreamMutateEntryRequest
|
||||
41, // 87: filer_pb.SeaweedFiler.AssignVolume:input_type -> filer_pb.AssignVolumeRequest
|
||||
43, // 88: filer_pb.SeaweedFiler.LookupVolume:input_type -> filer_pb.LookupVolumeRequest
|
||||
48, // 89: filer_pb.SeaweedFiler.CollectionList:input_type -> filer_pb.CollectionListRequest
|
||||
50, // 90: filer_pb.SeaweedFiler.DeleteCollection:input_type -> filer_pb.DeleteCollectionRequest
|
||||
52, // 91: filer_pb.SeaweedFiler.Statistics:input_type -> filer_pb.StatisticsRequest
|
||||
54, // 92: filer_pb.SeaweedFiler.Ping:input_type -> filer_pb.PingRequest
|
||||
56, // 93: filer_pb.SeaweedFiler.GetFilerConfiguration:input_type -> filer_pb.GetFilerConfigurationRequest
|
||||
64, // 94: filer_pb.SeaweedFiler.TraverseBfsMetadata:input_type -> filer_pb.TraverseBfsMetadataRequest
|
||||
58, // 95: filer_pb.SeaweedFiler.SubscribeMetadata:input_type -> filer_pb.SubscribeMetadataRequest
|
||||
58, // 96: filer_pb.SeaweedFiler.SubscribeLocalMetadata:input_type -> filer_pb.SubscribeMetadataRequest
|
||||
60, // 97: filer_pb.SeaweedFiler.ListMetadataSubscribers:input_type -> filer_pb.ListMetadataSubscribersRequest
|
||||
71, // 98: filer_pb.SeaweedFiler.KvGet:input_type -> filer_pb.KvGetRequest
|
||||
73, // 99: filer_pb.SeaweedFiler.KvPut:input_type -> filer_pb.KvPutRequest
|
||||
76, // 100: filer_pb.SeaweedFiler.CacheRemoteObjectToLocalCluster:input_type -> filer_pb.CacheRemoteObjectToLocalClusterRequest
|
||||
78, // 101: filer_pb.SeaweedFiler.DistributedLock:input_type -> filer_pb.LockRequest
|
||||
80, // 102: filer_pb.SeaweedFiler.DistributedUnlock:input_type -> filer_pb.UnlockRequest
|
||||
82, // 103: filer_pb.SeaweedFiler.FindLockOwner:input_type -> filer_pb.FindLockOwnerRequest
|
||||
85, // 104: filer_pb.SeaweedFiler.TransferLocks:input_type -> filer_pb.TransferLocksRequest
|
||||
87, // 105: filer_pb.SeaweedFiler.ReplicateLock:input_type -> filer_pb.ReplicateLockRequest
|
||||
91, // 106: filer_pb.SeaweedFiler.MountRegister:input_type -> filer_pb.MountRegisterRequest
|
||||
93, // 107: filer_pb.SeaweedFiler.MountList:input_type -> filer_pb.MountListRequest
|
||||
6, // 108: filer_pb.SeaweedFiler.LookupDirectoryEntry:output_type -> filer_pb.LookupDirectoryEntryResponse
|
||||
98, // 109: filer_pb.SeaweedFiler.LookupDirectoryEntries:output_type -> filer_pb.LookupDirectoryEntriesResponse
|
||||
8, // 110: filer_pb.SeaweedFiler.ListEntries:output_type -> filer_pb.ListEntriesResponse
|
||||
28, // 111: filer_pb.SeaweedFiler.CreateEntry:output_type -> filer_pb.CreateEntryResponse
|
||||
30, // 112: filer_pb.SeaweedFiler.UpdateEntry:output_type -> filer_pb.UpdateEntryResponse
|
||||
32, // 113: filer_pb.SeaweedFiler.TouchAccessTime:output_type -> filer_pb.TouchAccessTimeResponse
|
||||
34, // 114: filer_pb.SeaweedFiler.AppendToEntry:output_type -> filer_pb.AppendToEntryResponse
|
||||
36, // 115: filer_pb.SeaweedFiler.DeleteEntry:output_type -> filer_pb.DeleteEntryResponse
|
||||
22, // 116: filer_pb.SeaweedFiler.ObjectTransaction:output_type -> filer_pb.ObjectTransactionResponse
|
||||
27, // 117: filer_pb.SeaweedFiler.ObjectTransactionBatch:output_type -> filer_pb.ObjectTransactionBatchResponse
|
||||
25, // 118: filer_pb.SeaweedFiler.PosixLock:output_type -> filer_pb.PosixLockResponse
|
||||
38, // 119: filer_pb.SeaweedFiler.AtomicRenameEntry:output_type -> filer_pb.AtomicRenameEntryResponse
|
||||
40, // 120: filer_pb.SeaweedFiler.StreamRenameEntry:output_type -> filer_pb.StreamRenameEntryResponse
|
||||
90, // 121: filer_pb.SeaweedFiler.StreamMutateEntry:output_type -> filer_pb.StreamMutateEntryResponse
|
||||
42, // 122: filer_pb.SeaweedFiler.AssignVolume:output_type -> filer_pb.AssignVolumeResponse
|
||||
46, // 123: filer_pb.SeaweedFiler.LookupVolume:output_type -> filer_pb.LookupVolumeResponse
|
||||
49, // 124: filer_pb.SeaweedFiler.CollectionList:output_type -> filer_pb.CollectionListResponse
|
||||
51, // 125: filer_pb.SeaweedFiler.DeleteCollection:output_type -> filer_pb.DeleteCollectionResponse
|
||||
53, // 126: filer_pb.SeaweedFiler.Statistics:output_type -> filer_pb.StatisticsResponse
|
||||
55, // 127: filer_pb.SeaweedFiler.Ping:output_type -> filer_pb.PingResponse
|
||||
57, // 128: filer_pb.SeaweedFiler.GetFilerConfiguration:output_type -> filer_pb.GetFilerConfigurationResponse
|
||||
65, // 129: filer_pb.SeaweedFiler.TraverseBfsMetadata:output_type -> filer_pb.TraverseBfsMetadataResponse
|
||||
59, // 130: filer_pb.SeaweedFiler.SubscribeMetadata:output_type -> filer_pb.SubscribeMetadataResponse
|
||||
59, // 131: filer_pb.SeaweedFiler.SubscribeLocalMetadata:output_type -> filer_pb.SubscribeMetadataResponse
|
||||
61, // 132: filer_pb.SeaweedFiler.ListMetadataSubscribers:output_type -> filer_pb.ListMetadataSubscribersResponse
|
||||
72, // 133: filer_pb.SeaweedFiler.KvGet:output_type -> filer_pb.KvGetResponse
|
||||
74, // 134: filer_pb.SeaweedFiler.KvPut:output_type -> filer_pb.KvPutResponse
|
||||
77, // 135: filer_pb.SeaweedFiler.CacheRemoteObjectToLocalCluster:output_type -> filer_pb.CacheRemoteObjectToLocalClusterResponse
|
||||
79, // 136: filer_pb.SeaweedFiler.DistributedLock:output_type -> filer_pb.LockResponse
|
||||
81, // 137: filer_pb.SeaweedFiler.DistributedUnlock:output_type -> filer_pb.UnlockResponse
|
||||
83, // 138: filer_pb.SeaweedFiler.FindLockOwner:output_type -> filer_pb.FindLockOwnerResponse
|
||||
86, // 139: filer_pb.SeaweedFiler.TransferLocks:output_type -> filer_pb.TransferLocksResponse
|
||||
88, // 140: filer_pb.SeaweedFiler.ReplicateLock:output_type -> filer_pb.ReplicateLockResponse
|
||||
92, // 141: filer_pb.SeaweedFiler.MountRegister:output_type -> filer_pb.MountRegisterResponse
|
||||
94, // 142: filer_pb.SeaweedFiler.MountList:output_type -> filer_pb.MountListResponse
|
||||
108, // [108:143] is the sub-list for method output_type
|
||||
73, // [73:108] is the sub-list for method input_type
|
||||
73, // [73:73] is the sub-list for extension type_name
|
||||
73, // [73:73] is the sub-list for extension extendee
|
||||
0, // [0:73] is the sub-list for field type_name
|
||||
1, // 41: filer_pb.AssignVolumeResponse.error_code:type_name -> filer_pb.FilerError
|
||||
45, // 42: filer_pb.Locations.locations:type_name -> filer_pb.Location
|
||||
104, // 43: filer_pb.LookupVolumeResponse.locations_map:type_name -> filer_pb.LookupVolumeResponse.LocationsMapEntry
|
||||
47, // 44: filer_pb.CollectionListResponse.collections:type_name -> filer_pb.Collection
|
||||
12, // 45: filer_pb.SubscribeMetadataResponse.event_notification:type_name -> filer_pb.EventNotification
|
||||
59, // 46: filer_pb.SubscribeMetadataResponse.events:type_name -> filer_pb.SubscribeMetadataResponse
|
||||
63, // 47: filer_pb.SubscribeMetadataResponse.log_file_refs:type_name -> filer_pb.LogFileChunkRef
|
||||
62, // 48: filer_pb.ListMetadataSubscribersResponse.subscribers:type_name -> filer_pb.MetadataSubscriber
|
||||
13, // 49: filer_pb.LogFileChunkRef.chunks:type_name -> filer_pb.FileChunk
|
||||
10, // 50: filer_pb.TraverseBfsMetadataResponse.entry:type_name -> filer_pb.Entry
|
||||
105, // 51: filer_pb.LocateBrokerResponse.resources:type_name -> filer_pb.LocateBrokerResponse.Resource
|
||||
106, // 52: filer_pb.FilerConf.locations:type_name -> filer_pb.FilerConf.PathConf
|
||||
10, // 53: filer_pb.CacheRemoteObjectToLocalClusterResponse.entry:type_name -> filer_pb.Entry
|
||||
59, // 54: filer_pb.CacheRemoteObjectToLocalClusterResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
|
||||
84, // 55: filer_pb.TransferLocksRequest.locks:type_name -> filer_pb.Lock
|
||||
17, // 56: filer_pb.StreamMutateEntryRequest.create_request:type_name -> filer_pb.CreateEntryRequest
|
||||
29, // 57: filer_pb.StreamMutateEntryRequest.update_request:type_name -> filer_pb.UpdateEntryRequest
|
||||
35, // 58: filer_pb.StreamMutateEntryRequest.delete_request:type_name -> filer_pb.DeleteEntryRequest
|
||||
39, // 59: filer_pb.StreamMutateEntryRequest.rename_request:type_name -> filer_pb.StreamRenameEntryRequest
|
||||
28, // 60: filer_pb.StreamMutateEntryResponse.create_response:type_name -> filer_pb.CreateEntryResponse
|
||||
30, // 61: filer_pb.StreamMutateEntryResponse.update_response:type_name -> filer_pb.UpdateEntryResponse
|
||||
36, // 62: filer_pb.StreamMutateEntryResponse.delete_response:type_name -> filer_pb.DeleteEntryResponse
|
||||
40, // 63: filer_pb.StreamMutateEntryResponse.rename_response:type_name -> filer_pb.StreamRenameEntryResponse
|
||||
95, // 64: filer_pb.MountListResponse.mounts:type_name -> filer_pb.MountInfo
|
||||
5, // 65: filer_pb.LookupDirectoryEntriesRequest.requests:type_name -> filer_pb.LookupDirectoryEntryRequest
|
||||
10, // 66: filer_pb.LookupDirectoryEntryResult.entry:type_name -> filer_pb.Entry
|
||||
97, // 67: filer_pb.LookupDirectoryEntriesResponse.results:type_name -> filer_pb.LookupDirectoryEntryResult
|
||||
107, // 68: filer_pb.LookupDirectoryEntriesResponse.locations_map:type_name -> filer_pb.LookupDirectoryEntriesResponse.LocationsMapEntry
|
||||
108, // 69: filer_pb.LookupDirectoryEntriesResponse.read_auth:type_name -> filer_pb.LookupDirectoryEntriesResponse.ReadAuthEntry
|
||||
3, // 70: filer_pb.WriteCondition.Clause.kind:type_name -> filer_pb.WriteCondition.Kind
|
||||
10, // 71: filer_pb.WriteCondition.Clause.expected_entry:type_name -> filer_pb.Entry
|
||||
44, // 72: filer_pb.LookupVolumeResponse.LocationsMapEntry.value:type_name -> filer_pb.Locations
|
||||
44, // 73: filer_pb.LookupDirectoryEntriesResponse.LocationsMapEntry.value:type_name -> filer_pb.Locations
|
||||
5, // 74: filer_pb.SeaweedFiler.LookupDirectoryEntry:input_type -> filer_pb.LookupDirectoryEntryRequest
|
||||
96, // 75: filer_pb.SeaweedFiler.LookupDirectoryEntries:input_type -> filer_pb.LookupDirectoryEntriesRequest
|
||||
7, // 76: filer_pb.SeaweedFiler.ListEntries:input_type -> filer_pb.ListEntriesRequest
|
||||
17, // 77: filer_pb.SeaweedFiler.CreateEntry:input_type -> filer_pb.CreateEntryRequest
|
||||
29, // 78: filer_pb.SeaweedFiler.UpdateEntry:input_type -> filer_pb.UpdateEntryRequest
|
||||
31, // 79: filer_pb.SeaweedFiler.TouchAccessTime:input_type -> filer_pb.TouchAccessTimeRequest
|
||||
33, // 80: filer_pb.SeaweedFiler.AppendToEntry:input_type -> filer_pb.AppendToEntryRequest
|
||||
35, // 81: filer_pb.SeaweedFiler.DeleteEntry:input_type -> filer_pb.DeleteEntryRequest
|
||||
21, // 82: filer_pb.SeaweedFiler.ObjectTransaction:input_type -> filer_pb.ObjectTransactionRequest
|
||||
26, // 83: filer_pb.SeaweedFiler.ObjectTransactionBatch:input_type -> filer_pb.ObjectTransactionBatchRequest
|
||||
24, // 84: filer_pb.SeaweedFiler.PosixLock:input_type -> filer_pb.PosixLockRequest
|
||||
37, // 85: filer_pb.SeaweedFiler.AtomicRenameEntry:input_type -> filer_pb.AtomicRenameEntryRequest
|
||||
39, // 86: filer_pb.SeaweedFiler.StreamRenameEntry:input_type -> filer_pb.StreamRenameEntryRequest
|
||||
89, // 87: filer_pb.SeaweedFiler.StreamMutateEntry:input_type -> filer_pb.StreamMutateEntryRequest
|
||||
41, // 88: filer_pb.SeaweedFiler.AssignVolume:input_type -> filer_pb.AssignVolumeRequest
|
||||
43, // 89: filer_pb.SeaweedFiler.LookupVolume:input_type -> filer_pb.LookupVolumeRequest
|
||||
48, // 90: filer_pb.SeaweedFiler.CollectionList:input_type -> filer_pb.CollectionListRequest
|
||||
50, // 91: filer_pb.SeaweedFiler.DeleteCollection:input_type -> filer_pb.DeleteCollectionRequest
|
||||
52, // 92: filer_pb.SeaweedFiler.Statistics:input_type -> filer_pb.StatisticsRequest
|
||||
54, // 93: filer_pb.SeaweedFiler.Ping:input_type -> filer_pb.PingRequest
|
||||
56, // 94: filer_pb.SeaweedFiler.GetFilerConfiguration:input_type -> filer_pb.GetFilerConfigurationRequest
|
||||
64, // 95: filer_pb.SeaweedFiler.TraverseBfsMetadata:input_type -> filer_pb.TraverseBfsMetadataRequest
|
||||
58, // 96: filer_pb.SeaweedFiler.SubscribeMetadata:input_type -> filer_pb.SubscribeMetadataRequest
|
||||
58, // 97: filer_pb.SeaweedFiler.SubscribeLocalMetadata:input_type -> filer_pb.SubscribeMetadataRequest
|
||||
60, // 98: filer_pb.SeaweedFiler.ListMetadataSubscribers:input_type -> filer_pb.ListMetadataSubscribersRequest
|
||||
71, // 99: filer_pb.SeaweedFiler.KvGet:input_type -> filer_pb.KvGetRequest
|
||||
73, // 100: filer_pb.SeaweedFiler.KvPut:input_type -> filer_pb.KvPutRequest
|
||||
76, // 101: filer_pb.SeaweedFiler.CacheRemoteObjectToLocalCluster:input_type -> filer_pb.CacheRemoteObjectToLocalClusterRequest
|
||||
78, // 102: filer_pb.SeaweedFiler.DistributedLock:input_type -> filer_pb.LockRequest
|
||||
80, // 103: filer_pb.SeaweedFiler.DistributedUnlock:input_type -> filer_pb.UnlockRequest
|
||||
82, // 104: filer_pb.SeaweedFiler.FindLockOwner:input_type -> filer_pb.FindLockOwnerRequest
|
||||
85, // 105: filer_pb.SeaweedFiler.TransferLocks:input_type -> filer_pb.TransferLocksRequest
|
||||
87, // 106: filer_pb.SeaweedFiler.ReplicateLock:input_type -> filer_pb.ReplicateLockRequest
|
||||
91, // 107: filer_pb.SeaweedFiler.MountRegister:input_type -> filer_pb.MountRegisterRequest
|
||||
93, // 108: filer_pb.SeaweedFiler.MountList:input_type -> filer_pb.MountListRequest
|
||||
6, // 109: filer_pb.SeaweedFiler.LookupDirectoryEntry:output_type -> filer_pb.LookupDirectoryEntryResponse
|
||||
98, // 110: filer_pb.SeaweedFiler.LookupDirectoryEntries:output_type -> filer_pb.LookupDirectoryEntriesResponse
|
||||
8, // 111: filer_pb.SeaweedFiler.ListEntries:output_type -> filer_pb.ListEntriesResponse
|
||||
28, // 112: filer_pb.SeaweedFiler.CreateEntry:output_type -> filer_pb.CreateEntryResponse
|
||||
30, // 113: filer_pb.SeaweedFiler.UpdateEntry:output_type -> filer_pb.UpdateEntryResponse
|
||||
32, // 114: filer_pb.SeaweedFiler.TouchAccessTime:output_type -> filer_pb.TouchAccessTimeResponse
|
||||
34, // 115: filer_pb.SeaweedFiler.AppendToEntry:output_type -> filer_pb.AppendToEntryResponse
|
||||
36, // 116: filer_pb.SeaweedFiler.DeleteEntry:output_type -> filer_pb.DeleteEntryResponse
|
||||
22, // 117: filer_pb.SeaweedFiler.ObjectTransaction:output_type -> filer_pb.ObjectTransactionResponse
|
||||
27, // 118: filer_pb.SeaweedFiler.ObjectTransactionBatch:output_type -> filer_pb.ObjectTransactionBatchResponse
|
||||
25, // 119: filer_pb.SeaweedFiler.PosixLock:output_type -> filer_pb.PosixLockResponse
|
||||
38, // 120: filer_pb.SeaweedFiler.AtomicRenameEntry:output_type -> filer_pb.AtomicRenameEntryResponse
|
||||
40, // 121: filer_pb.SeaweedFiler.StreamRenameEntry:output_type -> filer_pb.StreamRenameEntryResponse
|
||||
90, // 122: filer_pb.SeaweedFiler.StreamMutateEntry:output_type -> filer_pb.StreamMutateEntryResponse
|
||||
42, // 123: filer_pb.SeaweedFiler.AssignVolume:output_type -> filer_pb.AssignVolumeResponse
|
||||
46, // 124: filer_pb.SeaweedFiler.LookupVolume:output_type -> filer_pb.LookupVolumeResponse
|
||||
49, // 125: filer_pb.SeaweedFiler.CollectionList:output_type -> filer_pb.CollectionListResponse
|
||||
51, // 126: filer_pb.SeaweedFiler.DeleteCollection:output_type -> filer_pb.DeleteCollectionResponse
|
||||
53, // 127: filer_pb.SeaweedFiler.Statistics:output_type -> filer_pb.StatisticsResponse
|
||||
55, // 128: filer_pb.SeaweedFiler.Ping:output_type -> filer_pb.PingResponse
|
||||
57, // 129: filer_pb.SeaweedFiler.GetFilerConfiguration:output_type -> filer_pb.GetFilerConfigurationResponse
|
||||
65, // 130: filer_pb.SeaweedFiler.TraverseBfsMetadata:output_type -> filer_pb.TraverseBfsMetadataResponse
|
||||
59, // 131: filer_pb.SeaweedFiler.SubscribeMetadata:output_type -> filer_pb.SubscribeMetadataResponse
|
||||
59, // 132: filer_pb.SeaweedFiler.SubscribeLocalMetadata:output_type -> filer_pb.SubscribeMetadataResponse
|
||||
61, // 133: filer_pb.SeaweedFiler.ListMetadataSubscribers:output_type -> filer_pb.ListMetadataSubscribersResponse
|
||||
72, // 134: filer_pb.SeaweedFiler.KvGet:output_type -> filer_pb.KvGetResponse
|
||||
74, // 135: filer_pb.SeaweedFiler.KvPut:output_type -> filer_pb.KvPutResponse
|
||||
77, // 136: filer_pb.SeaweedFiler.CacheRemoteObjectToLocalCluster:output_type -> filer_pb.CacheRemoteObjectToLocalClusterResponse
|
||||
79, // 137: filer_pb.SeaweedFiler.DistributedLock:output_type -> filer_pb.LockResponse
|
||||
81, // 138: filer_pb.SeaweedFiler.DistributedUnlock:output_type -> filer_pb.UnlockResponse
|
||||
83, // 139: filer_pb.SeaweedFiler.FindLockOwner:output_type -> filer_pb.FindLockOwnerResponse
|
||||
86, // 140: filer_pb.SeaweedFiler.TransferLocks:output_type -> filer_pb.TransferLocksResponse
|
||||
88, // 141: filer_pb.SeaweedFiler.ReplicateLock:output_type -> filer_pb.ReplicateLockResponse
|
||||
92, // 142: filer_pb.SeaweedFiler.MountRegister:output_type -> filer_pb.MountRegisterResponse
|
||||
94, // 143: filer_pb.SeaweedFiler.MountList:output_type -> filer_pb.MountListResponse
|
||||
109, // [109:144] is the sub-list for method output_type
|
||||
74, // [74:109] is the sub-list for method input_type
|
||||
74, // [74:74] is the sub-list for extension type_name
|
||||
74, // [74:74] is the sub-list for extension extendee
|
||||
0, // [0:74] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_filer_proto_init() }
|
||||
|
||||
@@ -234,6 +234,7 @@ var (
|
||||
ErrExistingIsDirectory = errors.New("existing entry is a directory")
|
||||
ErrExistingIsFile = errors.New("existing entry is a file")
|
||||
ErrEntryAlreadyExists = errors.New("entry already exists")
|
||||
ErrReadOnly = errors.New("read only")
|
||||
)
|
||||
|
||||
// FilerErrorToSentinel maps a proto FilerError code to its sentinel error.
|
||||
@@ -250,11 +251,33 @@ func FilerErrorToSentinel(code FilerError) error {
|
||||
return ErrExistingIsFile
|
||||
case FilerError_ENTRY_ALREADY_EXISTS:
|
||||
return ErrEntryAlreadyExists
|
||||
case FilerError_READ_ONLY:
|
||||
return ErrReadOnly
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// AssignVolumeResponseError returns the failure resp reports, or nil. The error
|
||||
// keeps the filer's text and, when error_code is set, unwraps to its sentinel so
|
||||
// callers can match it with errors.Is.
|
||||
func AssignVolumeResponseError(resp *AssignVolumeResponse) error {
|
||||
if resp.Error == "" && resp.ErrorCode == FilerError_OK {
|
||||
return nil
|
||||
}
|
||||
return &codedError{msg: resp.Error, sentinel: FilerErrorToSentinel(resp.ErrorCode)}
|
||||
}
|
||||
|
||||
// codedError is a filer error message paired with the sentinel of the code
|
||||
// sent alongside it (nil for OK or an unknown code).
|
||||
type codedError struct {
|
||||
msg string
|
||||
sentinel error
|
||||
}
|
||||
|
||||
func (e *codedError) Error() string { return e.msg }
|
||||
func (e *codedError) Unwrap() error { return e.sentinel }
|
||||
|
||||
func IsEmpty(event *SubscribeMetadataResponse) bool {
|
||||
return event.EventNotification.NewEntry == nil && event.EventNotification.OldEntry == nil
|
||||
}
|
||||
|
||||
@@ -1,12 +1,37 @@
|
||||
package filer_pb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
)
|
||||
|
||||
func TestAssignVolumeResponseError(t *testing.T) {
|
||||
if err := AssignVolumeResponseError(&AssignVolumeResponse{FileId: "3,01"}); err != nil {
|
||||
t.Fatalf("success response: err = %v, want nil", err)
|
||||
}
|
||||
|
||||
// A filer that predates error_code: the text survives, no sentinel is implied.
|
||||
err := AssignVolumeResponseError(&AssignVolumeResponse{Error: "assign volume: no free volumes"})
|
||||
if err == nil || err.Error() != "assign volume: no free volumes" {
|
||||
t.Fatalf("uncoded response: err = %v, want the filer's text", err)
|
||||
}
|
||||
if errors.Is(err, ErrReadOnly) {
|
||||
t.Fatal("uncoded response must not match ErrReadOnly")
|
||||
}
|
||||
|
||||
msg := "assign volume: read only: /buckets/b (e.g. bucket over quota)"
|
||||
err = AssignVolumeResponseError(&AssignVolumeResponse{Error: msg, ErrorCode: FilerError_READ_ONLY})
|
||||
if !errors.Is(err, ErrReadOnly) {
|
||||
t.Fatalf("READ_ONLY response: err = %v, want ErrReadOnly", err)
|
||||
}
|
||||
if err.Error() != msg {
|
||||
t.Fatalf("READ_ONLY response: message = %q, want %q", err.Error(), msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDirectoryKeyObject(t *testing.T) {
|
||||
chunk := []*FileChunk{{FileId: "1,01", Size: 75}}
|
||||
|
||||
|
||||
@@ -2974,6 +2974,11 @@ func (m *AssignVolumeResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error)
|
||||
i -= len(m.unknownFields)
|
||||
copy(dAtA[i:], m.unknownFields)
|
||||
}
|
||||
if m.ErrorCode != 0 {
|
||||
i = protohelpers.EncodeVarint(dAtA, i, uint64(m.ErrorCode))
|
||||
i--
|
||||
dAtA[i] = 0x60
|
||||
}
|
||||
if m.Fsync {
|
||||
i--
|
||||
if m.Fsync {
|
||||
@@ -7698,6 +7703,9 @@ func (m *AssignVolumeResponse) SizeVT() (n int) {
|
||||
if m.Fsync {
|
||||
n += 2
|
||||
}
|
||||
if m.ErrorCode != 0 {
|
||||
n += 1 + protohelpers.SizeOfVarint(uint64(m.ErrorCode))
|
||||
}
|
||||
n += len(m.unknownFields)
|
||||
return n
|
||||
}
|
||||
@@ -17459,6 +17467,25 @@ func (m *AssignVolumeResponse) UnmarshalVT(dAtA []byte) error {
|
||||
}
|
||||
}
|
||||
m.Fsync = bool(v != 0)
|
||||
case 12:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field ErrorCode", wireType)
|
||||
}
|
||||
m.ErrorCode = 0
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return protohelpers.ErrIntOverflow
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
m.ErrorCode |= FilerError(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := protohelpers.Skip(dAtA[iNdEx:])
|
||||
|
||||
@@ -1084,7 +1084,7 @@ func (s3a *S3ApiServer) CopyObjectPartHandler(w http.ResponseWriter, r *http.Req
|
||||
dstChunks, err := s3a.copyChunksForRange(entry, startOffset, endOffset, dstAssignPath)
|
||||
if err != nil {
|
||||
glog.Errorf("CopyObjectPartHandler copy chunks error: %v", err)
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
||||
s3err.WriteErrorResponse(w, r, s3a.mapCopyErrorToS3Error(err))
|
||||
return
|
||||
}
|
||||
dstEntry.Chunks = dstChunks
|
||||
@@ -1310,7 +1310,7 @@ func (s3a *S3ApiServer) copyChunks(entry *filer_pb.Entry, dstPath string) ([]*fi
|
||||
executor.Execute(func() {
|
||||
dstChunk, err := s3a.copySingleChunk(chunk, dstPath)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("chunk %d: %v", chunkIndex, err)
|
||||
errChan <- fmt.Errorf("chunk %d: %w", chunkIndex, err)
|
||||
return
|
||||
}
|
||||
dstChunks[chunkIndex] = dstChunk
|
||||
@@ -1444,8 +1444,8 @@ func (s3a *S3ApiServer) assignNewVolume(dstPath string, expectedDataSize uint64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("assign volume: %w", err)
|
||||
}
|
||||
if resp.Error != "" {
|
||||
return fmt.Errorf("assign volume: %v", resp.Error)
|
||||
if err := filer_pb.AssignVolumeResponseError(resp); err != nil {
|
||||
return fmt.Errorf("assign volume: %w", err)
|
||||
}
|
||||
assignResult = resp
|
||||
return nil
|
||||
@@ -1528,7 +1528,7 @@ func (s3a *S3ApiServer) copyChunksForRange(entry *filer_pb.Entry, startOffset, e
|
||||
executor.Execute(func() {
|
||||
dstChunk, err := s3a.copySingleChunkForRange(originalChunk, chunk, startOffset, endOffset, dstPath)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("chunk %d: %v", chunkIndex, err)
|
||||
errChan <- fmt.Errorf("chunk %d: %w", chunkIndex, err)
|
||||
return
|
||||
}
|
||||
dstChunks[chunkIndex] = dstChunk
|
||||
@@ -2653,7 +2653,7 @@ func (s3a *S3ApiServer) copyChunksWithReencryption(entry *filer_pb.Entry, copySo
|
||||
executor.Execute(func() {
|
||||
dstChunk, err := s3a.copyChunkWithReencryption(chunk, copySourceKey, destKey, dstPath, entry.Extended, destIV)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("chunk %d: %v", chunkIndex, err)
|
||||
errChan <- fmt.Errorf("chunk %d: %w", chunkIndex, err)
|
||||
return
|
||||
}
|
||||
dstChunks[chunkIndex] = dstChunk
|
||||
|
||||
@@ -580,8 +580,8 @@ func (s3a *S3ApiServer) putToFiler(r *http.Request, filePath string, dataReader
|
||||
if err != nil {
|
||||
return fmt.Errorf("assign volume: %w", err)
|
||||
}
|
||||
if resp.Error != "" {
|
||||
return fmt.Errorf("assign volume: %v", resp.Error)
|
||||
if err := filer_pb.AssignVolumeResponseError(resp); err != nil {
|
||||
return fmt.Errorf("assign volume: %w", err)
|
||||
}
|
||||
assignResult = resp
|
||||
return nil
|
||||
@@ -1491,8 +1491,13 @@ func filerErrorToS3Error(err error) s3err.ErrorCode {
|
||||
// added later that does — a request budget, an auth deadline, shutdown draining —
|
||||
// would have to cancel with its own cause and be excluded here, otherwise a body
|
||||
// truncated at that instant gets attributed to the peer.
|
||||
//
|
||||
// A read-only destination (the filer refused the volume assign, e.g. bucket over
|
||||
// quota) is AccessDenied, as in filerErrorToS3Error and mapCopyErrorToS3Error.
|
||||
func mapChunkedUploadErrorToS3Error(reqCtx context.Context, err error) s3err.ErrorCode {
|
||||
switch {
|
||||
case errors.Is(err, weed_server.ErrReadOnly):
|
||||
return s3err.ErrAccessDenied
|
||||
case strings.Contains(err.Error(), s3err.ErrMsgPayloadChecksumMismatch):
|
||||
return s3err.ErrInvalidDigest
|
||||
case errors.Is(err, operation.ErrTruncatedBody):
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/operation"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||
)
|
||||
|
||||
@@ -42,6 +43,22 @@ func TestMapChunkedUploadErrorToS3Error(t *testing.T) {
|
||||
err: errors.New("assign volume: no free volumes"),
|
||||
want: s3err.ErrInternalError,
|
||||
},
|
||||
{
|
||||
// Over-quota (read-only) buckets must be 403, not retryable 500.
|
||||
name: "filer read-only verdict maps to AccessDenied",
|
||||
err: fmt.Errorf("upload chunk: assign volume: %w", filer_pb.AssignVolumeResponseError(&filer_pb.AssignVolumeResponse{
|
||||
Error: "assign volume: read only: /buckets/q (e.g. bucket over quota)",
|
||||
ErrorCode: filer_pb.FilerError_READ_ONLY,
|
||||
})),
|
||||
want: s3err.ErrAccessDenied,
|
||||
},
|
||||
{
|
||||
// A full volume rejecting a write is a transient server fault the
|
||||
// client should retry, even though its text also says "read only".
|
||||
name: "volume server read-only write stays InternalError",
|
||||
err: errors.New("upload chunk: upload data: unexpected status 500: volume 5 is read only"),
|
||||
want: s3err.ErrInternalError,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||
weed_server "github.com/seaweedfs/seaweedfs/weed/server"
|
||||
)
|
||||
|
||||
// readOnlyAssignFiler answers AssignVolume the way a filer does for a path
|
||||
// under a read-only rule (e.g. an over-quota bucket), and counts the calls.
|
||||
type readOnlyAssignFiler struct {
|
||||
filer_pb.UnimplementedSeaweedFilerServer
|
||||
calls int32
|
||||
}
|
||||
|
||||
func (f *readOnlyAssignFiler) AssignVolume(context.Context, *filer_pb.AssignVolumeRequest) (*filer_pb.AssignVolumeResponse, error) {
|
||||
atomic.AddInt32(&f.calls, 1)
|
||||
return &filer_pb.AssignVolumeResponse{
|
||||
Error: "assign volume: read only: /buckets/b (e.g. bucket over quota)",
|
||||
ErrorCode: filer_pb.FilerError_READ_ONLY,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LookupDirectoryEntry finds nothing, so the bucket has no config to apply.
|
||||
func (f *readOnlyAssignFiler) LookupDirectoryEntry(context.Context, *filer_pb.LookupDirectoryEntryRequest) (*filer_pb.LookupDirectoryEntryResponse, error) {
|
||||
return &filer_pb.LookupDirectoryEntryResponse{}, nil
|
||||
}
|
||||
|
||||
// startReadOnlyFilers starts a read-only filer followed by a spare one, so a
|
||||
// test can assert the verdict was not treated as a reason to fail over.
|
||||
func startReadOnlyFilers(t *testing.T) (*S3ApiServer, *readOnlyAssignFiler, *readOnlyAssignFiler) {
|
||||
t.Helper()
|
||||
first, spare := &readOnlyAssignFiler{}, &readOnlyAssignFiler{}
|
||||
s3a := newPutTestServer(t, startFakeFiler(t, first), startFakeFiler(t, spare))
|
||||
return s3a, first, spare
|
||||
}
|
||||
|
||||
func assertNoFailover(t *testing.T, first, spare *readOnlyAssignFiler) {
|
||||
t.Helper()
|
||||
if calls := atomic.LoadInt32(&first.calls); calls == 0 {
|
||||
t.Fatal("the first filer was never asked")
|
||||
}
|
||||
if calls := atomic.LoadInt32(&spare.calls); calls != 0 {
|
||||
t.Fatalf("read-only verdict failed over to the next filer %d time(s)", calls)
|
||||
}
|
||||
}
|
||||
|
||||
// PutObject into an over-quota bucket is 403, not a retryable 500.
|
||||
func TestPutToFilerReadOnlyBucketIsAccessDenied(t *testing.T) {
|
||||
s3a, first, spare := startReadOnlyFilers(t)
|
||||
|
||||
if _, code := putTestObject(t, s3a); code != s3err.ErrAccessDenied {
|
||||
t.Fatalf("putToFiler = %v, want %v", code, s3err.ErrAccessDenied)
|
||||
}
|
||||
assertNoFailover(t, first, spare)
|
||||
}
|
||||
|
||||
func TestAssignNewVolumeReadOnlyKeepsSentinel(t *testing.T) {
|
||||
s3a, first, spare := startReadOnlyFilers(t)
|
||||
|
||||
_, err := s3a.assignNewVolume("/buckets/b/o", 1)
|
||||
if !errors.Is(err, weed_server.ErrReadOnly) {
|
||||
t.Fatalf("assignNewVolume err = %v, want ErrReadOnly", err)
|
||||
}
|
||||
assertNoFailover(t, first, spare)
|
||||
}
|
||||
|
||||
// The copy paths fan chunks out to workers; the sentinel must survive the
|
||||
// per-chunk wrapping so CopyObject and UploadPartCopy report 403 too.
|
||||
func TestCopyChunksReadOnlyIsAccessDenied(t *testing.T) {
|
||||
s3a, _, _ := startReadOnlyFilers(t)
|
||||
entry := &filer_pb.Entry{
|
||||
Attributes: &filer_pb.FuseAttributes{FileSize: 8},
|
||||
Chunks: []*filer_pb.FileChunk{
|
||||
{FileId: "3,01637037d6", Offset: 0, Size: 4},
|
||||
{FileId: "3,02637037d6", Offset: 4, Size: 4},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := s3a.copyChunks(entry, "/buckets/b/dst")
|
||||
if code := s3a.mapCopyErrorToS3Error(err); code != s3err.ErrAccessDenied {
|
||||
t.Fatalf("copyChunks err %v maps to %v, want %v", err, code, s3err.ErrAccessDenied)
|
||||
}
|
||||
|
||||
_, err = s3a.copyChunksForRange(entry, 0, 7, "/buckets/b/dst")
|
||||
if code := s3a.mapCopyErrorToS3Error(err); code != s3err.ErrAccessDenied {
|
||||
t.Fatalf("copyChunksForRange err %v maps to %v, want %v", err, code, s3err.ErrAccessDenied)
|
||||
}
|
||||
}
|
||||
@@ -909,7 +909,13 @@ func (fs *FilerServer) AssignVolume(ctx context.Context, req *filer_pb.AssignVol
|
||||
so, err := fs.resolveAssignStorageOption(ctx, req)
|
||||
if err != nil {
|
||||
glog.V(3).InfofCtx(ctx, "AssignVolume: %v", err)
|
||||
return &filer_pb.AssignVolumeResponse{Error: fmt.Sprintf("assign volume: %v", err)}, nil
|
||||
resp = &filer_pb.AssignVolumeResponse{Error: fmt.Sprintf("assign volume: %v", err)}
|
||||
if errors.Is(err, ErrReadOnly) {
|
||||
// Still a successful RPC: clients treat gRPC errors as transport
|
||||
// failures and retry or fail over, but read-only is a verdict.
|
||||
resp.ErrorCode = filer_pb.FilerError_READ_ONLY
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
assignRequest, altRequest := so.ToAssignRequests(int(req.Count))
|
||||
|
||||
@@ -2,12 +2,27 @@ 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{
|
||||
@@ -17,16 +32,7 @@ func TestResolveAssignStorageOptionUsesBucketRuleBeforeFilerDiskDefault(t *testi
|
||||
t.Fatalf("set location conf: %v", err)
|
||||
}
|
||||
|
||||
fs := &FilerServer{
|
||||
option: &FilerOption{
|
||||
DiskType: "hdd",
|
||||
},
|
||||
filer: &filer.Filer{
|
||||
DirBucketsPath: "/buckets",
|
||||
FilerConf: fc,
|
||||
MaxFilenameLength: 255,
|
||||
},
|
||||
}
|
||||
fs := newAssignTestServer(fc, "hdd")
|
||||
|
||||
so, err := fs.resolveAssignStorageOption(context.Background(), &filer_pb.AssignVolumeRequest{
|
||||
Path: "/buckets/zot/.uploads/upload-id/0001_part.part",
|
||||
@@ -44,16 +50,7 @@ func TestResolveAssignStorageOptionUsesBucketRuleBeforeFilerDiskDefault(t *testi
|
||||
}
|
||||
|
||||
func TestResolveAssignStorageOptionFallsBackToFilerDiskDefault(t *testing.T) {
|
||||
fs := &FilerServer{
|
||||
option: &FilerOption{
|
||||
DiskType: "hdd",
|
||||
},
|
||||
filer: &filer.Filer{
|
||||
DirBucketsPath: "/buckets",
|
||||
FilerConf: filer.NewFilerConf(),
|
||||
MaxFilenameLength: 255,
|
||||
},
|
||||
}
|
||||
fs := newAssignTestServer(filer.NewFilerConf(), "hdd")
|
||||
|
||||
so, err := fs.resolveAssignStorageOption(context.Background(), &filer_pb.AssignVolumeRequest{
|
||||
Path: "/tmp/unmatched/file.bin",
|
||||
@@ -66,3 +63,34 @@ func TestResolveAssignStorageOptionFallsBackToFilerDiskDefault(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,9 @@ var (
|
||||
OS_UID = uint32(os.Getuid())
|
||||
OS_GID = uint32(os.Getgid())
|
||||
|
||||
ErrReadOnly = errors.New("read only")
|
||||
// ErrReadOnly is filer_pb.ErrReadOnly so errors.Is matches on either side
|
||||
// of the gRPC boundary.
|
||||
ErrReadOnly = filer_pb.ErrReadOnly
|
||||
)
|
||||
|
||||
type FilerPostResult struct {
|
||||
|
||||
Reference in new issue
Block a user