filer: support nanosecond timestamps for conditional delete (#11339) (#12071)

* 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:
authored and GitHub committed 2026-10-26 07:51:33 -07:00
1 parent b85281f0da
commit a6be3c89c2
25 files changed
+326 -100

No files matched your search

+13 -8
View File
@@ -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