Files
seaweedfs/weed/mount/weedfs_file_mkrm.go
T
10f8b2d5b1 mount: guard peer-truncate gaps, stale append EOF, and terminal writeback refusal (#12630)
* mount: guard append gaps over concurrent content merges

* mount: fence a writeback inode when its async publication is abandoned

A drain that gives up on a non-precondition error left the token clear, so a
reopened writeback handle could publish past the stored EOF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: fence a writeback inode when its async data flush fails

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: fence the inode when its last handle drops uncommitted content

A synchronous release whose final flush fails destroys the handle's dirty
pages, while under writeback the kernel EOF already counts them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 01:13:37 -08:00

617 lines
22 KiB
Go

package mount
import (
"context"
"fmt"
"syscall"
"time"
"github.com/seaweedfs/go-fuse/v2/fuse"
"google.golang.org/protobuf/proto"
"github.com/seaweedfs/seaweedfs/weed/cluster"
"github.com/seaweedfs/seaweedfs/weed/cluster/lock_manager"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
)
/**
* Create and open a file
*
* If the file does not exist, first create it with the specified
* mode, and then open it.
*
* If this method is not implemented or under Linux kernel
* versions earlier than 2.6.15, the mknod() and open() methods
* will be called instead.
*/
func (wfs *WFS) Create(cancel <-chan struct{}, in *fuse.CreateIn, name string, out *fuse.CreateOut) (code fuse.Status) {
var s fuse.Status
if name, s = checkName(name); s != fuse.OK {
return s
}
dirFullPath, code := wfs.inodeToPath.GetPath(in.NodeId)
if code != fuse.OK {
return code
}
entryFullPath := dirFullPath.Child(name)
var inode uint64
ctx, cancelFunc := ctxFromCancel(cancel)
defer cancelFunc()
newEntry, _, code := wfs.maybeLoadEntry(ctx, entryFullPath)
if code == fuse.OK {
return wfs.openExistingForCreate(ctx, in, entryFullPath, newEntry, out)
}
if code != fuse.ENOENT {
return code
}
// Acquire the DLM lock before the filer create: a lock failure after an
// eager create would return EAGAIN while the file stays persisted.
var dlmLock *cluster.LiveLock
if wfs.lockClient != nil {
owner := fmt.Sprintf("mount-%d", wfs.signature)
dlmLock = wfs.lockClient.NewBlockingLongLivedLock(
string(entryFullPath), owner, lock_manager.LiveLockTTL,
)
if dlmLock == nil {
return fuse.Status(syscall.EAGAIN)
}
// A deferred create does not ask the filer, so find a file the
// previous lock holder created while this create waited.
if !wfs.option.EagerFilerCreate {
if peerEntry, _, err := wfs.lookupFilerEntry(ctx, entryFullPath); err == nil {
dlmLock.Stop()
return wfs.openExistingForCreate(ctx, in, entryFullPath, peerEntry, out)
}
}
}
inode, newEntry, code = wfs.createRegularFile(ctx, dirFullPath, name, in.Mode, in.Uid, in.Gid, 0, !wfs.option.EagerFilerCreate, true)
if code == fuse.Status(syscall.EEXIST) && in.Flags&syscall.O_EXCL == 0 {
// Race: another process created the file between our check and create.
// Reopen the winner's entry; AcquireHandle takes its own lock.
if dlmLock != nil {
dlmLock.Stop()
}
newEntry, _, code = wfs.maybeLoadEntry(ctx, entryFullPath)
if code != fuse.OK {
return code
}
return wfs.openExistingForCreate(ctx, in, entryFullPath, newEntry, out)
} else if code != fuse.OK {
if dlmLock != nil {
dlmLock.Stop()
}
return code
} else {
inode = wfs.inodeToPath.Lookup(entryFullPath, newEntry.Attributes.Crtime, false, false, inode, true)
}
wfs.outputPbEntry(&out.EntryOut, inode, newEntry)
// For deferred creates, bypass AcquireHandle (which calls maybeReadEntry
// and would fail since the entry is not yet on the filer or in the meta cache).
// We already have the entry from createRegularFile, so create the handle directly.
var fileHandle *FileHandle
var openFh FileHandleId
var existed, draining bool
for {
fileHandle, openFh, existed, draining = wfs.fhMap.AcquireFileHandle(wfs, inode, newEntry, 0, 0, in.Flags&syscall.O_APPEND != 0)
if !draining {
break
}
// A prior incarnation's deferred flush is still draining on this
// inode; wait for it to release the handle before attaching ours.
if !wfs.waitForPendingAsyncFlush(ctx, inode) {
return fuse.EINTR
}
}
if existed {
// A create is authoritative for the entry it just made, so it takes
// effect even on a handle that outlived a previous incarnation.
fileHandle.SetEntry(newEntry)
}
fileHandle.RememberPath(entryFullPath)
// Mark dirty so the deferred filer create happens on Flush, even if the
// file is closed without any writes. An eager create has already
// persisted the entry, so its handle starts clean.
fileHandle.dirtyMetadata = !wfs.option.EagerFilerCreate
if !wfs.option.EagerFilerCreate {
// This is a local placeholder, not an acknowledged filer base.
fileHandle.baseEntry.Store(nil)
} else {
fileHandle.setAuthoritativeBase(proto.Clone(newEntry).(*filer_pb.Entry))
}
// Create bypasses AcquireHandle, so attach the lock acquired above.
// A surviving handle may already hold one; ours is redundant then.
if dlmLock != nil {
if fileHandle.dlmLock == nil {
fileHandle.dlmLock = dlmLock
glog.V(1).Infof("DLM lock acquired for new file %s", entryFullPath)
} else {
dlmLock.Stop()
}
}
out.Fh = uint64(openFh)
out.OpenFlags = 0
return fuse.OK
}
// openExistingForCreate answers a create whose name already exists: an
// exclusive create fails, any other opens the entry, truncating it if asked.
func (wfs *WFS) openExistingForCreate(ctx context.Context, in *fuse.CreateIn, entryFullPath util.FullPath, entry *filer_pb.Entry, out *fuse.CreateOut) fuse.Status {
if entry == nil || entry.Attributes == nil {
return fuse.EIO
}
if in.Flags&syscall.O_EXCL != 0 {
glog.V(0).Infof("Create O_EXCL %s: already exists (uid=%d gid=%d mode=%o)",
entryFullPath, entry.Attributes.Uid, entry.Attributes.Gid, entry.Attributes.FileMode)
return fuse.Status(syscall.EEXIST)
}
inode := wfs.inodeToPath.Lookup(entryFullPath, entry.Attributes.Crtime, false, len(entry.HardLinkId) > 0, entry.Attributes.Inode, true)
fileHandle, openFh, status := wfs.AcquireHandle(ctx, inode, in.Flags, in.Uid, in.Gid)
if status != fuse.OK {
return status
}
if in.Flags&syscall.O_TRUNC != 0 && in.Flags&fuse.O_ANYWRITE != 0 {
if code := wfs.truncateEntry(entryFullPath, entry); code != fuse.OK {
wfs.ReleaseHandle(openFh)
return code
}
entry = fileHandle.GetEntry().GetEntry()
}
wfs.outputPbEntry(&out.EntryOut, inode, entry)
out.Fh = uint64(openFh)
out.OpenFlags = 0
return fuse.OK
}
/** Create a file node
*
* This is called for creation of all non-directory, non-symlink
* nodes. If the filesystem defines a create() method, then for
* regular files that will be called instead.
*/
func (wfs *WFS) Mknod(cancel <-chan struct{}, in *fuse.MknodIn, name string, out *fuse.EntryOut) (code fuse.Status) {
var s fuse.Status
if name, s = checkName(name); s != fuse.OK {
return s
}
dirFullPath, code := wfs.inodeToPath.GetPath(in.NodeId)
if code != fuse.OK {
return
}
ctx, cancelFunc := ctxFromCancel(cancel)
defer cancelFunc()
inode, newEntry, code := wfs.createRegularFile(ctx, dirFullPath, name, in.Mode, in.Uid, in.Gid, in.Rdev, false, false)
if code != fuse.OK {
return code
}
// this is to increase nlookup counter
entryFullPath := dirFullPath.Child(name)
inode = wfs.inodeToPath.Lookup(entryFullPath, newEntry.Attributes.Crtime, false, false, inode, true)
wfs.outputPbEntry(out, inode, newEntry)
return fuse.OK
}
/** Remove a file */
func (wfs *WFS) Unlink(cancel <-chan struct{}, header *fuse.InHeader, name string) (code fuse.Status) {
// Sanitize before it reaches DeleteEntryRequest.Name; see sanitizeFuseName.
name = sanitizeFuseName(name)
dirFullPath, code := wfs.inodeToPath.GetPath(header.NodeId)
if code != fuse.OK {
if code == fuse.ENOENT {
return fuse.OK
}
return code
}
entryFullPath := dirFullPath.Child(name)
ctx, cancelFunc := ctxFromCancel(cancel)
defer cancelFunc()
entry, _, code := wfs.maybeLoadEntry(ctx, entryFullPath)
if code != fuse.OK {
if code == fuse.ENOENT {
return fuse.OK
}
return code
}
if wormEnforced, _ := wfs.wormEnforcedForEntry(entryFullPath, entry); wormEnforced {
return fuse.EPERM
}
// For hard-linked files, serialize all concurrent mutations on the
// same HardLinkId so the filer-side blob decrement and the mount-side
// sibling cache sync are observed atomically by other unlinkers.
// Without this, two concurrent unlinks on different links of the same
// file both read the same pre-decrement counter and both stamp their
// siblings to counter-1, leaving the cache one higher than the blob.
// Re-load the entry under the lock so we see any sibling update a
// prior holder just applied.
if entry != nil && len(entry.HardLinkId) > 0 {
hlKey := string(entry.HardLinkId)
lock := wfs.hardLinkLockTable.AcquireLock("unlink", hlKey, util.ExclusiveLock)
defer wfs.hardLinkLockTable.ReleaseLock(hlKey, lock)
// If another thread unlinked the entry while we waited for the
// lock, the file is already gone — return OK like the initial
// maybeLoadEntry path above. Do not fall back to the stale
// pre-lock snapshot: proceeding with its HardLinkCounter would
// reintroduce the stale-base update the lock is meant to prevent.
fresh, _, freshCode := wfs.maybeLoadEntry(ctx, entryFullPath)
if freshCode == fuse.ENOENT {
return fuse.OK
}
if freshCode != fuse.OK {
return freshCode
}
entry = fresh
}
// POSIX: enforce sticky bit on the parent directory.
if dirEntry, _, dirCode := wfs.maybeLoadEntry(ctx, dirFullPath); dirCode != fuse.OK {
return dirCode
} else if dirEntry != nil && dirEntry.Attributes != nil {
targetUid := uint32(0)
if entry != nil && entry.Attributes != nil {
targetUid = entry.Attributes.Uid
}
if code := checkStickyBit(dirEntry.Attributes.FileMode, dirEntry.Attributes.Uid, targetUid, header.Uid); code != fuse.OK {
return code
}
}
// Before deleting from the filer, mark any draining async-flush handle
// as deleted and wait for it to complete. Without this, the async flush
// can race with the filer delete and recreate the just-unlinked entry
// (the worker checks isDeleted, but it may have already passed that check
// before Unlink sets the flag). By waiting here, any in-flight flush
// finishes first; even if it recreated the entry, the filer delete below
// will remove it again.
isHardLink := entry != nil && entry.Attributes != nil && len(entry.HardLinkId) > 0 && entry.HardLinkCounter > 1
var markedInode, deleteEpoch uint64
if inode, found := wfs.inodeToPath.GetInode(entryFullPath); found {
markedInode = inode
} else if entry != nil && entry.Attributes != nil && entry.Attributes.Inode != 0 {
markedInode = entry.Attributes.Inode
}
if markedInode != 0 {
if isHardLink {
// Another link keeps the content, so a queued close publishes it.
wfs.waitForPendingAsyncFlush(context.Background(), markedInode)
}
deleteEpoch = wfs.markHandleDeleted(markedInode)
wfs.waitForPendingAsyncFlush(context.Background(), markedInode)
}
// first, ensure the filer store can correctly delete
glog.V(3).Infof("remove file: %v", entryFullPath)
// Always let the filer decide whether to delete chunks based on its authoritative data.
// The filer has the correct hard link count and will only delete chunks when appropriate.
deleteReq := &filer_pb.DeleteEntryRequest{
// See weedfs_dir_mkrm.go Mkdir for why Directory is sanitized.
Directory: dirFullPath.Sanitized(),
Name: name,
IsDeleteData: true,
Signatures: []int32{wfs.signature},
}
resp, err := wfs.streamDeleteEntry(context.Background(), deleteReq)
if err != nil {
glog.V(0).Infof("remove %s: %v", entryFullPath, err)
return fuse.OK
}
var event *filer_pb.SubscribeMetadataResponse
if resp != nil && resp.MetadataEvent != nil {
event = resp.MetadataEvent
} else {
event = metadataDeleteEvent(string(dirFullPath), name, false)
}
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
glog.Warningf("unlink %s: best-effort metadata apply failed: %v", entryFullPath, applyErr)
wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
}
wfs.inodeToPath.TouchDirectory(dirFullPath)
wfs.touchDirMtimeCtimeBest(dirFullPath)
// For hard-linked files, the filer's DeleteHardLink decremented the
// shared blob, but the sibling link entries in the local metacache
// still carry the pre-unlink HardLinkCounter, and the kernel has
// cached attrs on the shared inode. Resolve the shared inode before
// removing the path from inodeToPath, then propagate the decrement
// to every sibling cache entry and invalidate the kernel attr cache
// so subsequent lstats on other links see the new nlink — pjdfstest
// link/00.t asserts this after `unlink n0` leaves n1/n2 behind.
var sharedInode uint64
if isHardLink {
sharedInode = entry.Attributes.Inode
if sharedInode == 0 {
if resolved, found := wfs.inodeToPath.GetInode(entryFullPath); found {
sharedInode = resolved
}
}
}
wfs.inodeToPath.RemovePath(entryFullPath, nil)
if isHardLink && sharedInode != 0 {
decremented := proto.Clone(entry).(*filer_pb.Entry)
decremented.HardLinkCounter = entry.HardLinkCounter - 1
now := time.Now()
decremented.Attributes.Ctime = now.Unix()
decremented.Attributes.CtimeNs = int32(now.Nanosecond())
wfs.syncHardLinkSiblings(ctx, sharedInode, decremented, event.GetTsNs(), entryFullPath)
}
// Another link still names the inode, so its open handle keeps flushing
// through that name, over the count and ctime the filer stored there.
if isHardLink && markedInode != 0 {
if survivor, status := wfs.inodeToPath.GetPath(markedInode); status == fuse.OK {
if fh, found := wfs.fhMap.FindFileHandle(markedInode); found {
lock := wfs.fhLockTable.AcquireLock("Unlink", fh.fh, util.ExclusiveLock)
if err := wfs.adoptStoredLink(ctx, fh, survivor, entry.HardLinkId); err != nil {
glog.V(1).Infof("unlink %s: read %s again at the next flush: %v", entryFullPath, survivor, err)
}
wfs.fhLockTable.ReleaseLock(fh.fh, lock)
}
wfs.clearHandleDeleted(markedInode, deleteEpoch)
}
}
return fuse.OK
}
func (wfs *WFS) createRegularFile(ctx context.Context, dirFullPath util.FullPath, name string, mode uint32, uid, gid, rdev uint32, deferFilerCreate bool, skipExistenceCheck bool) (inode uint64, newEntry *filer_pb.Entry, code fuse.Status) {
if wfs.IsOverQuotaWithUncommitted() {
return 0, nil, fuse.Status(syscall.ENOSPC)
}
// Load the parent directory to validate it exists (the create RPC sets
// SkipCheckParentDirectory, so this is the only parent check). With
// default_permissions the kernel already verified write+search on it before
// Create/Mknod, so skip only the mode-bit check and its group lookup.
parentEntry, _, parentStatus := wfs.maybeLoadEntry(ctx, dirFullPath)
if parentStatus != fuse.OK {
return 0, nil, parentStatus
}
if parentEntry == nil || parentEntry.Attributes == nil {
return 0, nil, fuse.EIO
}
if !wfs.option.DefaultPermissions {
// Map parent dir uid/gid from filer-space to local-space so the
// permission check compares like with like (caller uid/gid are local).
parentUid, parentGid := parentEntry.Attributes.Uid, parentEntry.Attributes.Gid
if wfs.option.UidGidMapper != nil {
parentUid, parentGid = wfs.option.UidGidMapper.FilerToLocal(parentUid, parentGid)
}
if !hasAccess(uid, gid, parentUid, parentGid, parentEntry.Attributes.FileMode, fuse.W_OK|fuse.X_OK) {
return 0, nil, fuse.Status(syscall.EACCES)
}
}
entryFullPath := dirFullPath.Child(name)
if !skipExistenceCheck {
if _, _, status := wfs.maybeLoadEntry(ctx, entryFullPath); status == fuse.OK {
return 0, nil, fuse.Status(syscall.EEXIST)
} else if status != fuse.ENOENT {
return 0, nil, status
}
}
fileMode := toOsFileMode(mode)
now := time.Now().Unix()
inode = wfs.inodeToPath.AllocateInode(entryFullPath, now)
newEntry = &filer_pb.Entry{
Name: name,
IsDirectory: false,
Attributes: &filer_pb.FuseAttributes{
Mtime: now,
Crtime: now,
Ctime: now,
FileMode: uint32(fileMode),
Uid: uid,
Gid: gid,
TtlSec: wfs.option.TtlSec,
Rdev: rdev,
Inode: inode,
},
}
if deferFilerCreate || wfs.option.WritebackCache {
// Insert a local placeholder into the metadata cache so that
// maybeLoadEntry() can find the file immediately (e.g., duplicate-
// create checks, stat, readdir).
// We use InsertEntry directly instead of applyLocalMetadataEvent to avoid
// triggering directory hot-threshold eviction that would wipe the entry.
if insertErr := wfs.metaCache.InsertEntry(context.Background(), filer.FromPbEntry(string(dirFullPath), newEntry), 0); insertErr != nil {
glog.Warningf("createFile %s: insert local entry: %v", entryFullPath, insertErr)
}
wfs.inodeToPath.TouchDirectory(dirFullPath)
wfs.touchDirMtimeCtimeBest(dirFullPath)
if deferFilerCreate {
// Fully deferred: the caller (Create) will build a file handle
// directly from newEntry. The actual filer entry is created by
// flushMetadataToFiler on close.
glog.V(3).Infof("createFile %s: deferred to flush", entryFullPath)
} else {
// Async create: Mknod with writeback caching. The node is
// visible locally; fire the filer RPC in the background.
wfs.asyncCreateEntry(dirFullPath, newEntry)
glog.V(3).Infof("createFile %s: async create", entryFullPath)
}
return inode, newEntry, fuse.OK
}
wfs.mapPbIdFromLocalToFiler(newEntry)
defer wfs.mapPbIdFromFilerToLocal(newEntry)
request := &filer_pb.CreateEntryRequest{
Directory: string(dirFullPath),
Entry: newEntry,
Signatures: []int32{wfs.signature},
SkipCheckParentDirectory: true,
}
glog.V(1).Infof("createFile: %s", entryFullPath)
resp, err := wfs.streamCreateEntry(context.Background(), request)
if err != nil {
glog.V(0).Infof("createFile %s: %v", entryFullPath, err)
} else {
event := resp.GetMetadataEvent()
if event == nil {
event = metadataCreateEvent(string(dirFullPath), newEntry)
}
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
glog.Warningf("createFile %s: best-effort metadata apply failed: %v", entryFullPath, applyErr)
wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
}
wfs.inodeToPath.TouchDirectory(dirFullPath)
wfs.touchDirMtimeCtimeBest(dirFullPath)
}
glog.V(3).Infof("createFile %s: %v", entryFullPath, err)
if err != nil {
return 0, nil, grpcErrorToFuseStatus(err)
}
return inode, wfs.ackedBase(resp, newEntry), fuse.OK
}
// markHandleDeleted flags the inode's open handle so its flushes stop writing
// the entry back, and reports which mark this was. Taken and released under
// the handle's flush lock: a flush already holding it finishes before the
// caller's delete runs, and any later flush sees the flag; setting the flag
// bare raced the flush's own check and resurrected the entry right after the
// delete.
func (wfs *WFS) markHandleDeleted(inode uint64) uint64 {
fh, found := wfs.fhMap.FindFileHandle(inode)
if !found {
return 0
}
fhActiveLock := wfs.fhLockTable.AcquireLock("markHandleDeleted", fh.fh, util.ExclusiveLock)
fh.isDeleted = true
fh.deleteEpoch++
epoch := fh.deleteEpoch
wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock)
return epoch
}
// clearHandleDeleted lifts a mark whose delete did not go through. Only the
// mark epoch names: an unlink that raised the flag after it owns the entry's
// fate and keeps it.
func (wfs *WFS) clearHandleDeleted(inode uint64, epoch uint64) {
if epoch == 0 {
return
}
fh, found := wfs.fhMap.FindFileHandle(inode)
if !found {
return
}
fhActiveLock := wfs.fhLockTable.AcquireLock("clearHandleDeleted", fh.fh, util.ExclusiveLock)
if fh.deleteEpoch == epoch {
fh.isDeleted = false
}
wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock)
}
// asyncCreateEntry sends a CreateEntry RPC to the filer in the background.
// The entry is already in the local meta cache; this persists it to the filer.
// Used by Mknod with writeback caching — the node is visible locally right away.
//
// If the filer RPC fails after retries, the local cache entry is removed so the
// phantom file does not persist across cache invalidation or mount restart.
func (wfs *WFS) asyncCreateEntry(dirFullPath util.FullPath, entry *filer_pb.Entry) {
// Clone so the goroutine has its own copy for uid/gid mapping.
requestEntry := proto.Clone(entry).(*filer_pb.Entry)
dir := string(dirFullPath)
entryPath := dirFullPath.Child(entry.Name)
go func() {
wfs.mapPbIdFromLocalToFiler(requestEntry)
request := &filer_pb.CreateEntryRequest{
Directory: dir,
Entry: requestEntry,
Signatures: []int32{wfs.signature},
SkipCheckParentDirectory: true,
}
err := retryMetadataFlush(context.Background(), func() error {
resp, createErr := wfs.streamCreateEntry(context.Background(), request)
if createErr != nil {
return createErr
}
event := resp.GetMetadataEvent()
if event == nil {
event = metadataCreateEvent(dir, requestEntry)
}
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
glog.Warningf("async createFile %s: metadata apply: %v", entryPath, applyErr)
wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
}
return nil
}, func(nextAttempt, totalAttempts int, backoff time.Duration, err error) {
glog.Warningf("async createFile %s: retrying (attempt %d/%d) after %v: %v",
entryPath, nextAttempt, totalAttempts, backoff, err)
})
if err != nil {
glog.Errorf("async createFile %s: failed after retries: %v — removing local entry", entryPath, err)
wfs.metaCache.DeleteEntry(context.Background(), entryPath)
wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
}
}()
}
func (wfs *WFS) truncateEntry(entryFullPath util.FullPath, entry *filer_pb.Entry) fuse.Status {
if entry == nil {
return fuse.EIO
}
if entry.Attributes == nil {
entry.Attributes = &filer_pb.FuseAttributes{}
}
entry.Content = nil
entry.Chunks = nil
entry.Attributes.FileSize = 0
truncNow := time.Now()
entry.Attributes.Mtime = truncNow.Unix()
entry.Attributes.MtimeNs = int32(truncNow.Nanosecond())
entry.Attributes.Ctime = truncNow.Unix()
entry.Attributes.CtimeNs = int32(truncNow.Nanosecond())
if code := wfs.saveEntry(entryFullPath, entry); code != fuse.OK {
return code
}
if inode, found := wfs.inodeToPath.GetInode(entryFullPath); found {
wfs.invalidateOpenMtimeCache(inode)
if fh, fhFound := wfs.fhMap.FindFileHandle(inode); fhFound {
fhActiveLock := fh.wfs.fhLockTable.AcquireLock("truncateEntry", fh.fh, util.ExclusiveLock)
fh.ResetDirtyPages()
fh.SetEntry(entry)
fh.setAuthoritativeBase(proto.Clone(entry).(*filer_pb.Entry))
fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock)
}
}
return fuse.OK
}