mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-27 15:30:45 +01:00
* filer: support nanosecond timestamps for conditional delete (#11339) * shell: fs.verify prune carries the entry mtime in nanoseconds IfNotModifiedAfter now resolves to sub-second precision on the filer, so pass the looked-up mtime_ns along with mtime; the re-lookup check catches same-second rewrites that share the second, and the test mock mirrors the filer's seconds/nanoseconds cutoff. * shell: volume.fsck guards empty-directory deletes with mtime nanoseconds The looked-up mtime_ns joins the seconds in IfNotModifiedAfter so a directory rewritten within the same second is not dropped; the stub filer mirrors the filer's scale heuristic and the race fixtures use a realistic mtime so the nanosecond branch engages. * filer: cover the seconds guard and equality boundary Test second-resolution cutoffs on both sides of the entry's second and the exact-equality nanosecond guard that still deletes. * filer: compare the nanosecond guard as a time, not UnixNano UnixNano overflows for mtimes outside 1678-2262, letting a far-future entry slip past the guard; time.After compares exactly at every epoch. The seconds branch keeps Unix() so a same-second cutoff still deletes. * filer_pb: add explicit if_not_modified_after_ns to DeleteEntryRequest * filer: prefer explicit nanoseconds guard, keep if_not_modified_after as seconds * shell: send seconds and nanoseconds delete cutoffs, skip unexpressible mtimes * filer: merge conditional-delete guards into one nanosecond cutoff DeleteEntryMetaAndData carried both the seconds and nanoseconds guards even though the precedence between them is fixed, so every caller passed a dead parameter. The gRPC handler now folds if_not_modified_after into nanoseconds at the boundary, where (sec+1)*1e9-1 keeps the whole-second cutoff exact, and the filer takes a single ifNotModifiedAfterNs. * filer: hold the conditional-delete cutoff as a time, not int64 nanoseconds A seconds-only guard at or past year 2262 saturated to MaxInt64 nanoseconds (~2262), so a delete whose cutoff matched a far-future mtime was skipped while the request still reported success, and any entry between 2262 and the real cutoff survived pruning. The merged cutoff is now a time.Time, where a whole-second guard maps to the last nanosecond of its second for any int64 second. --------- Co-authored-by: user.email <chrislusf@users.noreply.github.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
This commit is contained in:
25 files changed
+326
-100
No files matched your search
@@ -524,8 +524,9 @@ message DeleteEntryRequest {
|
||||
bool ignore_recursive_error = 6;
|
||||
bool is_from_other_cluster = 7;
|
||||
repeated int32 signatures = 8;
|
||||
int64 if_not_modified_after = 9;
|
||||
int64 if_not_modified_after = 9; // unix time in seconds
|
||||
bool keep_remote_object = 10;
|
||||
int64 if_not_modified_after_ns = 11; // unix time in nanoseconds; overrides if_not_modified_after when set
|
||||
}
|
||||
|
||||
message DeleteEntryResponse {
|
||||
|
||||
@@ -52,7 +52,7 @@ type deletedFolder struct {
|
||||
// FilerOperations defines the filer operations needed by EmptyFolderCleaner
|
||||
type FilerOperations interface {
|
||||
CountDirectoryEntries(ctx context.Context, dirPath util.FullPath, limit int) (count int, err error)
|
||||
DeleteEntryMetaAndData(ctx context.Context, p util.FullPath, isRecursive, ignoreRecursiveError, shouldDeleteChunks, isFromOtherCluster bool, signatures []int32, ifNotModifiedAfter int64) error
|
||||
DeleteEntryMetaAndData(ctx context.Context, p util.FullPath, isRecursive, ignoreRecursiveError, shouldDeleteChunks, isFromOtherCluster bool, signatures []int32, notModifiedAfter time.Time) error
|
||||
GetEntryAttributes(ctx context.Context, p util.FullPath) (attributes map[string][]byte, err error)
|
||||
IsDirectoryKeyObject(ctx context.Context, p util.FullPath) (bool, error)
|
||||
DirectoryAttributes(ctx context.Context, p util.FullPath) (DirectoryAttributes, error)
|
||||
@@ -656,7 +656,7 @@ func (efc *EmptyFolderCleaner) countItems(ctx context.Context, folder string) (i
|
||||
|
||||
// deleteFolder deletes an empty folder
|
||||
func (efc *EmptyFolderCleaner) deleteFolder(ctx context.Context, folder string) error {
|
||||
return efc.filer.DeleteEntryMetaAndData(ctx, util.FullPath(folder), false, false, false, false, nil, 0)
|
||||
return efc.filer.DeleteEntryMetaAndData(ctx, util.FullPath(folder), false, false, false, false, nil, time.Time{})
|
||||
}
|
||||
|
||||
func (efc *EmptyFolderCleaner) getBucketCleanupPolicy(ctx context.Context, folder string) (bucketPath string, autoRemove bool, source string, attrValue string, err error) {
|
||||
|
||||
@@ -42,7 +42,7 @@ func (m *mockFilerOps) CountDirectoryEntries(_ context.Context, dirPath util.Ful
|
||||
return m.countFn(dirPath)
|
||||
}
|
||||
|
||||
func (m *mockFilerOps) DeleteEntryMetaAndData(_ context.Context, p util.FullPath, _, _, _, _ bool, _ []int32, _ int64) error {
|
||||
func (m *mockFilerOps) DeleteEntryMetaAndData(_ context.Context, p util.FullPath, _, _, _, _ bool, _ []int32, _ time.Time) error {
|
||||
if m.deleteFn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func TestDeleteEntryMetaAndDataDeletesCollectionWhenTheRequestIsCancelledMidDele
|
||||
// The client hangs up just as the bucket entry comes out of the store.
|
||||
store.onDeleteEntry = func(util.FullPath) { cancel() }
|
||||
|
||||
if err := f.DeleteEntryMetaAndData(ctx, bucketPath, true, false, true, false, nil, 0); err != nil {
|
||||
if err := f.DeleteEntryMetaAndData(ctx, bucketPath, true, false, true, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("DeleteEntryMetaAndData: %v", err)
|
||||
}
|
||||
if ctx.Err() == nil {
|
||||
@@ -205,7 +205,7 @@ func TestDeleteBucketKeepsSharedCollection(t *testing.T) {
|
||||
seedBucket(t, store, util.FullPath("/buckets/a"))
|
||||
seedBucket(t, store, util.FullPath("/buckets/b"))
|
||||
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/a", true, false, true, false, nil, 0); err != nil {
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/a", true, false, true, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("DeleteEntryMetaAndData: %v", err)
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ func TestDeleteBucketNamedAfterSharedCollection(t *testing.T) {
|
||||
seedBucket(t, store, util.FullPath("/buckets/shared"))
|
||||
seedBucket(t, store, util.FullPath("/buckets/other"))
|
||||
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/shared", true, false, true, false, nil, 0); err != nil {
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/shared", true, false, true, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("DeleteEntryMetaAndData: %v", err)
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ func TestDeleteBucketKeepsCollectionUsedByNonBucketPath(t *testing.T) {
|
||||
})
|
||||
seedBucket(t, store, util.FullPath("/buckets/a"))
|
||||
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/a", true, false, true, false, nil, 0); err != nil {
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/a", true, false, true, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("DeleteEntryMetaAndData: %v", err)
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ func TestDeleteBucketKeepsCollectionFromBroadRule(t *testing.T) {
|
||||
})
|
||||
seedBucket(t, store, util.FullPath("/buckets/a"))
|
||||
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/a", true, false, true, false, nil, 0); err != nil {
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/a", true, false, true, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("DeleteEntryMetaAndData: %v", err)
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ func TestDeleteBucketKeepsDefaultCollection(t *testing.T) {
|
||||
})
|
||||
seedBucket(t, store, util.FullPath("/buckets/a"))
|
||||
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/a", true, false, true, false, nil, 0); err != nil {
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/a", true, false, true, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("DeleteEntryMetaAndData: %v", err)
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ func TestDeleteBucketKeepsCollectionFromNestedSiblingRule(t *testing.T) {
|
||||
seedBucket(t, store, util.FullPath("/buckets/a"))
|
||||
seedBucket(t, store, util.FullPath("/buckets/b"))
|
||||
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/a", true, false, true, false, nil, 0); err != nil {
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/a", true, false, true, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("DeleteEntryMetaAndData: %v", err)
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@ func TestDeleteBucketUnderFilerGroup(t *testing.T) {
|
||||
})
|
||||
seedBucket(t, store, util.FullPath("/buckets/photos"))
|
||||
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/photos", true, false, true, false, nil, 0); err != nil {
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/photos", true, false, true, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("DeleteEntryMetaAndData: %v", err)
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ func TestDeleteBucketDeletesResolvedCollection(t *testing.T) {
|
||||
})
|
||||
seedBucket(t, store, util.FullPath("/buckets/only"))
|
||||
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/only", true, false, true, false, nil, 0); err != nil {
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/only", true, false, true, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("DeleteEntryMetaAndData: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ func deleteEventEntry(ctx context.Context, entry *Entry) *Entry {
|
||||
type OnChunksFunc func([]*filer_pb.FileChunk) error
|
||||
type OnHardLinkIdsFunc func([]HardLinkId) error
|
||||
|
||||
func (f *Filer) DeleteEntryMetaAndData(ctx context.Context, p util.FullPath, isRecursive, ignoreRecursiveError, shouldDeleteChunks, isFromOtherCluster bool, signatures []int32, ifNotModifiedAfter int64) (err error) {
|
||||
func (f *Filer) DeleteEntryMetaAndData(ctx context.Context, p util.FullPath, isRecursive, ignoreRecursiveError, shouldDeleteChunks, isFromOtherCluster bool, signatures []int32, notModifiedAfter time.Time) (err error) {
|
||||
if p == "/" {
|
||||
return nil
|
||||
}
|
||||
@@ -128,7 +128,7 @@ func (f *Filer) DeleteEntryMetaAndData(ctx context.Context, p util.FullPath, isR
|
||||
if findErr != nil {
|
||||
return findErr
|
||||
}
|
||||
if ifNotModifiedAfter > 0 && entry.Attr.Mtime.Unix() > ifNotModifiedAfter {
|
||||
if !notModifiedAfter.IsZero() && entry.Attr.Mtime.After(notModifiedAfter) {
|
||||
return nil
|
||||
}
|
||||
if !isRecursive && entry.IsDirectory() {
|
||||
|
||||
@@ -55,12 +55,12 @@ func TestKeepRemoteObjectMarkerOnlyLivesOnDeleteEvents(t *testing.T) {
|
||||
require.Contains(t, stored.Extended, "Seaweed-Other")
|
||||
|
||||
ordinaryCtx, ordinary := WithMetadataEventSink(ctx)
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(ordinaryCtx, file.FullPath, false, false, false, false, nil, 0))
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(ordinaryCtx, file.FullPath, false, false, false, false, nil, time.Time{}))
|
||||
require.False(t, IsMetadataOnlyDelete(ordinary.Last().EventNotification.OldEntry))
|
||||
|
||||
require.NoError(t, f.CreateEntry(ctx, &Entry{FullPath: file.FullPath, Attr: Attr{Mode: 0644}}, nil, false, false, nil, true, 255))
|
||||
keepCtx, keep := WithMetadataEventSink(WithKeepRemoteObject(ctx))
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(keepCtx, dir.FullPath, true, false, false, false, nil, 0))
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(keepCtx, dir.FullPath, true, false, false, false, nil, time.Time{}))
|
||||
require.Equal(t, "dir", keep.Last().EventNotification.OldEntry.Name)
|
||||
require.True(t, IsMetadataOnlyDelete(keep.Last().EventNotification.OldEntry))
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func TestDeleteFolderFreesLastHardLinkChunks(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, f.CreateEntry(ctx, linkEntry, nil, false, false, nil, true, 255))
|
||||
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(ctx, "/d", true, false, true, false, nil, 0))
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(ctx, "/d", true, false, true, false, nil, time.Time{}))
|
||||
|
||||
_, err := store.KvGet(ctx, hardLinkId)
|
||||
require.ErrorIs(t, err, ErrKvNotFound)
|
||||
@@ -111,9 +111,61 @@ func TestDeleteFolderKeepsSharedHardLinkChunks(t *testing.T) {
|
||||
}, nil, false, false, nil, true, 255))
|
||||
}
|
||||
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(ctx, "/d", true, false, true, false, nil, 0))
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(ctx, "/d", true, false, true, false, nil, time.Time{}))
|
||||
|
||||
_, err := store.KvGet(ctx, hardLinkId)
|
||||
require.NoError(t, err, "surviving link must keep the inode record")
|
||||
require.Equal(t, 0, f.pendingDeletionCount())
|
||||
}
|
||||
|
||||
func TestDeleteEntryMetaAndData_IfNotModifiedAfterSubsecond(t *testing.T) {
|
||||
store := newStubFilerStore()
|
||||
f := newTestFiler(t, store, NewFilerRemoteStorage())
|
||||
ctx := context.Background()
|
||||
|
||||
baseTime := time.Date(2026, 10, 26, 12, 0, 0, 500_000_000, time.UTC) // 12:00:00.500
|
||||
filePath := util.FullPath("/subsecond.txt")
|
||||
|
||||
entry := &Entry{
|
||||
FullPath: filePath,
|
||||
Attr: Attr{Mode: 0644, Mtime: baseTime},
|
||||
}
|
||||
require.NoError(t, f.CreateEntry(ctx, entry, nil, false, false, nil, true, 255))
|
||||
|
||||
// Guard is 100ms before entry Mtime (within the same second)
|
||||
guardNs := baseTime.Add(-100 * time.Millisecond).UnixNano()
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(ctx, filePath, false, false, false, false, nil, time.Unix(0, guardNs)))
|
||||
|
||||
// Entry must NOT be deleted because entry's Mtime is newer than guardNs
|
||||
stored, err := f.FindEntry(ctx, filePath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, stored, "entry modified after guardNs must be preserved")
|
||||
|
||||
// Guard is at or after entry Mtime
|
||||
olderGuardNs := baseTime.Add(100 * time.Millisecond).UnixNano()
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(ctx, filePath, false, false, false, false, nil, time.Unix(0, olderGuardNs)))
|
||||
|
||||
// Entry must be deleted now
|
||||
_, err = f.FindEntry(ctx, filePath)
|
||||
require.ErrorIs(t, err, filer_pb.ErrNotFound)
|
||||
|
||||
// A guard equal to the entry's Mtime still deletes
|
||||
require.NoError(t, f.CreateEntry(ctx, entry, nil, false, false, nil, true, 255))
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(ctx, filePath, false, false, false, false, nil, baseTime))
|
||||
_, err = f.FindEntry(ctx, filePath)
|
||||
require.ErrorIs(t, err, filer_pb.ErrNotFound)
|
||||
|
||||
// An entry mtime beyond the UnixNano range compares as a time, not an int
|
||||
farFuture := &Entry{FullPath: filePath, Attr: Attr{Mode: 0644, Mtime: time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}}
|
||||
require.NoError(t, f.CreateEntry(ctx, farFuture, nil, false, false, nil, true, 255))
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(ctx, filePath, false, false, false, false, nil, baseTime))
|
||||
stored, err = f.FindEntry(ctx, filePath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, stored, "entry past the UnixNano range must be preserved")
|
||||
|
||||
// A whole-second cutoff beyond the UnixNano range still matches: the
|
||||
// unchanged year-3000 entry is at its guard's second, so it is deleted
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(ctx, filePath, false, false, false, false, nil, time.Unix(farFuture.Attr.Mtime.Unix(), 999_999_999)))
|
||||
_, err = f.FindEntry(ctx, filePath)
|
||||
require.ErrorIs(t, err, filer_pb.ErrNotFound)
|
||||
}
|
||||
@@ -551,7 +551,7 @@ func TestDeleteEntryMetaAndData_EmptyFolderUnderMountKeepsRemotePrefix(t *testin
|
||||
dirPath := util.FullPath("/buckets/mybucket/dir")
|
||||
putDirEntry(store, string(dirPath))
|
||||
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(context.Background(), dirPath, false, false, false, false, nil, 0))
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(context.Background(), dirPath, false, false, false, false, nil, time.Time{}))
|
||||
|
||||
assert.Empty(t, stub.removeCalls, "an empty local folder must not remove its remote prefix")
|
||||
assert.Nil(t, store.getEntry(string(dirPath)))
|
||||
@@ -818,7 +818,7 @@ func TestDeleteEntryMetaAndData_GuardedClientRejectsRemoteBackedFile(t *testing.
|
||||
return nil, fmt.Errorf("reject remote endpoint")
|
||||
}
|
||||
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, 0)
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, time.Time{})
|
||||
require.Error(t, err, "guarded rejection should block the remote-backed delete")
|
||||
assert.Len(t, stub.deleteCalls, 0, "guarded rejection should not reach the remote")
|
||||
}
|
||||
@@ -854,7 +854,7 @@ func TestDeleteEntryMetaAndData_GuardedClientAllowsLocalOnlyFile(t *testing.T) {
|
||||
return nil, fmt.Errorf("reject remote endpoint")
|
||||
}
|
||||
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, 0)
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, time.Time{})
|
||||
require.NoError(t, err, "local-only file should delete despite a rejected mount endpoint")
|
||||
_, findErr := store.FindEntry(context.Background(), filePath)
|
||||
require.ErrorIs(t, findErr, filer_pb.ErrNotFound, "local metadata should be deleted")
|
||||
@@ -921,7 +921,7 @@ func TestDeleteEntryMetaAndData_RemoteOnlyFileDeletesRemoteAndMetadata(t *testin
|
||||
}
|
||||
f := newTestFiler(t, store, rs)
|
||||
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, 0)
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, time.Time{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, findErr := store.FindEntry(context.Background(), filePath)
|
||||
@@ -961,7 +961,7 @@ func TestDeleteEntryMetaAndData_IsFromOtherClusterSkipsRemoteDelete(t *testing.T
|
||||
f := newTestFiler(t, store, rs)
|
||||
|
||||
// isFromOtherCluster=true simulates a replicated delete from another filer
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, true, nil, 0)
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, true, nil, time.Time{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Local metadata should be deleted
|
||||
@@ -1018,7 +1018,7 @@ func TestDeleteEntryMetaAndData_UnmountedDirectorySkipsRemoteDelete(t *testing.T
|
||||
_, remoteLoc = f.RemoteStorage.FindMountDirectory(filePath)
|
||||
require.Nil(t, remoteLoc)
|
||||
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, true, false, nil, 0))
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, true, false, nil, time.Time{}))
|
||||
require.Len(t, stub.deleteCalls, 0)
|
||||
require.Len(t, stub.removeCalls, 0)
|
||||
}
|
||||
@@ -1051,7 +1051,7 @@ func TestDeleteEntryMetaAndData_RemoteOnlyFileNotUnderMountSkipsRemoteDelete(t *
|
||||
}
|
||||
f := newTestFiler(t, store, rs)
|
||||
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, 0)
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, time.Time{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, stub.deleteCalls, 0)
|
||||
}
|
||||
@@ -1079,7 +1079,7 @@ func TestDeleteEntryMetaAndData_RemoteMountWithoutClientResolutionKeepsMetadata(
|
||||
}
|
||||
f := newTestFiler(t, store, rs)
|
||||
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, 0)
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, time.Time{})
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "resolve remote storage client")
|
||||
require.ErrorContains(t, err, string(filePath))
|
||||
@@ -1118,7 +1118,7 @@ func TestDeleteEntryMetaAndData_LocalDeleteFailurePreservesMetadata(t *testing.T
|
||||
store.deleteErrByPath[string(filePath)] = errors.New("simulated local delete failure")
|
||||
f := newTestFiler(t, store, rs)
|
||||
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, 0)
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, time.Time{})
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "filer store delete")
|
||||
require.Len(t, stub.deleteCalls, 1)
|
||||
@@ -1157,7 +1157,7 @@ func TestDeleteEntryMetaAndData_RemoteDeleteNotFoundStillDeletesMetadata(t *test
|
||||
}
|
||||
f := newTestFiler(t, store, rs)
|
||||
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, 0)
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, time.Time{})
|
||||
require.NoError(t, err)
|
||||
_, findErr := store.FindEntry(context.Background(), filePath)
|
||||
require.ErrorIs(t, findErr, filer_pb.ErrNotFound)
|
||||
@@ -1191,7 +1191,7 @@ func TestDeleteEntryMetaAndData_RemoteDeleteErrorKeepsMetadata(t *testing.T) {
|
||||
}
|
||||
f := newTestFiler(t, store, rs)
|
||||
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, 0)
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, time.Time{})
|
||||
require.Error(t, err)
|
||||
stored, findErr := store.FindEntry(context.Background(), filePath)
|
||||
require.NoError(t, findErr)
|
||||
@@ -1272,7 +1272,7 @@ func TestDeleteEntryMetaAndData_RecursiveFolderDeleteRemotesChildren(t *testing.
|
||||
}
|
||||
f := newTestFiler(t, store, rs)
|
||||
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), dirPath, true, false, false, false, nil, 0)
|
||||
err := f.DeleteEntryMetaAndData(context.Background(), dirPath, true, false, false, false, nil, time.Time{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Child file should have been deleted from remote
|
||||
|
||||
@@ -236,7 +236,7 @@ func TestDeleteEntryMetaAndData_TombstonesPath(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, 0))
|
||||
require.NoError(t, f.DeleteEntryMetaAndData(context.Background(), filePath, false, false, false, false, nil, time.Time{}))
|
||||
|
||||
// the deleted path must not resurrect through the lazy fetch even while
|
||||
// the remote object is still present
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer/empty_folder_cleanup"
|
||||
@@ -71,7 +72,7 @@ func TestNonRecursiveFolderDeleteKeepsRacingChild(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := testFiler.DeleteEntryMetaAndData(ctx, dir, false, false, false, false, nil, 0); err != nil {
|
||||
if err := testFiler.DeleteEntryMetaAndData(ctx, dir, false, false, false, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("delete empty folder: %v", err)
|
||||
}
|
||||
|
||||
@@ -123,7 +124,7 @@ func TestEnsureDirectoryEntryRestoresRacingParent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := testFiler.DeleteEntryMetaAndData(ctx, dir, false, false, false, false, nil, 0); err != nil {
|
||||
if err := testFiler.DeleteEntryMetaAndData(ctx, dir, false, false, false, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("delete empty folder: %v", err)
|
||||
}
|
||||
|
||||
@@ -170,7 +171,7 @@ func TestEnsureDirectoryEntryRestoresExactAttributes(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("read folder attributes: %v", err)
|
||||
}
|
||||
if err := testFiler.DeleteEntryMetaAndData(ctx, dir, false, false, false, false, nil, 0); err != nil {
|
||||
if err := testFiler.DeleteEntryMetaAndData(ctx, dir, false, false, false, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("delete folder: %v", err)
|
||||
}
|
||||
|
||||
@@ -316,7 +317,7 @@ func TestEnsureDirectoryEntryNarrowsAWiderRestore(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("read folder attributes: %v", err)
|
||||
}
|
||||
if err := testFiler.DeleteEntryMetaAndData(ctx, dir, false, false, false, false, nil, 0); err != nil {
|
||||
if err := testFiler.DeleteEntryMetaAndData(ctx, dir, false, false, false, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("delete folder: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package filer
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
@@ -125,7 +126,7 @@ func TestBucketCollectionKeepsMetaLogTargetCollection(t *testing.T) {
|
||||
})
|
||||
seedBucket(t, store, util.FullPath("/buckets/a"))
|
||||
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/a", true, false, true, false, nil, 0); err != nil {
|
||||
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/a", true, false, true, false, nil, time.Time{}); err != nil {
|
||||
t.Fatalf("DeleteEntryMetaAndData: %v", err)
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -524,8 +524,9 @@ message DeleteEntryRequest {
|
||||
bool ignore_recursive_error = 6;
|
||||
bool is_from_other_cluster = 7;
|
||||
repeated int32 signatures = 8;
|
||||
int64 if_not_modified_after = 9;
|
||||
int64 if_not_modified_after = 9; // unix time in seconds
|
||||
bool keep_remote_object = 10;
|
||||
int64 if_not_modified_after_ns = 11; // unix time in nanoseconds; overrides if_not_modified_after when set
|
||||
}
|
||||
|
||||
message DeleteEntryResponse {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc v7.35.0
|
||||
// protoc v3.21.12
|
||||
// source: filer.proto
|
||||
|
||||
package filer_pb
|
||||
@@ -2780,8 +2780,9 @@ type DeleteEntryRequest struct {
|
||||
IgnoreRecursiveError bool `protobuf:"varint,6,opt,name=ignore_recursive_error,json=ignoreRecursiveError,proto3" json:"ignore_recursive_error,omitempty"`
|
||||
IsFromOtherCluster bool `protobuf:"varint,7,opt,name=is_from_other_cluster,json=isFromOtherCluster,proto3" json:"is_from_other_cluster,omitempty"`
|
||||
Signatures []int32 `protobuf:"varint,8,rep,packed,name=signatures,proto3" json:"signatures,omitempty"`
|
||||
IfNotModifiedAfter int64 `protobuf:"varint,9,opt,name=if_not_modified_after,json=ifNotModifiedAfter,proto3" json:"if_not_modified_after,omitempty"`
|
||||
IfNotModifiedAfter int64 `protobuf:"varint,9,opt,name=if_not_modified_after,json=ifNotModifiedAfter,proto3" json:"if_not_modified_after,omitempty"` // unix time in seconds
|
||||
KeepRemoteObject bool `protobuf:"varint,10,opt,name=keep_remote_object,json=keepRemoteObject,proto3" json:"keep_remote_object,omitempty"`
|
||||
IfNotModifiedAfterNs int64 `protobuf:"varint,11,opt,name=if_not_modified_after_ns,json=ifNotModifiedAfterNs,proto3" json:"if_not_modified_after_ns,omitempty"` // unix time in nanoseconds; overrides if_not_modified_after when set
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -2879,6 +2880,13 @@ func (x *DeleteEntryRequest) GetKeepRemoteObject() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *DeleteEntryRequest) GetIfNotModifiedAfterNs() int64 {
|
||||
if x != nil {
|
||||
return x.IfNotModifiedAfterNs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type DeleteEntryResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
|
||||
@@ -7553,7 +7561,7 @@ const file_filer_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"entry_name\x18\x02 \x01(\tR\tentryName\x12+\n" +
|
||||
"\x06chunks\x18\x03 \x03(\v2\x13.filer_pb.FileChunkR\x06chunks\"\x17\n" +
|
||||
"\x15AppendToEntryResponse\"\xf9\x02\n" +
|
||||
"\x15AppendToEntryResponse\"\xb1\x03\n" +
|
||||
"\x12DeleteEntryRequest\x12\x1c\n" +
|
||||
"\tdirectory\x18\x01 \x01(\tR\tdirectory\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12$\n" +
|
||||
@@ -7566,7 +7574,8 @@ const file_filer_proto_rawDesc = "" +
|
||||
"signatures\x121\n" +
|
||||
"\x15if_not_modified_after\x18\t \x01(\x03R\x12ifNotModifiedAfter\x12,\n" +
|
||||
"\x12keep_remote_object\x18\n" +
|
||||
" \x01(\bR\x10keepRemoteObject\"w\n" +
|
||||
" \x01(\bR\x10keepRemoteObject\x126\n" +
|
||||
"\x18if_not_modified_after_ns\x18\v \x01(\x03R\x14ifNotModifiedAfterNs\"w\n" +
|
||||
"\x13DeleteEntryResponse\x12\x14\n" +
|
||||
"\x05error\x18\x01 \x01(\tR\x05error\x12J\n" +
|
||||
"\x0emetadata_event\x18\x02 \x01(\v2#.filer_pb.SubscribeMetadataResponseR\rmetadataEvent\"\xed\x01\n" +
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.0
|
||||
// - protoc v3.21.12
|
||||
// source: filer.proto
|
||||
|
||||
package filer_pb
|
||||
|
||||
@@ -2491,6 +2491,11 @@ func (m *DeleteEntryRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
|
||||
i -= len(m.unknownFields)
|
||||
copy(dAtA[i:], m.unknownFields)
|
||||
}
|
||||
if m.IfNotModifiedAfterNs != 0 {
|
||||
i = protohelpers.EncodeVarint(dAtA, i, uint64(m.IfNotModifiedAfterNs))
|
||||
i--
|
||||
dAtA[i] = 0x58
|
||||
}
|
||||
if m.KeepRemoteObject {
|
||||
i--
|
||||
if m.KeepRemoteObject {
|
||||
@@ -7566,6 +7571,9 @@ func (m *DeleteEntryRequest) SizeVT() (n int) {
|
||||
if m.KeepRemoteObject {
|
||||
n += 2
|
||||
}
|
||||
if m.IfNotModifiedAfterNs != 0 {
|
||||
n += 1 + protohelpers.SizeOfVarint(uint64(m.IfNotModifiedAfterNs))
|
||||
}
|
||||
n += len(m.unknownFields)
|
||||
return n
|
||||
}
|
||||
@@ -16197,6 +16205,25 @@ func (m *DeleteEntryRequest) UnmarshalVT(dAtA []byte) error {
|
||||
}
|
||||
}
|
||||
m.KeepRemoteObject = bool(v != 0)
|
||||
case 11:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field IfNotModifiedAfterNs", wireType)
|
||||
}
|
||||
m.IfNotModifiedAfterNs = 0
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return protohelpers.ErrIntOverflow
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
m.IfNotModifiedAfterNs |= int64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := protohelpers.Skip(dAtA[iNdEx:])
|
||||
|
||||
@@ -618,7 +618,7 @@ func (fs *FilerServer) applyObjectMutation(ctx context.Context, m *filer_pb.Obje
|
||||
if m.KeepRemoteObject {
|
||||
ctx = filer.WithKeepRemoteObject(ctx)
|
||||
}
|
||||
err := fs.filer.DeleteEntryMetaAndData(ctx, fullpath, m.IsRecursive, false, m.IsDeleteData, fromOtherCluster, signatures, 0)
|
||||
err := fs.filer.DeleteEntryMetaAndData(ctx, fullpath, m.IsRecursive, false, m.IsDeleteData, fromOtherCluster, signatures, time.Time{})
|
||||
if err != nil && err != filer_pb.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
@@ -628,7 +628,7 @@ func (fs *FilerServer) applyObjectMutation(ctx context.Context, m *filer_pb.Obje
|
||||
// transaction. Best-effort: a non-empty or already-removed parent is
|
||||
// the expected no-op, and a failed teardown must not fail the
|
||||
// already-applied delete.
|
||||
parentErr := fs.filer.DeleteEntryMetaAndData(ctx, util.FullPath(m.Directory), false, false, false, fromOtherCluster, signatures, 0)
|
||||
parentErr := fs.filer.DeleteEntryMetaAndData(ctx, util.FullPath(m.Directory), false, false, false, fromOtherCluster, signatures, time.Time{})
|
||||
if parentErr != nil && parentErr != filer_pb.ErrNotFound && !errors.Is(parentErr, filer.ErrNonEmptyFolder) {
|
||||
glog.V(1).InfofCtx(ctx, "remove empty parent %s: %v", m.Directory, parentErr)
|
||||
}
|
||||
@@ -1045,6 +1045,21 @@ func (fs *FilerServer) AppendToEntry(ctx context.Context, req *filer_pb.AppendTo
|
||||
return &filer_pb.AppendToEntryResponse{}, err
|
||||
}
|
||||
|
||||
// deleteCutoff merges the conditional-delete guards into one cutoff:
|
||||
// if_not_modified_after_ns wins when set, and the whole-second
|
||||
// if_not_modified_after maps to the last nanosecond of its second so that
|
||||
// entry.Mtime.Unix() > sec keeps its exact meaning as a time comparison,
|
||||
// even for dates int64 nanoseconds cannot hold.
|
||||
func deleteCutoff(ifNotModifiedAfter, ifNotModifiedAfterNs int64) time.Time {
|
||||
if ifNotModifiedAfterNs > 0 {
|
||||
return time.Unix(0, ifNotModifiedAfterNs)
|
||||
}
|
||||
if ifNotModifiedAfter <= 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Unix(ifNotModifiedAfter, 999_999_999)
|
||||
}
|
||||
|
||||
func (fs *FilerServer) DeleteEntry(ctx context.Context, req *filer_pb.DeleteEntryRequest) (resp *filer_pb.DeleteEntryResponse, err error) {
|
||||
|
||||
glog.V(4).InfofCtx(ctx, "DeleteEntry %v", req)
|
||||
@@ -1061,7 +1076,7 @@ func (fs *FilerServer) DeleteEntry(ctx context.Context, req *filer_pb.DeleteEntr
|
||||
ctx = filer.WithKeepRemoteObject(ctx)
|
||||
}
|
||||
ctx, eventSink := filer.WithMetadataEventSink(ctx)
|
||||
err = fs.filer.DeleteEntryMetaAndData(ctx, fullpath, 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, deleteCutoff(req.IfNotModifiedAfter, req.IfNotModifiedAfterNs))
|
||||
resp = &filer_pb.DeleteEntryResponse{}
|
||||
if err != nil && err != filer_pb.ErrNotFound {
|
||||
resp.Error = err.Error()
|
||||
|
||||
@@ -382,3 +382,38 @@ func TestUpdateEntryConditionEnforced(t *testing.T) {
|
||||
t.Fatalf("update not applied, mtime %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteCutoff(t *testing.T) {
|
||||
base := time.Unix(1700000000, 500_000_000)
|
||||
|
||||
// nanoseconds take precedence when set
|
||||
if got := deleteCutoff(1, base.UnixNano()); !got.Equal(base) {
|
||||
t.Fatalf("ns guard should win, got %v", got)
|
||||
}
|
||||
// no guard stays disabled
|
||||
if got := deleteCutoff(0, 0); !got.IsZero() {
|
||||
t.Fatalf("no guard must stay zero, got %v", got)
|
||||
}
|
||||
// a whole-second guard covers its full second: entry.Mtime.Unix() > sec
|
||||
// is entry.Mtime after the last nanosecond of that second
|
||||
cutoff := deleteCutoff(base.Unix(), 0)
|
||||
if want := time.Unix(0, (base.Unix()+1)*1_000_000_000-1); !cutoff.Equal(want) {
|
||||
t.Fatalf("seconds guard = %v, want %v", cutoff, want)
|
||||
}
|
||||
if base.After(cutoff) {
|
||||
t.Fatal("entry inside the guard second must not be treated as newer")
|
||||
}
|
||||
// a seconds guard beyond the nanosecond range keeps its date: an entry
|
||||
// in the guarded second is deleted and only a newer one is skipped
|
||||
farSec := time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC).Unix()
|
||||
farCutoff := deleteCutoff(farSec, 0)
|
||||
if want := time.Unix(farSec, 999_999_999); !farCutoff.Equal(want) {
|
||||
t.Fatalf("huge seconds guard = %v, want %v", farCutoff, want)
|
||||
}
|
||||
if time.Unix(farSec, 0).After(farCutoff) {
|
||||
t.Fatal("entry in the guarded far-future second must not be treated as newer")
|
||||
}
|
||||
if !time.Unix(farSec+1, 0).After(farCutoff) {
|
||||
t.Fatal("entry past the guarded far-future second must be treated as newer")
|
||||
}
|
||||
}
|
||||
@@ -182,7 +182,7 @@ func TestPruneLeavesTheRemoteAlone(t *testing.T) {
|
||||
assert.False(t, events[0].notification.IsFromOtherCluster, "filer.sync must replicate the prune")
|
||||
assert.Contains(t, events[0].notification.OldEntry.Extended, filer.ExtKeepRemoteObjectKey, "filer.remote.sync must not delete the remote object")
|
||||
|
||||
require.NoError(t, fs.filer.DeleteEntryMetaAndData(context.Background(), deleted.FullPath, false, false, false, false, nil, 0))
|
||||
require.NoError(t, fs.filer.DeleteEntryMetaAndData(context.Background(), deleted.FullPath, false, false, false, false, nil, time.Time{}))
|
||||
assert.Equal(t, []string{"/deleted.bin"}, client.deletedPaths())
|
||||
}
|
||||
|
||||
|
||||
@@ -412,8 +412,7 @@ func (fs *FilerServer) moveSelfEntry(ctx context.Context, stream filer_pb.Seawee
|
||||
false,
|
||||
false,
|
||||
signatures,
|
||||
0,
|
||||
); deleteErr != nil {
|
||||
time.Time{}); deleteErr != nil {
|
||||
return deleteErr
|
||||
}
|
||||
}
|
||||
@@ -532,7 +531,7 @@ func (fs *FilerServer) moveSelfEntry(ctx context.Context, stream filer_pb.Seawee
|
||||
if keepOldRemote {
|
||||
deleteCtx = filer.WithKeepRemoteObject(deleteCtx)
|
||||
}
|
||||
deleteErr := fs.filer.DeleteEntryMetaAndData(deleteCtx, oldPath, false, false, false, false, signatures, 0)
|
||||
deleteErr := fs.filer.DeleteEntryMetaAndData(deleteCtx, oldPath, false, false, false, false, signatures, time.Time{})
|
||||
if deleteErr != nil {
|
||||
return deleteErr
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ func (fs *FilerServer) DeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
defer fs.entryLockTable.ReleaseLock(fullPath, pathLock)
|
||||
}
|
||||
|
||||
err = fs.filer.DeleteEntryMetaAndData(context.Background(), fullPath, isRecursive, ignoreRecursiveError, !skipChunkDeletion, false, nil, 0)
|
||||
err = fs.filer.DeleteEntryMetaAndData(context.Background(), fullPath, isRecursive, ignoreRecursiveError, !skipChunkDeletion, false, nil, time.Time{})
|
||||
if err != nil && err != filer_pb.ErrNotFound {
|
||||
glog.V(1).Infoln("deleting", objectPath, ":", err.Error())
|
||||
writeJsonError(w, r, http.StatusInternalServerError, err)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
@@ -482,7 +483,7 @@ func (fs *FilerServer) tusConcatFinalHandler(w http.ResponseWriter, r *http.Requ
|
||||
// The chunks now belong to the final entry, so remove only the partials'
|
||||
// session metadata.
|
||||
for _, partial := range partials {
|
||||
if err := fs.filer.DeleteEntryMetaAndData(ctx, util.FullPath(fs.tusSessionPath(partial.ID)), true, false, false, false, nil, 0); err != nil {
|
||||
if err := fs.filer.DeleteEntryMetaAndData(ctx, util.FullPath(fs.tusSessionPath(partial.ID)), true, false, false, false, nil, time.Time{}); err != nil {
|
||||
glog.V(1).Infof("Failed to cleanup TUS partial session %s: %v", partial.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func (fs *FilerServer) claimTusPartial(ctx context.Context, partial *TusSession)
|
||||
// completion. A failed rollback wedges the session as consumed: DELETE and
|
||||
// expiry then preserve its chunks, which leak until removed by fsck.
|
||||
func (fs *FilerServer) rollbackTusSessionConsumed(ctx context.Context, uploadID string) {
|
||||
if err := fs.filer.DeleteEntryMetaAndData(ctx, util.FullPath(fs.tusSessionConsumedPath(uploadID)), false, false, false, false, nil, 0); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
|
||||
if err := fs.filer.DeleteEntryMetaAndData(ctx, util.FullPath(fs.tusSessionConsumedPath(uploadID)), false, false, false, false, nil, time.Time{}); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
|
||||
glog.Errorf("TUS session %s wedged as consumed, marker rollback failed: %v", uploadID, err)
|
||||
}
|
||||
}
|
||||
@@ -224,7 +224,7 @@ func (fs *FilerServer) createTusSession(ctx context.Context, uploadID, targetPat
|
||||
// Save session info
|
||||
if err := fs.saveTusSession(ctx, session); err != nil {
|
||||
// Cleanup the directory on failure
|
||||
fs.filer.DeleteEntryMetaAndData(ctx, sessionDirPath, true, true, false, false, nil, 0)
|
||||
fs.filer.DeleteEntryMetaAndData(ctx, sessionDirPath, true, true, false, false, nil, time.Time{})
|
||||
return nil, fmt.Errorf("save session info: %w", err)
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@ func (fs *FilerServer) saveTusChunk(ctx context.Context, uploadID string, chunk
|
||||
// that is about to be deleted. Data outliving a lost record only leaks.
|
||||
func (fs *FilerServer) deleteTusChunk(ctx context.Context, session *TusSession, chunk *TusChunkInfo) {
|
||||
chunkPath := util.FullPath(fs.tusChunkPath(session.ID, chunk.Offset, chunk.Size, chunk.FileId))
|
||||
if err := fs.filer.DeleteEntryMetaAndData(ctx, chunkPath, false, false, false, false, nil, 0); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
|
||||
if err := fs.filer.DeleteEntryMetaAndData(ctx, chunkPath, false, false, false, false, nil, time.Time{}); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
|
||||
glog.Errorf("TUS chunk %s record kept, its data leaks: %v", chunkPath, err)
|
||||
return
|
||||
}
|
||||
@@ -446,7 +446,7 @@ func (fs *FilerServer) deleteTusSession(ctx context.Context, uploadID string) er
|
||||
// its consumed marker before re-verifying the .info, so once the .info is
|
||||
// gone no new claim can pass verification, and a claim that did pass was
|
||||
// created earlier and is visible at the check below.
|
||||
if err := fs.filer.DeleteEntryMetaAndData(ctx, util.FullPath(fs.tusSessionInfoPath(uploadID)), false, false, false, false, nil, 0); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
|
||||
if err := fs.filer.DeleteEntryMetaAndData(ctx, util.FullPath(fs.tusSessionInfoPath(uploadID)), false, false, false, false, nil, time.Time{}); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
|
||||
return fmt.Errorf("delete session info: %w", err)
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ func (fs *FilerServer) deleteTusSession(ctx context.Context, uploadID string) er
|
||||
|
||||
// Delete the session directory
|
||||
sessionDirPath := util.FullPath(fs.tusSessionPath(uploadID))
|
||||
if err := fs.filer.DeleteEntryMetaAndData(ctx, sessionDirPath, true, true, false, false, nil, 0); err != nil {
|
||||
if err := fs.filer.DeleteEntryMetaAndData(ctx, sessionDirPath, true, true, false, false, nil, time.Time{}); err != nil {
|
||||
return fmt.Errorf("delete session directory: %w", err)
|
||||
}
|
||||
|
||||
@@ -614,7 +614,7 @@ func (fs *FilerServer) completeTusUpload(ctx context.Context, session *TusSessio
|
||||
|
||||
// Delete the session (but keep the chunks since they're now part of the final file)
|
||||
sessionDirPath := util.FullPath(fs.tusSessionPath(session.ID))
|
||||
if err := fs.filer.DeleteEntryMetaAndData(ctx, sessionDirPath, true, false, false, false, nil, 0); err != nil {
|
||||
if err := fs.filer.DeleteEntryMetaAndData(ctx, sessionDirPath, true, false, false, false, nil, time.Time{}); err != nil {
|
||||
glog.V(1).Infof("Failed to cleanup TUS session directory %s: %v", session.ID, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -163,8 +163,7 @@ type ItemEntry struct {
|
||||
chunks []*filer_pb.FileChunk
|
||||
rawChunks []*filer_pb.FileChunk
|
||||
path util.FullPath
|
||||
mtimeSec int64
|
||||
md5 []byte
|
||||
attr *filer_pb.FuseAttributes
|
||||
}
|
||||
|
||||
// resolveAndVerify expands chunk manifests and verifies the entry's needles.
|
||||
@@ -220,8 +219,7 @@ func (c *commandFsVerify) verifyProcessMetadata(path string, wg *sync.WaitGroup)
|
||||
if *c.pruneEntries && hasMissingNeedles {
|
||||
pruned, pruneErr := c.pruneEntry(
|
||||
util.NewFullPath(message.NewParentPath, message.NewEntry.Name),
|
||||
message.NewEntry.Attributes.Mtime,
|
||||
message.NewEntry.Attributes.Md5,
|
||||
message.NewEntry.Attributes,
|
||||
message.NewEntry.Chunks)
|
||||
if pruneErr != nil {
|
||||
fmt.Fprintf(c.writer, "prune %s failed: %v\n", entryPath, pruneErr)
|
||||
@@ -371,13 +369,28 @@ func (c *commandFsVerify) verifyEntry(path string, chunks []*filer_pb.FileChunk,
|
||||
return itemIsVerifed.Load(), hasMissingNeedlesFlag.Load()
|
||||
}
|
||||
|
||||
// deleteEntryCutoff splits an entry mtime into the conditional-delete fields:
|
||||
// seconds keep older filers enforcing the guard, nanoseconds keep same-second
|
||||
// precision where int64 can hold them. ok is false when neither field can
|
||||
// express the cutoff and the delete must be skipped rather than sent unguarded.
|
||||
func deleteEntryCutoff(mtimeSec int64, mtimeNsec int32) (sec, ns int64, ok bool) {
|
||||
sec = mtimeSec
|
||||
if sec <= 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
if nsec := int64(mtimeNsec); nsec >= 0 && nsec < 1e9 && sec <= (math.MaxInt64-nsec)/1e9 {
|
||||
ns = sec*1e9 + nsec
|
||||
}
|
||||
return sec, ns, true
|
||||
}
|
||||
|
||||
// pruneEntry deletes an entry whose needles are gone, unless the entry
|
||||
// changed since verification. Three guards protect a concurrent repair:
|
||||
// re-lookup with mtime/md5 comparison, an identity check of the chunk set
|
||||
// (an append or rewrite in the same second as the original mtime changes the
|
||||
// chunks but not the timestamps), and IfNotModifiedAfter on the delete. The
|
||||
// entry is only counted as pruned when a follow-up lookup confirms it is gone.
|
||||
func (c *commandFsVerify) pruneEntry(path util.FullPath, mtimeSec int64, md5 []byte, expectedChunks []*filer_pb.FileChunk) (pruned bool, err error) {
|
||||
func (c *commandFsVerify) pruneEntry(path util.FullPath, attr *filer_pb.FuseAttributes, expectedChunks []*filer_pb.FileChunk) (pruned bool, err error) {
|
||||
dir, name := path.DirAndName()
|
||||
expectedIds := chunkFingerprints(expectedChunks)
|
||||
lookupErr := c.env.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
@@ -392,11 +405,11 @@ func (c *commandFsVerify) pruneEntry(path util.FullPath, mtimeSec int64, md5 []b
|
||||
return err
|
||||
}
|
||||
att := lookupResp.Entry.GetAttributes()
|
||||
if att.GetMtime() != mtimeSec {
|
||||
if att.GetMtime() != attr.GetMtime() || att.GetMtimeNs() != attr.GetMtimeNs() {
|
||||
fmt.Fprintf(c.writer, "skip pruning %s: entry changed since verification\n", path)
|
||||
return nil
|
||||
}
|
||||
if len(md5) > 0 && !bytes.Equal(att.GetMd5(), md5) {
|
||||
if md5 := attr.GetMd5(); len(md5) > 0 && !bytes.Equal(att.GetMd5(), md5) {
|
||||
fmt.Fprintf(c.writer, "skip pruning %s: entry changed since verification\n", path)
|
||||
return nil
|
||||
}
|
||||
@@ -404,10 +417,16 @@ func (c *commandFsVerify) pruneEntry(path util.FullPath, mtimeSec int64, md5 []b
|
||||
fmt.Fprintf(c.writer, "skip pruning %s: entry changed since verification\n", path)
|
||||
return nil
|
||||
}
|
||||
sec, ns, ok := deleteEntryCutoff(attr.GetMtime(), attr.GetMtimeNs())
|
||||
if !ok {
|
||||
fmt.Fprintf(c.writer, "skip pruning %s: mtime cannot set a delete condition\n", path)
|
||||
return nil
|
||||
}
|
||||
deleteResp, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{
|
||||
Directory: dir,
|
||||
Name: name,
|
||||
IfNotModifiedAfter: mtimeSec,
|
||||
Directory: dir,
|
||||
Name: name,
|
||||
IfNotModifiedAfter: sec,
|
||||
IfNotModifiedAfterNs: ns,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -487,8 +506,7 @@ func (c *commandFsVerify) verifyTraverseBfs(path string) (fileCount uint64, errC
|
||||
chunks: dataChunks,
|
||||
rawChunks: entry.Entry.GetChunks(),
|
||||
path: util.NewFullPath(entry.Dir, entry.Entry.Name),
|
||||
mtimeSec: entry.Entry.GetAttributes().GetMtime(),
|
||||
md5: entry.Entry.GetAttributes().GetMd5(),
|
||||
attr: entry.Entry.GetAttributes(),
|
||||
}:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
@@ -508,7 +526,7 @@ func (c *commandFsVerify) verifyTraverseBfs(path string) (fileCount uint64, errC
|
||||
}
|
||||
fileCount++
|
||||
} else if *c.pruneEntries && hasMissingNeedles {
|
||||
pruned, pruneErr := c.pruneEntry(i.path, i.mtimeSec, i.md5, i.rawChunks)
|
||||
pruned, pruneErr := c.pruneEntry(i.path, i.attr, i.rawChunks)
|
||||
if pruneErr != nil {
|
||||
fmt.Fprintf(c.writer, "prune %s failed: %v\n", itemPath, pruneErr)
|
||||
} else if pruned {
|
||||
|
||||
@@ -4,11 +4,13 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
@@ -48,10 +50,18 @@ func (s *fsVerifyTestFilerServer) LookupDirectoryEntry(_ context.Context, req *f
|
||||
|
||||
func (s *fsVerifyTestFilerServer) DeleteEntry(_ context.Context, req *filer_pb.DeleteEntryRequest) (*filer_pb.DeleteEntryResponse, error) {
|
||||
s.deleted = append(s.deleted, req)
|
||||
// mirror the real filer: an entry newer than IfNotModifiedAfter is
|
||||
// mirror the real filer: an entry newer than the cutoff is
|
||||
// silently skipped, no error and no Error field
|
||||
if s.entry != nil && req.IfNotModifiedAfter > 0 && s.entry.Attributes.GetMtime() > req.IfNotModifiedAfter {
|
||||
return &filer_pb.DeleteEntryResponse{}, nil
|
||||
if s.entry != nil {
|
||||
entryNewer := false
|
||||
if req.IfNotModifiedAfterNs > 0 {
|
||||
entryNewer = s.entry.Attributes.GetMtime()*1e9+int64(s.entry.Attributes.GetMtimeNs()) > req.IfNotModifiedAfterNs
|
||||
} else if req.IfNotModifiedAfter > 0 {
|
||||
entryNewer = s.entry.Attributes.GetMtime() > req.IfNotModifiedAfter
|
||||
}
|
||||
if entryNewer {
|
||||
return &filer_pb.DeleteEntryResponse{}, nil
|
||||
}
|
||||
}
|
||||
s.entry = nil
|
||||
return &filer_pb.DeleteEntryResponse{}, nil
|
||||
@@ -234,13 +244,16 @@ func TestFsVerifyVerifyEntryPrunability(t *testing.T) {
|
||||
// Pruning must only delete the entry when it did not change since the
|
||||
// verification pass — a re-upload fixes the file and must survive.
|
||||
func TestFsVerifyPruneEntryGuards(t *testing.T) {
|
||||
mtimeNs := time.Now().Add(-time.Hour).UnixNano()
|
||||
attr := &filer_pb.FuseAttributes{
|
||||
Mtime: mtimeNs / 1e9,
|
||||
MtimeNs: int32(mtimeNs % 1e9),
|
||||
Md5: []byte("md5"),
|
||||
}
|
||||
filer := &fsVerifyTestFilerServer{entry: &filer_pb.Entry{
|
||||
Name: "file",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Mtime: 100,
|
||||
Md5: []byte("md5"),
|
||||
},
|
||||
Chunks: []*filer_pb.FileChunk{{Fid: &filer_pb.FileId{VolumeId: 7, FileKey: 42}}},
|
||||
Name: "file",
|
||||
Attributes: attr,
|
||||
Chunks: []*filer_pb.FileChunk{{Fid: &filer_pb.FileId{VolumeId: 7, FileKey: 42}}},
|
||||
}}
|
||||
commandEnv, _, cleanup := newFsVerifyTestCommandEnv(t, filer, &fsVerifyTestVolumeServer{})
|
||||
defer cleanup()
|
||||
@@ -248,27 +261,43 @@ func TestFsVerifyPruneEntryGuards(t *testing.T) {
|
||||
|
||||
chunks := []*filer_pb.FileChunk{{Fid: &filer_pb.FileId{VolumeId: 7, FileKey: 42}}}
|
||||
|
||||
// unchanged entry: deleted
|
||||
pruned, err := c.pruneEntry(util.NewFullPath("/buckets/b", "file"), 100, []byte("md5"), chunks)
|
||||
// unchanged entry: deleted; the request carries the cutoff in seconds
|
||||
// for older filers and nanoseconds for same-second precision
|
||||
pruned, err := c.pruneEntry(util.NewFullPath("/buckets/b", "file"), attr, chunks)
|
||||
if err != nil || !pruned {
|
||||
t.Fatalf("expected prune of unchanged entry, got pruned=%v err=%v", pruned, err)
|
||||
}
|
||||
if len(filer.deleted) != 1 || filer.deleted[0].IfNotModifiedAfter != mtimeNs/1e9 || filer.deleted[0].IfNotModifiedAfterNs != mtimeNs {
|
||||
t.Fatalf("delete sent wrong cutoffs: %+v", filer.deleted)
|
||||
}
|
||||
|
||||
// re-uploaded entry (new mtime/md5): must survive
|
||||
filer.entry = &filer_pb.Entry{Name: "file", Attributes: &filer_pb.FuseAttributes{Mtime: 200, Md5: []byte("new")}}
|
||||
pruned, err = c.pruneEntry(util.NewFullPath("/buckets/b", "file"), 100, []byte("md5"), chunks)
|
||||
filer.entry = &filer_pb.Entry{Name: "file", Attributes: &filer_pb.FuseAttributes{Mtime: mtimeNs/1e9 + 1, Md5: []byte("new")}}
|
||||
pruned, err = c.pruneEntry(util.NewFullPath("/buckets/b", "file"), attr, chunks)
|
||||
if err != nil || pruned {
|
||||
t.Fatalf("changed entry must not be pruned, got pruned=%v err=%v", pruned, err)
|
||||
}
|
||||
|
||||
// same-second rewrite that only advanced the nanosecond component:
|
||||
// must survive
|
||||
filer.entry = &filer_pb.Entry{
|
||||
Name: "file",
|
||||
Attributes: &filer_pb.FuseAttributes{Mtime: mtimeNs / 1e9, MtimeNs: int32(mtimeNs%1e9) + 1, Md5: []byte("md5")},
|
||||
Chunks: []*filer_pb.FileChunk{{Fid: &filer_pb.FileId{VolumeId: 7, FileKey: 42}}},
|
||||
}
|
||||
pruned, err = c.pruneEntry(util.NewFullPath("/buckets/b", "file"), attr, chunks)
|
||||
if err != nil || pruned {
|
||||
t.Fatalf("sub-second rewrite must not be pruned, got pruned=%v err=%v", pruned, err)
|
||||
}
|
||||
|
||||
// same-second rewrite: timestamps identical but the chunk set differs —
|
||||
// the new upload must survive
|
||||
filer.entry = &filer_pb.Entry{
|
||||
Name: "file",
|
||||
Attributes: &filer_pb.FuseAttributes{Mtime: 100, Md5: []byte("md5")},
|
||||
Attributes: &filer_pb.FuseAttributes{Mtime: mtimeNs / 1e9, MtimeNs: int32(mtimeNs % 1e9), Md5: []byte("md5")},
|
||||
Chunks: []*filer_pb.FileChunk{{Fid: &filer_pb.FileId{VolumeId: 7, FileKey: 999}}},
|
||||
}
|
||||
pruned, err = c.pruneEntry(util.NewFullPath("/buckets/b", "file"), 100, []byte("md5"), chunks)
|
||||
pruned, err = c.pruneEntry(util.NewFullPath("/buckets/b", "file"), attr, chunks)
|
||||
if err != nil || pruned {
|
||||
t.Fatalf("same-second rewrite must not be pruned, got pruned=%v err=%v", pruned, err)
|
||||
}
|
||||
@@ -276,22 +305,48 @@ func TestFsVerifyPruneEntryGuards(t *testing.T) {
|
||||
// identical chunk set (order-insensitive): prunes
|
||||
filer.entry = &filer_pb.Entry{
|
||||
Name: "file",
|
||||
Attributes: &filer_pb.FuseAttributes{Mtime: 100, Md5: []byte("md5")},
|
||||
Attributes: &filer_pb.FuseAttributes{Mtime: mtimeNs / 1e9, MtimeNs: int32(mtimeNs % 1e9), Md5: []byte("md5")},
|
||||
Chunks: []*filer_pb.FileChunk{{Fid: &filer_pb.FileId{VolumeId: 7, FileKey: 42}}},
|
||||
}
|
||||
pruned, err = c.pruneEntry(util.NewFullPath("/buckets/b", "file"), 100, []byte("md5"), chunks)
|
||||
pruned, err = c.pruneEntry(util.NewFullPath("/buckets/b", "file"), attr, chunks)
|
||||
if err != nil || !pruned {
|
||||
t.Fatalf("expected prune when chunk set matches, got pruned=%v err=%v", pruned, err)
|
||||
}
|
||||
|
||||
// entry already gone: no error, no delete
|
||||
filer.entry = nil
|
||||
pruned, err = c.pruneEntry(util.NewFullPath("/buckets/b", "file"), 100, nil, chunks)
|
||||
pruned, err = c.pruneEntry(util.NewFullPath("/buckets/b", "file"), &filer_pb.FuseAttributes{Mtime: mtimeNs / 1e9, MtimeNs: int32(mtimeNs % 1e9)}, chunks)
|
||||
if err != nil || pruned {
|
||||
t.Fatalf("missing entry must be a no-op, got pruned=%v err=%v", pruned, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteEntryCutoff(t *testing.T) {
|
||||
// a regular mtime fills both fields
|
||||
sec, ns, ok := deleteEntryCutoff(1_761_504_000, 500_000_000)
|
||||
if !ok || sec != 1_761_504_000 || ns != 1_761_504_000_500_000_000 {
|
||||
t.Fatalf("regular mtime: got %d %d %v", sec, ns, ok)
|
||||
}
|
||||
// an mtime in the first seconds after the epoch is unambiguous in both
|
||||
// fields once units are explicit
|
||||
sec, ns, ok = deleteEntryCutoff(100, 0)
|
||||
if !ok || sec != 100 || ns != 100_000_000_000 {
|
||||
t.Fatalf("epoch mtime: got %d %d %v", sec, ns, ok)
|
||||
}
|
||||
// a pre-epoch mtime cannot arm either field — the delete must be skipped
|
||||
if _, _, ok = deleteEntryCutoff(-100, 0); ok {
|
||||
t.Fatal("pre-epoch mtime must report ok=false")
|
||||
}
|
||||
if _, _, ok = deleteEntryCutoff(0, 0); ok {
|
||||
t.Fatal("zero mtime must report ok=false")
|
||||
}
|
||||
// an mtime past the UnixNano range degrades to the seconds field
|
||||
sec, ns, ok = deleteEntryCutoff(math.MaxInt64/1_000_000_000+1, 0)
|
||||
if !ok || ns != 0 || sec != math.MaxInt64/1_000_000_000+1 {
|
||||
t.Fatalf("overflow mtime: got %d %d %v", sec, ns, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// An unresolvable chunk manifest is an entry-level verification failure even
|
||||
// when the raw manifest needle is healthy; raw chunks are still checked so a
|
||||
// missing manifest needle can be pruned.
|
||||
|
||||
@@ -863,7 +863,7 @@ func (c *commandVolumeFsck) purgeEmptyDirectories() {
|
||||
if mtime <= 0 || mtime >= time.Now().Add(-directoryQuietPeriod).Unix() {
|
||||
continue
|
||||
}
|
||||
if err := c.deleteEmptyDirectory(dir, mtime); err != nil {
|
||||
if err := c.deleteEmptyDirectory(dir, entry.Attributes); err != nil {
|
||||
if !errors.Is(err, filer.ErrNonEmptyFolder) {
|
||||
fmt.Fprintf(c.writer, "delete empty directory %s: %v\n", dir, err)
|
||||
}
|
||||
@@ -873,16 +873,21 @@ func (c *commandVolumeFsck) purgeEmptyDirectories() {
|
||||
}
|
||||
}
|
||||
|
||||
// deleteEmptyDirectory deletes dir unless it changed since it was looked up at mtime,
|
||||
// so a directory promoted to an S3 object meanwhile survives. The delete is not
|
||||
// recursive, leaving the filer to reject a directory that is not empty.
|
||||
func (c *commandVolumeFsck) deleteEmptyDirectory(dir util.FullPath, mtime int64) error {
|
||||
// deleteEmptyDirectory deletes dir unless it changed since it was looked up,
|
||||
// so a directory promoted to an S3 object meanwhile survives. The delete is
|
||||
// not recursive, leaving the filer to reject a directory that is not empty.
|
||||
func (c *commandVolumeFsck) deleteEmptyDirectory(dir util.FullPath, attr *filer_pb.FuseAttributes) error {
|
||||
parent, name := dir.DirAndName()
|
||||
sec, ns, ok := deleteEntryCutoff(attr.GetMtime(), attr.GetMtimeNs())
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return c.env.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
resp, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{
|
||||
Directory: parent,
|
||||
Name: name,
|
||||
IfNotModifiedAfter: mtime,
|
||||
Directory: parent,
|
||||
Name: name,
|
||||
IfNotModifiedAfter: sec,
|
||||
IfNotModifiedAfterNs: ns,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -107,7 +107,13 @@ func (s *stubFilerServer) DeleteEntry(ctx context.Context, req *filer_pb.DeleteE
|
||||
if !found {
|
||||
return &filer_pb.DeleteEntryResponse{Error: filer_pb.ErrNotFound.Error()}, nil
|
||||
}
|
||||
if req.IfNotModifiedAfter > 0 && entry.Attributes.GetMtime() > req.IfNotModifiedAfter {
|
||||
entryNewer := false
|
||||
if req.IfNotModifiedAfterNs > 0 {
|
||||
entryNewer = entry.Attributes.GetMtime()*1e9+int64(entry.Attributes.GetMtimeNs()) > req.IfNotModifiedAfterNs
|
||||
} else if req.IfNotModifiedAfter > 0 {
|
||||
entryNewer = entry.Attributes.GetMtime() > req.IfNotModifiedAfter
|
||||
}
|
||||
if entryNewer {
|
||||
return &filer_pb.DeleteEntryResponse{}, nil
|
||||
}
|
||||
for path := range s.entries {
|
||||
@@ -209,8 +215,8 @@ func TestVolumeFsckPurgeEmptyDirectoriesKeepsFreshDirectory(t *testing.T) {
|
||||
func TestVolumeFsckPurgeEmptyDirectoriesKeepsChangedDirectory(t *testing.T) {
|
||||
stub := &stubFilerServer{
|
||||
entries: map[string]*filer_pb.Entry{
|
||||
"/race": {IsDirectory: true, Attributes: &filer_pb.FuseAttributes{Mtime: 100}},
|
||||
"/race/dir": {IsDirectory: true, Attributes: &filer_pb.FuseAttributes{Mtime: 100}},
|
||||
"/race": {IsDirectory: true, Attributes: &filer_pb.FuseAttributes{Mtime: time.Now().Add(-time.Hour).Unix()}},
|
||||
"/race/dir": {IsDirectory: true, Attributes: &filer_pb.FuseAttributes{Mtime: time.Now().Add(-time.Hour).Unix()}},
|
||||
},
|
||||
writtenAfterLookup: map[string]bool{"/race/dir": true},
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user