filer: compare IF_ENTRY_EQUAL chunks by fid, not file_id (#11514)

The stored entry came through FindEntry, which restores chunk file ids
from their fid form, while an expected entry built from a metadata-log
event still carries the serialized form (file_id moved into fid). The
proto.Equal saw file_id "" against the restored id and refused every
stamp, so remote.sync re-uploaded each entry and the RemoteEntry stamp
never landed.

Clone both sides and run BeforeEntrySerialization before comparing, so
chunks match on their fid and the file_id spelling is ignored; the stored
entry and the request's ExpectedEntry are left untouched.

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Chris Lu
2026-09-29 11:34:07 +08:00
committed by GitHub
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 67b19b2db5
commit a901c1a5e2
2 changed files with 33 additions and 4 deletions
+8 -4
View File
@@ -87,10 +87,14 @@ func clauseSatisfied(c *filer_pb.WriteCondition_Clause, current *filer.Entry) bo
if !exists || c.ExpectedEntry == nil {
return !exists && c.ExpectedEntry == nil
}
// Normalize the expected entry the way FindEntry normalizes the stored
// one (e.g. FileSize grows to the chunk extent), or an unchanged entry
// can compare unequal.
return proto.Equal(current.ToProtoEntry(), filer.FromPbEntry("", c.ExpectedEntry).ToProtoEntry())
// Compare both sides in serialized form on clones: chunks are matched
// by their fid only — the stored entry may carry the restored file_id
// while an expected one built from a metadata event does not.
expected := proto.Clone(c.ExpectedEntry).(*filer_pb.Entry)
filer_pb.BeforeEntrySerialization(expected.Chunks)
actual := proto.Clone(current.ToProtoEntry()).(*filer_pb.Entry)
filer_pb.BeforeEntrySerialization(actual.Chunks)
return proto.Equal(actual, filer.FromPbEntry("", expected).ToProtoEntry())
default:
// An unrecognized clause kind (e.g. from a newer client) must not be
// treated as satisfied, which would silently bypass the guard. Fail
@@ -12,6 +12,7 @@ import (
"github.com/seaweedfs/seaweedfs/weed/util"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
func entryWithETag(etag string, mtime time.Time) *filer.Entry {
@@ -221,6 +222,30 @@ func TestIfEntryEqualNormalizesExpected(t *testing.T) {
}
}
// A stamp built from the metadata-log event carries chunks in serialized
// form (file_id moved into fid), while the stored entry came through
// FindEntry which restores file_id. The comparison must still match.
func TestIfEntryEqualSerializedExpected(t *testing.T) {
serialized := &filer_pb.Entry{
Name: "f",
Attributes: &filer_pb.FuseAttributes{Mtime: 42},
Chunks: []*filer_pb.FileChunk{
{Fid: &filer_pb.FileId{VolumeId: 3, FileKey: 1, Cookie: 2}, Size: 100},
},
}
storedProto := proto.Clone(serialized).(*filer_pb.Entry)
filer_pb.AfterEntryDeserialization(storedProto.Chunks)
stored := filer.FromPbEntry("/d", storedProto)
cond := one(&filer_pb.WriteCondition_Clause{
Kind: filer_pb.WriteCondition_IF_ENTRY_EQUAL,
ExpectedEntry: serialized,
})
if !writeConditionSatisfied(cond, stored) {
t.Error("serialized expected entry must equal the deserialized stored entry")
}
}
// storedEntryETag prefers the stored Seaweed ETag attribute and falls back to
// the Md5-derived ETag, matching the S3 gateway.
func TestStoredEntryETag(t *testing.T) {