filer: conditional UpdateEntry with a chunk-set write condition (#10382)

* filer: accept a WriteCondition on UpdateEntry, under the per-path lock

UpdateEntry was a bare read-modify-write: the precondition check, the
chunk garbage diff, and the store write could interleave with a
concurrent update to the same path. Take the per-path lock CreateEntry
already holds, and evaluate an optional CreateEntry-style WriteCondition
under it, failing with FailedPrecondition like expected_extended.

* filer: IF_CHUNKS_EQUAL write condition compares the stored chunk fid set

A chunk-preserving read-modify-write (tagging, setattr, copy-in-place)
races UpdateEntry's garbage diff: if a concurrent update empties the
chunk list first, the stale writer's commit resurrects fids that are
already queued for deletion, stranding the entry on a dead needle once
vacuum reclaims it. The reverse also holds: a writer that read an empty
chunk list can wipe chunks a concurrent update just added.

IF_CHUNKS_EQUAL guards both: the stored chunk fid multiset must still
equal what the caller read, order-independent, with an empty fids list
expecting no chunks. Absent entry counts as no chunks for CreateEntry
overwrites and transactions.

* filer: delete and append serialize on the entry path lock

DeleteEntry queues the entry's chunks for deletion and AppendToEntry
rewrites the chunk list, but neither held the per-path lock, so either
could interleave with a conditional update between its precondition
check and its write — a passed IF_CHUNKS_EQUAL would then resurrect
fids already on the deletion queue, or clobber a freshly appended
chunk. AppendToEntry keeps the cluster lock for cross-filer append
serialization; the path lock covers the local read-modify-write.

* filer: reuse lockPath in UpdateEntry lookup
This commit is contained in:
Chris Lu
2026-07-21 00:19:15 -07:00
committed by GitHub
parent 542495f1f1
commit cdb60069a6
8 changed files with 549 additions and 114 deletions
@@ -265,6 +265,7 @@ message WriteCondition {
IF_MODIFIED_SINCE = 6; // fail if present and mtime <= unix_time
IF_EXTENDED_NOT_EQUAL = 7; // fail if present and extended[ext_key] == ext_value
IF_EXTENDED_TIME_ELAPSED = 8; // fail if present and extended[ext_key] (unix seconds) is in the future
IF_CHUNKS_EQUAL = 9; // fail unless the stored chunk fid multiset equals fids (absent entry = no chunks)
}
// Clause is one primitive comparison. IF_ETAG_MATCH holds when the current
// entry's ETag equals any value in etags; IF_ETAG_NOT_MATCH holds when it
@@ -278,6 +279,11 @@ message WriteCondition {
// clock). The caller composes these and, for governance-bypass, simply omits
// the retention clause when the bypass is authorized — the filer makes no
// authorization decision.
//
// IF_CHUNKS_EQUAL guards a chunk-preserving read-modify-write: the stored
// chunk fid set must still equal what the caller read, so a stale write
// cannot resurrect needles that a concurrent update already diffed away
// and queued for deletion. An empty fids list expects no chunks.
message Clause {
Kind kind = 1;
repeated string etags = 2; // ETag set for IF_ETAG_* kinds
@@ -287,6 +293,7 @@ message WriteCondition {
string ext_value = 6; // blocking value for IF_EXTENDED_NOT_EQUAL
string gate_key = 7; // IF_EXTENDED_TIME_ELAPSED: only enforce when extended[gate_key] == gate_value
string gate_value = 8; // gate value (e.g. retention mode COMPLIANCE for governance bypass)
repeated string fids = 9; // chunk fid strings for IF_CHUNKS_EQUAL
}
repeated Clause clauses = 1; // all must hold (logical AND)
}
@@ -447,6 +454,10 @@ message UpdateEntryRequest {
bool is_from_other_cluster = 3;
repeated int32 signatures = 4;
map<string, bytes> expected_extended = 5;
// Optional precondition evaluated against the current entry atomically with
// the write, under the filer's per-path lock. The caller must route the
// key's writes to this entry's owner filer for the check to be authoritative.
WriteCondition condition = 6;
}
message UpdateEntryResponse {
SubscribeMetadataResponse metadata_event = 1;
+11
View File
@@ -265,6 +265,7 @@ message WriteCondition {
IF_MODIFIED_SINCE = 6; // fail if present and mtime <= unix_time
IF_EXTENDED_NOT_EQUAL = 7; // fail if present and extended[ext_key] == ext_value
IF_EXTENDED_TIME_ELAPSED = 8; // fail if present and extended[ext_key] (unix seconds) is in the future
IF_CHUNKS_EQUAL = 9; // fail unless the stored chunk fid multiset equals fids (absent entry = no chunks)
}
// Clause is one primitive comparison. IF_ETAG_MATCH holds when the current
// entry's ETag equals any value in etags; IF_ETAG_NOT_MATCH holds when it
@@ -278,6 +279,11 @@ message WriteCondition {
// clock). The caller composes these and, for governance-bypass, simply omits
// the retention clause when the bypass is authorized — the filer makes no
// authorization decision.
//
// IF_CHUNKS_EQUAL guards a chunk-preserving read-modify-write: the stored
// chunk fid set must still equal what the caller read, so a stale write
// cannot resurrect needles that a concurrent update already diffed away
// and queued for deletion. An empty fids list expects no chunks.
message Clause {
Kind kind = 1;
repeated string etags = 2; // ETag set for IF_ETAG_* kinds
@@ -287,6 +293,7 @@ message WriteCondition {
string ext_value = 6; // blocking value for IF_EXTENDED_NOT_EQUAL
string gate_key = 7; // IF_EXTENDED_TIME_ELAPSED: only enforce when extended[gate_key] == gate_value
string gate_value = 8; // gate value (e.g. retention mode COMPLIANCE for governance bypass)
repeated string fids = 9; // chunk fid strings for IF_CHUNKS_EQUAL
}
repeated Clause clauses = 1; // all must hold (logical AND)
}
@@ -447,6 +454,10 @@ message UpdateEntryRequest {
bool is_from_other_cluster = 3;
repeated int32 signatures = 4;
map<string, bytes> expected_extended = 5;
// Optional precondition evaluated against the current entry atomically with
// the write, under the filer's per-path lock. The caller must route the
// key's writes to this entry's owner filer for the check to be authoritative.
WriteCondition condition = 6;
}
message UpdateEntryResponse {
SubscribeMetadataResponse metadata_event = 1;
+143 -112
View File
@@ -206,6 +206,7 @@ const (
WriteCondition_IF_MODIFIED_SINCE WriteCondition_Kind = 6 // fail if present and mtime <= unix_time
WriteCondition_IF_EXTENDED_NOT_EQUAL WriteCondition_Kind = 7 // fail if present and extended[ext_key] == ext_value
WriteCondition_IF_EXTENDED_TIME_ELAPSED WriteCondition_Kind = 8 // fail if present and extended[ext_key] (unix seconds) is in the future
WriteCondition_IF_CHUNKS_EQUAL WriteCondition_Kind = 9 // fail unless the stored chunk fid multiset equals fids (absent entry = no chunks)
)
// Enum value maps for WriteCondition_Kind.
@@ -220,6 +221,7 @@ var (
6: "IF_MODIFIED_SINCE",
7: "IF_EXTENDED_NOT_EQUAL",
8: "IF_EXTENDED_TIME_ELAPSED",
9: "IF_CHUNKS_EQUAL",
}
WriteCondition_Kind_value = map[string]int32{
"NONE": 0,
@@ -231,6 +233,7 @@ var (
"IF_MODIFIED_SINCE": 6,
"IF_EXTENDED_NOT_EQUAL": 7,
"IF_EXTENDED_TIME_ELAPSED": 8,
"IF_CHUNKS_EQUAL": 9,
}
)
@@ -2310,8 +2313,12 @@ type UpdateEntryRequest struct {
IsFromOtherCluster bool `protobuf:"varint,3,opt,name=is_from_other_cluster,json=isFromOtherCluster,proto3" json:"is_from_other_cluster,omitempty"`
Signatures []int32 `protobuf:"varint,4,rep,packed,name=signatures,proto3" json:"signatures,omitempty"`
ExpectedExtended map[string][]byte `protobuf:"bytes,5,rep,name=expected_extended,json=expectedExtended,proto3" json:"expected_extended,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// Optional precondition evaluated against the current entry atomically with
// the write, under the filer's per-path lock. The caller must route the
// key's writes to this entry's owner filer for the check to be authoritative.
Condition *WriteCondition `protobuf:"bytes,6,opt,name=condition,proto3" json:"condition,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateEntryRequest) Reset() {
@@ -2379,6 +2386,13 @@ func (x *UpdateEntryRequest) GetExpectedExtended() map[string][]byte {
return nil
}
func (x *UpdateEntryRequest) GetCondition() *WriteCondition {
if x != nil {
return x.Condition
}
return nil
}
type UpdateEntryResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
MetadataEvent *SubscribeMetadataResponse `protobuf:"bytes,1,opt,name=metadata_event,json=metadataEvent,proto3" json:"metadata_event,omitempty"`
@@ -6491,6 +6505,11 @@ func (x *MountInfo) GetDataCenter() string {
// clock). The caller composes these and, for governance-bypass, simply omits
// the retention clause when the bypass is authorized — the filer makes no
// authorization decision.
//
// IF_CHUNKS_EQUAL guards a chunk-preserving read-modify-write: the stored
// chunk fid set must still equal what the caller read, so a stale write
// cannot resurrect needles that a concurrent update already diffed away
// and queued for deletion. An empty fids list expects no chunks.
type WriteCondition_Clause struct {
state protoimpl.MessageState `protogen:"open.v1"`
Kind WriteCondition_Kind `protobuf:"varint,1,opt,name=kind,proto3,enum=filer_pb.WriteCondition_Kind" json:"kind,omitempty"`
@@ -6501,6 +6520,7 @@ type WriteCondition_Clause struct {
ExtValue string `protobuf:"bytes,6,opt,name=ext_value,json=extValue,proto3" json:"ext_value,omitempty"` // blocking value for IF_EXTENDED_NOT_EQUAL
GateKey string `protobuf:"bytes,7,opt,name=gate_key,json=gateKey,proto3" json:"gate_key,omitempty"` // IF_EXTENDED_TIME_ELAPSED: only enforce when extended[gate_key] == gate_value
GateValue string `protobuf:"bytes,8,opt,name=gate_value,json=gateValue,proto3" json:"gate_value,omitempty"` // gate value (e.g. retention mode COMPLIANCE for governance bypass)
Fids []string `protobuf:"bytes,9,rep,name=fids,proto3" json:"fids,omitempty"` // chunk fid strings for IF_CHUNKS_EQUAL
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -6591,6 +6611,13 @@ func (x *WriteCondition_Clause) GetGateValue() string {
return ""
}
func (x *WriteCondition_Clause) GetFids() []string {
if x != nil {
return x.Fids
}
return nil
}
// if found, send the exact address
// if not found, send the full list of existing brokers
type LocateBrokerResponse_Resource struct {
@@ -6925,9 +6952,9 @@ const file_filer_proto_rawDesc = "" +
"signatures\x18\x05 \x03(\x05R\n" +
"signatures\x12=\n" +
"\x1bskip_check_parent_directory\x18\x06 \x01(\bR\x18skipCheckParentDirectory\x126\n" +
"\tcondition\x18\a \x01(\v2\x18.filer_pb.WriteConditionR\tcondition\"\x93\x04\n" +
"\tcondition\x18\a \x01(\v2\x18.filer_pb.WriteConditionR\tcondition\"\xbc\x04\n" +
"\x0eWriteCondition\x129\n" +
"\aclauses\x18\x01 \x03(\v2\x1f.filer_pb.WriteCondition.ClauseR\aclauses\x1a\xfd\x01\n" +
"\aclauses\x18\x01 \x03(\v2\x1f.filer_pb.WriteCondition.ClauseR\aclauses\x1a\x91\x02\n" +
"\x06Clause\x121\n" +
"\x04kind\x18\x01 \x01(\x0e2\x1d.filer_pb.WriteCondition.KindR\x04kind\x12\x14\n" +
"\x05etags\x18\x02 \x03(\tR\x05etags\x12\x1b\n" +
@@ -6938,7 +6965,8 @@ const file_filer_proto_rawDesc = "" +
"\text_value\x18\x06 \x01(\tR\bextValue\x12\x19\n" +
"\bgate_key\x18\a \x01(\tR\agateKey\x12\x1d\n" +
"\n" +
"gate_value\x18\b \x01(\tR\tgateValue\"\xc5\x01\n" +
"gate_value\x18\b \x01(\tR\tgateValue\x12\x12\n" +
"\x04fids\x18\t \x03(\tR\x04fids\"\xda\x01\n" +
"\x04Kind\x12\b\n" +
"\x04NONE\x10\x00\x12\x11\n" +
"\rIF_NOT_EXISTS\x10\x01\x12\r\n" +
@@ -6948,7 +6976,8 @@ const file_filer_proto_rawDesc = "" +
"\x13IF_UNMODIFIED_SINCE\x10\x05\x12\x15\n" +
"\x11IF_MODIFIED_SINCE\x10\x06\x12\x19\n" +
"\x15IF_EXTENDED_NOT_EQUAL\x10\a\x12\x1c\n" +
"\x18IF_EXTENDED_TIME_ELAPSED\x10\b\"\xa2\x05\n" +
"\x18IF_EXTENDED_TIME_ELAPSED\x10\b\x12\x13\n" +
"\x0fIF_CHUNKS_EQUAL\x10\t\"\xa2\x05\n" +
"\x0eObjectMutation\x121\n" +
"\x04type\x18\x01 \x01(\x0e2\x1d.filer_pb.ObjectMutation.TypeR\x04type\x12\x1c\n" +
"\tdirectory\x18\x02 \x01(\tR\tdirectory\x12\x12\n" +
@@ -7034,7 +7063,7 @@ const file_filer_proto_rawDesc = "" +
"\x05error\x18\x01 \x01(\tR\x05error\x12J\n" +
"\x0emetadata_event\x18\x02 \x01(\v2#.filer_pb.SubscribeMetadataResponseR\rmetadataEvent\x123\n" +
"\n" +
"error_code\x18\x03 \x01(\x0e2\x14.filer_pb.FilerErrorR\terrorCode\"\xd2\x02\n" +
"error_code\x18\x03 \x01(\x0e2\x14.filer_pb.FilerErrorR\terrorCode\"\x8a\x03\n" +
"\x12UpdateEntryRequest\x12\x1c\n" +
"\tdirectory\x18\x01 \x01(\tR\tdirectory\x12%\n" +
"\x05entry\x18\x02 \x01(\v2\x0f.filer_pb.EntryR\x05entry\x121\n" +
@@ -7042,7 +7071,8 @@ const file_filer_proto_rawDesc = "" +
"\n" +
"signatures\x18\x04 \x03(\x05R\n" +
"signatures\x12_\n" +
"\x11expected_extended\x18\x05 \x03(\v22.filer_pb.UpdateEntryRequest.ExpectedExtendedEntryR\x10expectedExtended\x1aC\n" +
"\x11expected_extended\x18\x05 \x03(\v22.filer_pb.UpdateEntryRequest.ExpectedExtendedEntryR\x10expectedExtended\x126\n" +
"\tcondition\x18\x06 \x01(\v2\x18.filer_pb.WriteConditionR\tcondition\x1aC\n" +
"\x15ExpectedExtendedEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\"a\n" +
@@ -7620,110 +7650,111 @@ var file_filer_proto_depIdxs = []int32{
1, // 31: filer_pb.CreateEntryResponse.error_code:type_name -> filer_pb.FilerError
10, // 32: filer_pb.UpdateEntryRequest.entry:type_name -> filer_pb.Entry
100, // 33: filer_pb.UpdateEntryRequest.expected_extended:type_name -> filer_pb.UpdateEntryRequest.ExpectedExtendedEntry
59, // 34: filer_pb.UpdateEntryResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
13, // 35: filer_pb.AppendToEntryRequest.chunks:type_name -> filer_pb.FileChunk
59, // 36: filer_pb.DeleteEntryResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
12, // 37: filer_pb.StreamRenameEntryResponse.event_notification:type_name -> filer_pb.EventNotification
45, // 38: filer_pb.AssignVolumeResponse.location:type_name -> filer_pb.Location
45, // 39: filer_pb.AssignVolumeResponse.replicas:type_name -> filer_pb.Location
45, // 40: filer_pb.Locations.locations:type_name -> filer_pb.Location
101, // 41: filer_pb.LookupVolumeResponse.locations_map:type_name -> filer_pb.LookupVolumeResponse.LocationsMapEntry
47, // 42: filer_pb.CollectionListResponse.collections:type_name -> filer_pb.Collection
12, // 43: filer_pb.SubscribeMetadataResponse.event_notification:type_name -> filer_pb.EventNotification
59, // 44: filer_pb.SubscribeMetadataResponse.events:type_name -> filer_pb.SubscribeMetadataResponse
63, // 45: filer_pb.SubscribeMetadataResponse.log_file_refs:type_name -> filer_pb.LogFileChunkRef
62, // 46: filer_pb.ListMetadataSubscribersResponse.subscribers:type_name -> filer_pb.MetadataSubscriber
13, // 47: filer_pb.LogFileChunkRef.chunks:type_name -> filer_pb.FileChunk
10, // 48: filer_pb.TraverseBfsMetadataResponse.entry:type_name -> filer_pb.Entry
102, // 49: filer_pb.LocateBrokerResponse.resources:type_name -> filer_pb.LocateBrokerResponse.Resource
103, // 50: filer_pb.FilerConf.locations:type_name -> filer_pb.FilerConf.PathConf
10, // 51: filer_pb.CacheRemoteObjectToLocalClusterResponse.entry:type_name -> filer_pb.Entry
59, // 52: filer_pb.CacheRemoteObjectToLocalClusterResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
84, // 53: filer_pb.TransferLocksRequest.locks:type_name -> filer_pb.Lock
17, // 54: filer_pb.StreamMutateEntryRequest.create_request:type_name -> filer_pb.CreateEntryRequest
29, // 55: filer_pb.StreamMutateEntryRequest.update_request:type_name -> filer_pb.UpdateEntryRequest
35, // 56: filer_pb.StreamMutateEntryRequest.delete_request:type_name -> filer_pb.DeleteEntryRequest
39, // 57: filer_pb.StreamMutateEntryRequest.rename_request:type_name -> filer_pb.StreamRenameEntryRequest
28, // 58: filer_pb.StreamMutateEntryResponse.create_response:type_name -> filer_pb.CreateEntryResponse
30, // 59: filer_pb.StreamMutateEntryResponse.update_response:type_name -> filer_pb.UpdateEntryResponse
36, // 60: filer_pb.StreamMutateEntryResponse.delete_response:type_name -> filer_pb.DeleteEntryResponse
40, // 61: filer_pb.StreamMutateEntryResponse.rename_response:type_name -> filer_pb.StreamRenameEntryResponse
95, // 62: filer_pb.MountListResponse.mounts:type_name -> filer_pb.MountInfo
3, // 63: filer_pb.WriteCondition.Clause.kind:type_name -> filer_pb.WriteCondition.Kind
44, // 64: filer_pb.LookupVolumeResponse.LocationsMapEntry.value:type_name -> filer_pb.Locations
5, // 65: filer_pb.SeaweedFiler.LookupDirectoryEntry:input_type -> filer_pb.LookupDirectoryEntryRequest
7, // 66: filer_pb.SeaweedFiler.ListEntries:input_type -> filer_pb.ListEntriesRequest
17, // 67: filer_pb.SeaweedFiler.CreateEntry:input_type -> filer_pb.CreateEntryRequest
29, // 68: filer_pb.SeaweedFiler.UpdateEntry:input_type -> filer_pb.UpdateEntryRequest
31, // 69: filer_pb.SeaweedFiler.TouchAccessTime:input_type -> filer_pb.TouchAccessTimeRequest
33, // 70: filer_pb.SeaweedFiler.AppendToEntry:input_type -> filer_pb.AppendToEntryRequest
35, // 71: filer_pb.SeaweedFiler.DeleteEntry:input_type -> filer_pb.DeleteEntryRequest
21, // 72: filer_pb.SeaweedFiler.ObjectTransaction:input_type -> filer_pb.ObjectTransactionRequest
26, // 73: filer_pb.SeaweedFiler.ObjectTransactionBatch:input_type -> filer_pb.ObjectTransactionBatchRequest
24, // 74: filer_pb.SeaweedFiler.PosixLock:input_type -> filer_pb.PosixLockRequest
37, // 75: filer_pb.SeaweedFiler.AtomicRenameEntry:input_type -> filer_pb.AtomicRenameEntryRequest
39, // 76: filer_pb.SeaweedFiler.StreamRenameEntry:input_type -> filer_pb.StreamRenameEntryRequest
89, // 77: filer_pb.SeaweedFiler.StreamMutateEntry:input_type -> filer_pb.StreamMutateEntryRequest
41, // 78: filer_pb.SeaweedFiler.AssignVolume:input_type -> filer_pb.AssignVolumeRequest
43, // 79: filer_pb.SeaweedFiler.LookupVolume:input_type -> filer_pb.LookupVolumeRequest
48, // 80: filer_pb.SeaweedFiler.CollectionList:input_type -> filer_pb.CollectionListRequest
50, // 81: filer_pb.SeaweedFiler.DeleteCollection:input_type -> filer_pb.DeleteCollectionRequest
52, // 82: filer_pb.SeaweedFiler.Statistics:input_type -> filer_pb.StatisticsRequest
54, // 83: filer_pb.SeaweedFiler.Ping:input_type -> filer_pb.PingRequest
56, // 84: filer_pb.SeaweedFiler.GetFilerConfiguration:input_type -> filer_pb.GetFilerConfigurationRequest
64, // 85: filer_pb.SeaweedFiler.TraverseBfsMetadata:input_type -> filer_pb.TraverseBfsMetadataRequest
58, // 86: filer_pb.SeaweedFiler.SubscribeMetadata:input_type -> filer_pb.SubscribeMetadataRequest
58, // 87: filer_pb.SeaweedFiler.SubscribeLocalMetadata:input_type -> filer_pb.SubscribeMetadataRequest
60, // 88: filer_pb.SeaweedFiler.ListMetadataSubscribers:input_type -> filer_pb.ListMetadataSubscribersRequest
71, // 89: filer_pb.SeaweedFiler.KvGet:input_type -> filer_pb.KvGetRequest
73, // 90: filer_pb.SeaweedFiler.KvPut:input_type -> filer_pb.KvPutRequest
76, // 91: filer_pb.SeaweedFiler.CacheRemoteObjectToLocalCluster:input_type -> filer_pb.CacheRemoteObjectToLocalClusterRequest
78, // 92: filer_pb.SeaweedFiler.DistributedLock:input_type -> filer_pb.LockRequest
80, // 93: filer_pb.SeaweedFiler.DistributedUnlock:input_type -> filer_pb.UnlockRequest
82, // 94: filer_pb.SeaweedFiler.FindLockOwner:input_type -> filer_pb.FindLockOwnerRequest
85, // 95: filer_pb.SeaweedFiler.TransferLocks:input_type -> filer_pb.TransferLocksRequest
87, // 96: filer_pb.SeaweedFiler.ReplicateLock:input_type -> filer_pb.ReplicateLockRequest
91, // 97: filer_pb.SeaweedFiler.MountRegister:input_type -> filer_pb.MountRegisterRequest
93, // 98: filer_pb.SeaweedFiler.MountList:input_type -> filer_pb.MountListRequest
6, // 99: filer_pb.SeaweedFiler.LookupDirectoryEntry:output_type -> filer_pb.LookupDirectoryEntryResponse
8, // 100: filer_pb.SeaweedFiler.ListEntries:output_type -> filer_pb.ListEntriesResponse
28, // 101: filer_pb.SeaweedFiler.CreateEntry:output_type -> filer_pb.CreateEntryResponse
30, // 102: filer_pb.SeaweedFiler.UpdateEntry:output_type -> filer_pb.UpdateEntryResponse
32, // 103: filer_pb.SeaweedFiler.TouchAccessTime:output_type -> filer_pb.TouchAccessTimeResponse
34, // 104: filer_pb.SeaweedFiler.AppendToEntry:output_type -> filer_pb.AppendToEntryResponse
36, // 105: filer_pb.SeaweedFiler.DeleteEntry:output_type -> filer_pb.DeleteEntryResponse
22, // 106: filer_pb.SeaweedFiler.ObjectTransaction:output_type -> filer_pb.ObjectTransactionResponse
27, // 107: filer_pb.SeaweedFiler.ObjectTransactionBatch:output_type -> filer_pb.ObjectTransactionBatchResponse
25, // 108: filer_pb.SeaweedFiler.PosixLock:output_type -> filer_pb.PosixLockResponse
38, // 109: filer_pb.SeaweedFiler.AtomicRenameEntry:output_type -> filer_pb.AtomicRenameEntryResponse
40, // 110: filer_pb.SeaweedFiler.StreamRenameEntry:output_type -> filer_pb.StreamRenameEntryResponse
90, // 111: filer_pb.SeaweedFiler.StreamMutateEntry:output_type -> filer_pb.StreamMutateEntryResponse
42, // 112: filer_pb.SeaweedFiler.AssignVolume:output_type -> filer_pb.AssignVolumeResponse
46, // 113: filer_pb.SeaweedFiler.LookupVolume:output_type -> filer_pb.LookupVolumeResponse
49, // 114: filer_pb.SeaweedFiler.CollectionList:output_type -> filer_pb.CollectionListResponse
51, // 115: filer_pb.SeaweedFiler.DeleteCollection:output_type -> filer_pb.DeleteCollectionResponse
53, // 116: filer_pb.SeaweedFiler.Statistics:output_type -> filer_pb.StatisticsResponse
55, // 117: filer_pb.SeaweedFiler.Ping:output_type -> filer_pb.PingResponse
57, // 118: filer_pb.SeaweedFiler.GetFilerConfiguration:output_type -> filer_pb.GetFilerConfigurationResponse
65, // 119: filer_pb.SeaweedFiler.TraverseBfsMetadata:output_type -> filer_pb.TraverseBfsMetadataResponse
59, // 120: filer_pb.SeaweedFiler.SubscribeMetadata:output_type -> filer_pb.SubscribeMetadataResponse
59, // 121: filer_pb.SeaweedFiler.SubscribeLocalMetadata:output_type -> filer_pb.SubscribeMetadataResponse
61, // 122: filer_pb.SeaweedFiler.ListMetadataSubscribers:output_type -> filer_pb.ListMetadataSubscribersResponse
72, // 123: filer_pb.SeaweedFiler.KvGet:output_type -> filer_pb.KvGetResponse
74, // 124: filer_pb.SeaweedFiler.KvPut:output_type -> filer_pb.KvPutResponse
77, // 125: filer_pb.SeaweedFiler.CacheRemoteObjectToLocalCluster:output_type -> filer_pb.CacheRemoteObjectToLocalClusterResponse
79, // 126: filer_pb.SeaweedFiler.DistributedLock:output_type -> filer_pb.LockResponse
81, // 127: filer_pb.SeaweedFiler.DistributedUnlock:output_type -> filer_pb.UnlockResponse
83, // 128: filer_pb.SeaweedFiler.FindLockOwner:output_type -> filer_pb.FindLockOwnerResponse
86, // 129: filer_pb.SeaweedFiler.TransferLocks:output_type -> filer_pb.TransferLocksResponse
88, // 130: filer_pb.SeaweedFiler.ReplicateLock:output_type -> filer_pb.ReplicateLockResponse
92, // 131: filer_pb.SeaweedFiler.MountRegister:output_type -> filer_pb.MountRegisterResponse
94, // 132: filer_pb.SeaweedFiler.MountList:output_type -> filer_pb.MountListResponse
99, // [99:133] is the sub-list for method output_type
65, // [65:99] is the sub-list for method input_type
65, // [65:65] is the sub-list for extension type_name
65, // [65:65] is the sub-list for extension extendee
0, // [0:65] is the sub-list for field type_name
18, // 34: filer_pb.UpdateEntryRequest.condition:type_name -> filer_pb.WriteCondition
59, // 35: filer_pb.UpdateEntryResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
13, // 36: filer_pb.AppendToEntryRequest.chunks:type_name -> filer_pb.FileChunk
59, // 37: filer_pb.DeleteEntryResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
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
101, // 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
102, // 50: filer_pb.LocateBrokerResponse.resources:type_name -> filer_pb.LocateBrokerResponse.Resource
103, // 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
3, // 64: filer_pb.WriteCondition.Clause.kind:type_name -> filer_pb.WriteCondition.Kind
44, // 65: filer_pb.LookupVolumeResponse.LocationsMapEntry.value:type_name -> filer_pb.Locations
5, // 66: filer_pb.SeaweedFiler.LookupDirectoryEntry:input_type -> filer_pb.LookupDirectoryEntryRequest
7, // 67: filer_pb.SeaweedFiler.ListEntries:input_type -> filer_pb.ListEntriesRequest
17, // 68: filer_pb.SeaweedFiler.CreateEntry:input_type -> filer_pb.CreateEntryRequest
29, // 69: filer_pb.SeaweedFiler.UpdateEntry:input_type -> filer_pb.UpdateEntryRequest
31, // 70: filer_pb.SeaweedFiler.TouchAccessTime:input_type -> filer_pb.TouchAccessTimeRequest
33, // 71: filer_pb.SeaweedFiler.AppendToEntry:input_type -> filer_pb.AppendToEntryRequest
35, // 72: filer_pb.SeaweedFiler.DeleteEntry:input_type -> filer_pb.DeleteEntryRequest
21, // 73: filer_pb.SeaweedFiler.ObjectTransaction:input_type -> filer_pb.ObjectTransactionRequest
26, // 74: filer_pb.SeaweedFiler.ObjectTransactionBatch:input_type -> filer_pb.ObjectTransactionBatchRequest
24, // 75: filer_pb.SeaweedFiler.PosixLock:input_type -> filer_pb.PosixLockRequest
37, // 76: filer_pb.SeaweedFiler.AtomicRenameEntry:input_type -> filer_pb.AtomicRenameEntryRequest
39, // 77: filer_pb.SeaweedFiler.StreamRenameEntry:input_type -> filer_pb.StreamRenameEntryRequest
89, // 78: filer_pb.SeaweedFiler.StreamMutateEntry:input_type -> filer_pb.StreamMutateEntryRequest
41, // 79: filer_pb.SeaweedFiler.AssignVolume:input_type -> filer_pb.AssignVolumeRequest
43, // 80: filer_pb.SeaweedFiler.LookupVolume:input_type -> filer_pb.LookupVolumeRequest
48, // 81: filer_pb.SeaweedFiler.CollectionList:input_type -> filer_pb.CollectionListRequest
50, // 82: filer_pb.SeaweedFiler.DeleteCollection:input_type -> filer_pb.DeleteCollectionRequest
52, // 83: filer_pb.SeaweedFiler.Statistics:input_type -> filer_pb.StatisticsRequest
54, // 84: filer_pb.SeaweedFiler.Ping:input_type -> filer_pb.PingRequest
56, // 85: filer_pb.SeaweedFiler.GetFilerConfiguration:input_type -> filer_pb.GetFilerConfigurationRequest
64, // 86: filer_pb.SeaweedFiler.TraverseBfsMetadata:input_type -> filer_pb.TraverseBfsMetadataRequest
58, // 87: filer_pb.SeaweedFiler.SubscribeMetadata:input_type -> filer_pb.SubscribeMetadataRequest
58, // 88: filer_pb.SeaweedFiler.SubscribeLocalMetadata:input_type -> filer_pb.SubscribeMetadataRequest
60, // 89: filer_pb.SeaweedFiler.ListMetadataSubscribers:input_type -> filer_pb.ListMetadataSubscribersRequest
71, // 90: filer_pb.SeaweedFiler.KvGet:input_type -> filer_pb.KvGetRequest
73, // 91: filer_pb.SeaweedFiler.KvPut:input_type -> filer_pb.KvPutRequest
76, // 92: filer_pb.SeaweedFiler.CacheRemoteObjectToLocalCluster:input_type -> filer_pb.CacheRemoteObjectToLocalClusterRequest
78, // 93: filer_pb.SeaweedFiler.DistributedLock:input_type -> filer_pb.LockRequest
80, // 94: filer_pb.SeaweedFiler.DistributedUnlock:input_type -> filer_pb.UnlockRequest
82, // 95: filer_pb.SeaweedFiler.FindLockOwner:input_type -> filer_pb.FindLockOwnerRequest
85, // 96: filer_pb.SeaweedFiler.TransferLocks:input_type -> filer_pb.TransferLocksRequest
87, // 97: filer_pb.SeaweedFiler.ReplicateLock:input_type -> filer_pb.ReplicateLockRequest
91, // 98: filer_pb.SeaweedFiler.MountRegister:input_type -> filer_pb.MountRegisterRequest
93, // 99: filer_pb.SeaweedFiler.MountList:input_type -> filer_pb.MountListRequest
6, // 100: filer_pb.SeaweedFiler.LookupDirectoryEntry:output_type -> filer_pb.LookupDirectoryEntryResponse
8, // 101: filer_pb.SeaweedFiler.ListEntries:output_type -> filer_pb.ListEntriesResponse
28, // 102: filer_pb.SeaweedFiler.CreateEntry:output_type -> filer_pb.CreateEntryResponse
30, // 103: filer_pb.SeaweedFiler.UpdateEntry:output_type -> filer_pb.UpdateEntryResponse
32, // 104: filer_pb.SeaweedFiler.TouchAccessTime:output_type -> filer_pb.TouchAccessTimeResponse
34, // 105: filer_pb.SeaweedFiler.AppendToEntry:output_type -> filer_pb.AppendToEntryResponse
36, // 106: filer_pb.SeaweedFiler.DeleteEntry:output_type -> filer_pb.DeleteEntryResponse
22, // 107: filer_pb.SeaweedFiler.ObjectTransaction:output_type -> filer_pb.ObjectTransactionResponse
27, // 108: filer_pb.SeaweedFiler.ObjectTransactionBatch:output_type -> filer_pb.ObjectTransactionBatchResponse
25, // 109: filer_pb.SeaweedFiler.PosixLock:output_type -> filer_pb.PosixLockResponse
38, // 110: filer_pb.SeaweedFiler.AtomicRenameEntry:output_type -> filer_pb.AtomicRenameEntryResponse
40, // 111: filer_pb.SeaweedFiler.StreamRenameEntry:output_type -> filer_pb.StreamRenameEntryResponse
90, // 112: filer_pb.SeaweedFiler.StreamMutateEntry:output_type -> filer_pb.StreamMutateEntryResponse
42, // 113: filer_pb.SeaweedFiler.AssignVolume:output_type -> filer_pb.AssignVolumeResponse
46, // 114: filer_pb.SeaweedFiler.LookupVolume:output_type -> filer_pb.LookupVolumeResponse
49, // 115: filer_pb.SeaweedFiler.CollectionList:output_type -> filer_pb.CollectionListResponse
51, // 116: filer_pb.SeaweedFiler.DeleteCollection:output_type -> filer_pb.DeleteCollectionResponse
53, // 117: filer_pb.SeaweedFiler.Statistics:output_type -> filer_pb.StatisticsResponse
55, // 118: filer_pb.SeaweedFiler.Ping:output_type -> filer_pb.PingResponse
57, // 119: filer_pb.SeaweedFiler.GetFilerConfiguration:output_type -> filer_pb.GetFilerConfigurationResponse
65, // 120: filer_pb.SeaweedFiler.TraverseBfsMetadata:output_type -> filer_pb.TraverseBfsMetadataResponse
59, // 121: filer_pb.SeaweedFiler.SubscribeMetadata:output_type -> filer_pb.SubscribeMetadataResponse
59, // 122: filer_pb.SeaweedFiler.SubscribeLocalMetadata:output_type -> filer_pb.SubscribeMetadataResponse
61, // 123: filer_pb.SeaweedFiler.ListMetadataSubscribers:output_type -> filer_pb.ListMetadataSubscribersResponse
72, // 124: filer_pb.SeaweedFiler.KvGet:output_type -> filer_pb.KvGetResponse
74, // 125: filer_pb.SeaweedFiler.KvPut:output_type -> filer_pb.KvPutResponse
77, // 126: filer_pb.SeaweedFiler.CacheRemoteObjectToLocalCluster:output_type -> filer_pb.CacheRemoteObjectToLocalClusterResponse
79, // 127: filer_pb.SeaweedFiler.DistributedLock:output_type -> filer_pb.LockResponse
81, // 128: filer_pb.SeaweedFiler.DistributedUnlock:output_type -> filer_pb.UnlockResponse
83, // 129: filer_pb.SeaweedFiler.FindLockOwner:output_type -> filer_pb.FindLockOwnerResponse
86, // 130: filer_pb.SeaweedFiler.TransferLocks:output_type -> filer_pb.TransferLocksResponse
88, // 131: filer_pb.SeaweedFiler.ReplicateLock:output_type -> filer_pb.ReplicateLockResponse
92, // 132: filer_pb.SeaweedFiler.MountRegister:output_type -> filer_pb.MountRegisterResponse
94, // 133: filer_pb.SeaweedFiler.MountList:output_type -> filer_pb.MountListResponse
100, // [100:134] is the sub-list for method output_type
66, // [66:100] is the sub-list for method input_type
66, // [66:66] is the sub-list for extension type_name
66, // [66:66] is the sub-list for extension extendee
0, // [0:66] is the sub-list for field type_name
}
func init() { file_filer_proto_init() }
+97
View File
@@ -1105,6 +1105,15 @@ func (m *WriteCondition_Clause) MarshalToSizedBufferVT(dAtA []byte) (int, error)
i -= len(m.unknownFields)
copy(dAtA[i:], m.unknownFields)
}
if len(m.Fids) > 0 {
for iNdEx := len(m.Fids) - 1; iNdEx >= 0; iNdEx-- {
i -= len(m.Fids[iNdEx])
copy(dAtA[i:], m.Fids[iNdEx])
i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Fids[iNdEx])))
i--
dAtA[i] = 0x4a
}
}
if len(m.GateValue) > 0 {
i -= len(m.GateValue)
copy(dAtA[i:], m.GateValue)
@@ -2038,6 +2047,16 @@ func (m *UpdateEntryRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
i -= len(m.unknownFields)
copy(dAtA[i:], m.unknownFields)
}
if m.Condition != nil {
size, err := m.Condition.MarshalToSizedBufferVT(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = protohelpers.EncodeVarint(dAtA, i, uint64(size))
i--
dAtA[i] = 0x32
}
if len(m.ExpectedExtended) > 0 {
for k := range m.ExpectedExtended {
v := m.ExpectedExtended[k]
@@ -6615,6 +6634,12 @@ func (m *WriteCondition_Clause) SizeVT() (n int) {
if l > 0 {
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
if len(m.Fids) > 0 {
for _, s := range m.Fids {
l = len(s)
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
}
n += len(m.unknownFields)
return n
}
@@ -6979,6 +7004,10 @@ func (m *UpdateEntryRequest) SizeVT() (n int) {
n += mapEntrySize + 1 + protohelpers.SizeOfVarint(uint64(mapEntrySize))
}
}
if m.Condition != nil {
l = m.Condition.SizeVT()
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
n += len(m.unknownFields)
return n
}
@@ -11801,6 +11830,38 @@ func (m *WriteCondition_Clause) UnmarshalVT(dAtA []byte) error {
}
m.GateValue = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 9:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Fids", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return protohelpers.ErrInvalidLength
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return protohelpers.ErrInvalidLength
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Fids = append(m.Fids, string(dAtA[iNdEx:postIndex]))
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := protohelpers.Skip(dAtA[iNdEx:])
@@ -14420,6 +14481,42 @@ func (m *UpdateEntryRequest) UnmarshalVT(dAtA []byte) error {
}
m.ExpectedExtended[mapkey] = mapvalue
iNdEx = postIndex
case 6:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Condition", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return protohelpers.ErrInvalidLength
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return protohelpers.ErrInvalidLength
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Condition == nil {
m.Condition = &WriteCondition{}
}
if err := m.Condition.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := protohelpers.Skip(dAtA[iNdEx:])
+29 -2
View File
@@ -594,13 +594,26 @@ func (fs *FilerServer) UpdateEntry(ctx context.Context, req *filer_pb.UpdateEntr
}
fullpath := util.Join(req.Directory, req.Entry.Name)
entry, err := fs.filer.FindEntry(ctx, util.FullPath(fullpath))
// Serialize concurrent mutations to the same path on this filer so the
// read (preconditions, garbage diff) and the write are atomic. Callers
// route a key's writes to this owner filer, making this local lock
// sufficient.
lockPath := util.FullPath(fullpath)
pathLock := fs.entryLockTable.AcquireLock("UpdateEntry", lockPath, util.ExclusiveLock)
defer fs.entryLockTable.ReleaseLock(lockPath, pathLock)
entry, err := fs.filer.FindEntry(ctx, lockPath)
if err != nil {
return &filer_pb.UpdateEntryResponse{}, fmt.Errorf("not found %s: %v", fullpath, err)
}
if err := validateUpdateEntryPreconditions(entry, req.ExpectedExtended); err != nil {
return &filer_pb.UpdateEntryResponse{}, err
}
if conditionIsSet(req.Condition) && !writeConditionSatisfied(req.Condition, entry) {
glog.V(3).InfofCtx(ctx, "UpdateEntry %s: precondition failed: %v", fullpath, req.Condition)
return &filer_pb.UpdateEntryResponse{}, status.Errorf(codes.FailedPrecondition, "precondition failed: %s", fullpath)
}
chunks, garbage, err2 := fs.cleanupChunks(ctx, fullpath, entry, req.Entry)
if err2 != nil {
@@ -706,6 +719,12 @@ func (fs *FilerServer) AppendToEntry(ctx context.Context, req *filer_pb.AppendTo
lock := lockClient.NewShortLivedLock(string(fullpath), string(fs.option.Host))
defer lock.StopShortLivedLock()
// The cluster lock serializes appenders across filers; the path lock makes
// this read-modify-write atomic against conditional updates and deletes on
// the owner filer.
pathLock := fs.entryLockTable.AcquireLock("AppendToEntry", fullpath, util.ExclusiveLock)
defer fs.entryLockTable.ReleaseLock(fullpath, pathLock)
var offset int64 = 0
entry, err := fs.filer.FindEntry(ctx, fullpath)
if err == filer_pb.ErrNotFound {
@@ -749,8 +768,16 @@ func (fs *FilerServer) DeleteEntry(ctx context.Context, req *filer_pb.DeleteEntr
glog.V(4).InfofCtx(ctx, "DeleteEntry %v", req)
// A delete queues the entry's chunks for deletion, so it must not
// interleave with a conditional update's check-then-write on the same
// path: the update would pass its precondition and then resurrect fids
// that are already on the deletion queue.
fullpath := util.JoinPath(req.Directory, req.Name)
pathLock := fs.entryLockTable.AcquireLock("DeleteEntry", fullpath, util.ExclusiveLock)
defer fs.entryLockTable.ReleaseLock(fullpath, pathLock)
ctx, eventSink := filer.WithMetadataEventSink(ctx)
err = fs.filer.DeleteEntryMetaAndData(ctx, util.JoinPath(req.Directory, req.Name), req.IsRecursive, req.IgnoreRecursiveError, req.IsDeleteData, req.IsFromOtherCluster, req.Signatures, req.IfNotModifiedAfter)
err = fs.filer.DeleteEntryMetaAndData(ctx, fullpath, req.IsRecursive, req.IgnoreRecursiveError, req.IsDeleteData, req.IsFromOtherCluster, req.Signatures, req.IfNotModifiedAfter)
resp = &filer_pb.DeleteEntryResponse{}
if err != nil && err != filer_pb.ErrNotFound {
resp.Error = err.Error()
@@ -0,0 +1,187 @@
package weed_server
import (
"context"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func chunkFid(fid string) *filer_pb.FileChunk {
return &filer_pb.FileChunk{FileId: fid, Size: 100}
}
func ifChunksEqual(fids ...string) *filer_pb.WriteCondition {
return one(&filer_pb.WriteCondition_Clause{Kind: filer_pb.WriteCondition_IF_CHUNKS_EQUAL, Fids: fids})
}
func TestChunkFidsEqualClause(t *testing.T) {
withChunks := func(fids ...string) *filer.Entry {
e := &filer.Entry{FullPath: "/test/obj"}
for _, fid := range fids {
e.Chunks = append(e.Chunks, chunkFid(fid))
}
return e
}
cases := []struct {
name string
cur *filer.Entry
fids []string
want bool
}{
{"absent-no-chunks", nil, nil, true},
{"absent-expected", nil, []string{"3,01"}, false},
{"empty-expects-none", withChunks(), nil, true},
{"exact", withChunks("3,01", "3,02"), []string{"3,01", "3,02"}, true},
{"reordered", withChunks("3,01", "3,02"), []string{"3,02", "3,01"}, true},
{"duplicates", withChunks("3,01", "3,01", "3,02"), []string{"3,02", "3,01", "3,01"}, true},
// The strand: a concurrent update emptied the stored chunk list, so the
// stale writer's expectation no longer holds.
{"stored-emptied", withChunks(), []string{"3,01"}, false},
// The reverse strand: the writer read no chunks, but a concurrent update
// has since added one; an empty fids list still guards it.
{"stored-filled", withChunks("3,01"), nil, false},
{"chunk-added", withChunks("3,01", "3,02"), []string{"3,01"}, false},
{"chunk-removed", withChunks("3,01"), []string{"3,01", "3,02"}, false},
{"chunk-replaced", withChunks("3,09"), []string{"3,01"}, false},
{"duplicate-count", withChunks("3,01"), []string{"3,01", "3,01"}, false},
}
for _, tc := range cases {
if got := writeConditionSatisfied(ifChunksEqual(tc.fids...), tc.cur); got != tc.want {
t.Errorf("%s: got %v want %v", tc.name, got, tc.want)
}
}
}
// TestUpdateEntryChunkConditionPreventsStrand drives the UpdateEntry handler
// through the interleaving that strands an entry on a dead needle:
//
// 1. entry has chunks=[F]; needle F is live.
// 2. R (a chunk-preserving read-modify-write) snapshots chunks=[F].
// 3. D (eviction) commits chunks=[] first; F is diffed away and queued for
// deletion. The store is seeded at chunks=[] to model D's commit.
// 4. R commits its stale snapshot last.
//
// Without a condition R's write wins and resurrects the reference to F; with
// IF_CHUNKS_EQUAL it fails with FailedPrecondition and the entry stays empty.
func TestUpdateEntryChunkConditionPreventsStrand(t *testing.T) {
const fidF = "3,01637037d6"
newServer := func(storedChunks ...*filer_pb.FileChunk) (*FilerServer, *renameTestStore) {
store := newRenameTestStore()
store.entries["/test/obj"] = &filer.Entry{
FullPath: "/test/obj",
Attr: filer.Attr{Inode: 1, Mtime: time.Unix(1700000000, 0), Mode: 0644},
Chunks: storedChunks,
}
f := newRenameTestFiler(store)
return &FilerServer{filer: f, option: &FilerOption{}, entryLockTable: util.NewLockTable[util.FullPath]()}, store
}
// R's stale request: it read chunks=[F] and writes back chunks=[F].
staleReq := func(cond *filer_pb.WriteCondition) *filer_pb.UpdateEntryRequest {
return &filer_pb.UpdateEntryRequest{
Directory: "/test",
Entry: &filer_pb.Entry{
Name: "obj",
Attributes: &filer_pb.FuseAttributes{Mtime: 1700000005, FileMode: 0644, Inode: 1},
Chunks: []*filer_pb.FileChunk{chunkFid(fidF)},
},
Condition: cond,
}
}
t.Run("unconditional-resurrects", func(t *testing.T) {
fs, store := newServer()
if _, err := fs.UpdateEntry(context.Background(), staleReq(nil)); err != nil {
t.Fatalf("UpdateEntry: %v", err)
}
if got := len(store.entries["/test/obj"].GetChunks()); got != 1 {
t.Fatalf("last-write-wins should have resurrected chunks=[F], got %d chunks", got)
}
})
t.Run("condition-rejects-stale", func(t *testing.T) {
fs, store := newServer()
_, err := fs.UpdateEntry(context.Background(), staleReq(ifChunksEqual(fidF)))
if status.Code(err) != codes.FailedPrecondition {
t.Fatalf("stale write must fail with FailedPrecondition, got %v", err)
}
if got := len(store.entries["/test/obj"].GetChunks()); got != 0 {
t.Fatalf("entry must stay chunks=[], got %d chunks", got)
}
})
t.Run("condition-passes-fresh", func(t *testing.T) {
fs, store := newServer(chunkFid(fidF))
if _, err := fs.UpdateEntry(context.Background(), staleReq(ifChunksEqual(fidF))); err != nil {
t.Fatalf("fresh read-modify-write must succeed: %v", err)
}
chunks := store.entries["/test/obj"].GetChunks()
if len(chunks) != 1 || chunks[0].GetFileIdString() != fidF {
t.Fatalf("chunks=[F] must be preserved, got %v", chunks)
}
})
// The reverse strand: R read the entry before a concurrent writer added F,
// so its stale write would wipe the chunk and queue F for deletion. An
// IF_CHUNKS_EQUAL clause with no fids guards the emptiness it observed.
t.Run("empty-expectation-rejects", func(t *testing.T) {
fs, store := newServer(chunkFid(fidF))
req := &filer_pb.UpdateEntryRequest{
Directory: "/test",
Entry: &filer_pb.Entry{
Name: "obj",
Attributes: &filer_pb.FuseAttributes{Mtime: 1700000005, FileMode: 0644, Inode: 1},
},
Condition: ifChunksEqual(),
}
_, err := fs.UpdateEntry(context.Background(), req)
if status.Code(err) != codes.FailedPrecondition {
t.Fatalf("stale wipe must fail with FailedPrecondition, got %v", err)
}
if got := len(store.entries["/test/obj"].GetChunks()); got != 1 {
t.Fatalf("chunks=[F] must be preserved, got %d chunks", got)
}
})
}
// DeleteEntry queues chunk deletions, so it must serialize on the same path
// lock the conditional writers hold; otherwise it can interleave with a
// passed precondition and the stale write resurrects the queued fids.
func TestDeleteEntryWaitsForPathLock(t *testing.T) {
store := newRenameTestStore()
store.entries["/test/obj"] = &filer.Entry{
FullPath: "/test/obj",
Attr: filer.Attr{Inode: 1, Mtime: time.Unix(1700000000, 0), Mode: 0644},
}
f := newRenameTestFiler(store)
fs := &FilerServer{filer: f, option: &FilerOption{}, entryLockTable: util.NewLockTable[util.FullPath]()}
lockPath := util.FullPath("/test/obj")
hold := fs.entryLockTable.AcquireLock("test", lockPath, util.ExclusiveLock)
done := make(chan struct{})
go func() {
fs.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{Directory: "/test", Name: "obj"})
close(done)
}()
select {
case <-done:
t.Fatal("DeleteEntry completed while the path lock was held")
case <-time.After(100 * time.Millisecond):
}
fs.entryLockTable.ReleaseLock(lockPath, hold)
<-done
if _, ok := store.entries["/test/obj"]; ok {
t.Fatal("entry not deleted after the lock was released")
}
}
@@ -80,6 +80,8 @@ func clauseSatisfied(c *filer_pb.WriteCondition_Clause, current *filer.Entry) bo
return false
}
return deadline <= time.Now().Unix()
case filer_pb.WriteCondition_IF_CHUNKS_EQUAL:
return chunkFidsEqual(current, c.Fids)
default:
// An unrecognized clause kind (e.g. from a newer client) must not be
// treated as satisfied, which would silently bypass the guard. Fail
@@ -88,6 +90,30 @@ func clauseSatisfied(c *filer_pb.WriteCondition_Clause, current *filer.Entry) bo
}
}
// chunkFidsEqual compares the stored chunk fids (absent entry = none) against
// expected as multisets; chunk order carries no meaning for needle liveness.
func chunkFidsEqual(current *filer.Entry, expected []string) bool {
var chunks []*filer_pb.FileChunk
if current != nil {
chunks = current.GetChunks()
}
if len(chunks) != len(expected) {
return false
}
counts := make(map[string]int, len(expected))
for _, fid := range expected {
counts[fid]++
}
for _, chunk := range chunks {
fid := chunk.GetFileIdString()
if counts[fid] == 0 {
return false
}
counts[fid]--
}
return true
}
// etagInSet reports whether stored matches any candidate. A strong comparison
// (allowWeak false) treats a weak ETag as never equal; a weak comparison
// ignores the W/ marker on both sides.
@@ -10,6 +10,8 @@ import (
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/util"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func entryWithETag(etag string, mtime time.Time) *filer.Entry {
@@ -284,3 +286,46 @@ func TestCreateEntryReusesProvidedExisting(t *testing.T) {
t.Fatalf("providing existing should save one path lookup: existing=%d nil=%d", withExisting, withNil)
}
}
// The UpdateEntry handler enforces the precondition under the per-path lock: a
// matching If-Match applies the update, a non-matching one fails with
// FailedPrecondition and leaves the stored entry untouched.
func TestUpdateEntryConditionEnforced(t *testing.T) {
newServer := func() (*FilerServer, *renameTestStore) {
store := newRenameTestStore()
store.entries["/test/obj"] = &filer.Entry{
FullPath: "/test/obj",
Attr: filer.Attr{Inode: 1, Mtime: time.Unix(1700000000, 0), Mode: 0644},
Extended: map[string][]byte{s3_constants.ExtETagKey: []byte("abc")},
}
f := newRenameTestFiler(store)
return &FilerServer{filer: f, option: &FilerOption{}, entryLockTable: util.NewLockTable[util.FullPath]()}, store
}
req := func(etag string) *filer_pb.UpdateEntryRequest {
return &filer_pb.UpdateEntryRequest{
Directory: "/test",
Entry: &filer_pb.Entry{
Name: "obj",
Attributes: &filer_pb.FuseAttributes{Mtime: 1700000005, FileMode: 0644, Inode: 1},
},
Condition: one(&filer_pb.WriteCondition_Clause{Kind: filer_pb.WriteCondition_IF_ETAG_MATCH, Etags: []string{etag}}),
}
}
fs, store := newServer()
if _, err := fs.UpdateEntry(context.Background(), req(`"zzz"`)); status.Code(err) != codes.FailedPrecondition {
t.Fatalf("mismatched etag: want FailedPrecondition, got %v", err)
}
if got := store.entries["/test/obj"].Attr.Mtime.Unix(); got != 1700000000 {
t.Fatalf("rejected update must not be applied, mtime %d", got)
}
fs, store = newServer()
if _, err := fs.UpdateEntry(context.Background(), req(`"abc"`)); err != nil {
t.Fatalf("matching etag should update: %v", err)
}
if got := store.entries["/test/obj"].Attr.Mtime.Unix(); got != 1700000005 {
t.Fatalf("update not applied, mtime %d", got)
}
}