From 1f332e58664a9835bc6ad29217c236514a8f99ef Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 6 Nov 2026 01:12:00 -0800 Subject: [PATCH] filer.remote.sync: stamp a copy of the event entry updateLocalEntry set the RemoteEntry and dropped the rename marker on the caller's entry before the RPC. A transient stamp failure retries the same event in place, which then read as already synced and skipped both the upload and the stamp. The stub filer now applies a successful update the way the filer does, since tests had relied on that shared object. Co-Authored-By: Claude Opus 5.5 --- weed/command/filer_remote_sync_dir.go | 1 + weed/command/filer_remote_sync_dir_test.go | 29 +++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/weed/command/filer_remote_sync_dir.go b/weed/command/filer_remote_sync_dir.go index 132ddae395..89b3237c6a 100644 --- a/weed/command/filer_remote_sync_dir.go +++ b/weed/command/filer_remote_sync_dir.go @@ -1243,6 +1243,7 @@ func updateLocalEntry(filerClient filer_pb.FilerClient, dir string, entry *filer remoteEntry = proto.Clone(remoteEntry).(*filer_pb.RemoteEntry) remoteEntry.LastLocalSyncTsNs = time.Now().UnixNano() expected := proto.Clone(entry).(*filer_pb.Entry) + entry = proto.Clone(entry).(*filer_pb.Entry) entry.RemoteEntry = remoteEntry delete(entry.Extended, filer.ExtRemoteRenameSourceKey) err := filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { diff --git a/weed/command/filer_remote_sync_dir_test.go b/weed/command/filer_remote_sync_dir_test.go index 4eb9740edf..cb92943f4d 100644 --- a/weed/command/filer_remote_sync_dir_test.go +++ b/weed/command/filer_remote_sync_dir_test.go @@ -456,6 +456,11 @@ func (c *stubFilerClient) UpdateEntry(_ context.Context, req *filer_pb.UpdateEnt if c.updateErr != nil { return &filer_pb.UpdateEntryResponse{}, c.updateErr } + if key := req.Directory + "/" + req.Entry.GetName(); c.entries[key] != nil { + c.entries[key] = req.Entry + } else if c.entry != nil && c.entry.Name == req.Entry.GetName() { + c.entry = req.Entry + } return &filer_pb.UpdateEntryResponse{}, nil } @@ -1549,6 +1554,28 @@ func TestDeleteEventAbsentRemoteObjectIsSuccess(t *testing.T) { }) } +// TestUpdateLocalEntryLeavesTheEventEntryAlone: a transient stamp failure +// retries the same event in place, so the event must still read as unsynced +// and keep its rename source, or the retry skips the upload and the stamp. +func TestUpdateLocalEntryLeavesTheEventEntryAlone(t *testing.T) { + const dir = "/buckets/b/vol" + event := entryWith("state.db", nil, chunk("3,01", "e1")) + event.Extended = map[string][]byte{filer.ExtRemoteRenameSourceKey: []byte("src")} + filerClient := &stubFilerClient{updateErr: status.Error(codes.Unavailable, "filer is shutting down")} + if err := updateLocalEntry(filerClient, dir, event, &filer_pb.RemoteEntry{StorageName: "gcs", RemoteSize: 7}); err == nil { + t.Fatal("err = nil, want the transient failure") + } + if event.RemoteEntry != nil || len(event.Extended[filer.ExtRemoteRenameSourceKey]) == 0 { + t.Errorf("event entry = %v, want it untouched by the failed stamp", event) + } + if !shouldSendToRemote(event) { + t.Error("shouldSendToRemote = false after a failed stamp: the in-place retry would skip the upload") + } + if sent := filerClient.updated.GetEntry(); sent.GetRemoteEntry() == nil || len(sent.GetExtended()[filer.ExtRemoteRenameSourceKey]) > 0 { + t.Errorf("stamp request entry = %v, want the remote entry set and the rename source dropped", sent) + } +} + // TestUpdateLocalEntrySkipsDeletedEntry: the stamp that follows an upload lands // on an entry a later event already deleted. The filer answers "not found"; the // delete's own event follows in the log, so the stamp is skipped rather than @@ -1834,7 +1861,7 @@ func TestCompleteRemoteOnlyRenameClearsThePendingSourceFirst(t *testing.T) { t.Run("a marker still in place keeps the source", func(t *testing.T) { // the stamp did not land: the filer still has the marker - filerClient := &stubFilerClient{entry: pending()} + filerClient := &stubFilerClient{entry: pending(), updateErr: status.Error(codes.FailedPrecondition, "entry changed")} remote := &recordingRemote{objects: objects(), statETag: `"copy"`} if err := completeRemoteOnlyRename(filerClient, remote, "/buckets/b/dst", pending(), renameSource{path: "/buckets/b/src/a.jpg", loc: oldDest}, dest, ""); err == nil { t.Fatal("err = nil, want the event retried")