* Add FUSE integration tests for POSIX file locking Test flock() and fcntl() advisory locks over the FUSE mount: - Exclusive and shared flock with conflict detection - flock upgrade (shared to exclusive) and release on close - fcntl F_SETLK write lock conflicts and shared read locks - fcntl F_GETLK conflict reporting on overlapping byte ranges - Non-overlapping byte-range locks held independently - F_SETLKW blocking until conflicting lock is released - Lock release on file descriptor close - Concurrent lock contention with multiple workers * Fix review feedback in POSIX lock integration tests - Assert specific EAGAIN error on fcntl lock conflicts instead of generic Error - Use O_APPEND in concurrent contention test so workers append rather than overwrite - Verify exact line count (numWorkers * writesPerWorker) after concurrent test - Check unlock error in F_SETLKW blocking test goroutine * Refactor fcntl tests to use subprocesses for inter-process semantics POSIX fcntl locks use the process's files_struct as lock owner, so all fds in the same process share the same owner and never conflict. This caused the fcntl tests to silently pass without exercising lock conflicts. Changes: - Add TestFcntlLockHelper subprocess entry point with hold/try/getlk actions - Add lockHolder with channel-based coordination (no scanner race) - Rewrite all fcntl tests to run contenders in separate subprocesses - Fix F_UNLCK int16 cast in GetLk assertion for type-safe comparison - Fix concurrent test: use non-blocking flock with retry to avoid exhausting go-fuse server reader goroutines (blocking FUSE SETLKW can starve unlock request processing, causing deadlock) flock tests remain same-process since flock uses per-struct-file owners. * Fix misleading comment and error handling in lock test subprocess - Fix comment: tryLockInSubprocess tests a subprocess, not the test process - Distinguish EAGAIN/EACCES from unexpected errors in subprocess try mode so real failures aren't silently masked as lock conflicts * Fix CI race in FcntlReleaseOnClose and increase flock retry budget - FcntlReleaseOnClose: retry lock acquisition after subprocess exits since the FUSE server may not process Release immediately - ConcurrentLockContention: increase retry limit from 500 to 3000 (5s → 30s budget) to handle CI load * separating flock and fcntl in the in-memory lock table and cleaning them up through the right release path: PID for POSIX locks, lock owner for flock * ReleasePosixOwner * weed/mount: flush before releasing posix close owner * weed/mount: keep woken lock waiters from losing inode state * test/fuse: make blocking fcntl helper state explicit * test/fuse: assert flock contention never overlaps * test/fuse: stabilize concurrent lock contention check * test/fuse: make concurrent contention writes deterministic * weed/mount: retry synchronous metadata flushes
88 lines
3.8 KiB
Go
88 lines
3.8 KiB
Go
package mount
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// completeAsyncFlush is called in a background goroutine when a file handle
|
|
// with pending async flush work is released. It performs the deferred data
|
|
// upload and metadata flush that was skipped in doFlush() for writebackCache mode.
|
|
//
|
|
// This enables close() to return immediately for small file workloads (e.g., rsync),
|
|
// while the actual I/O happens concurrently in the background.
|
|
//
|
|
// The caller (submitAsyncFlush) owns asyncFlushWg and the per-inode done channel.
|
|
func (wfs *WFS) completeAsyncFlush(fh *FileHandle) {
|
|
// Phase 1: Flush dirty pages — seals writable chunks, uploads to volume servers, and waits.
|
|
// The underlying UploadWithRetry already retries transient HTTP/gRPC errors internally,
|
|
// so a failure here indicates a persistent issue; the chunk data has been freed.
|
|
if err := fh.dirtyPages.FlushData(); err != nil {
|
|
glog.Errorf("completeAsyncFlush inode %d: data flush failed: %v", fh.inode, err)
|
|
// Data is lost at this point (chunks freed after internal retry exhaustion).
|
|
// Proceed to cleanup to avoid resource leaks and unmount hangs.
|
|
} else if fh.dirtyMetadata {
|
|
// Phase 2: Flush metadata unless the file was explicitly unlinked.
|
|
//
|
|
// isDeleted is set by the Unlink handler when it finds a draining
|
|
// handle. In that case the filer entry is already gone and
|
|
// flushing would recreate it. The uploaded chunks become orphans
|
|
// and are cleaned up by volume.fsck.
|
|
if fh.isDeleted {
|
|
glog.V(3).Infof("completeAsyncFlush inode %d: file was unlinked, skipping metadata flush", fh.inode)
|
|
} else {
|
|
// Resolve the current path for metadata flush.
|
|
//
|
|
// Try GetPath first — it reflects any rename that happened
|
|
// after close(). If the inode mapping is gone (Forget
|
|
// dropped it after the kernel's lookup count hit zero), fall
|
|
// back to the last path saved on the handle. Rename keeps
|
|
// that fallback current, so it is always the newest known path.
|
|
//
|
|
// Forget does NOT mean the file was deleted — it only means
|
|
// the kernel evicted its cache entry.
|
|
dir, name := fh.savedDir, fh.savedName
|
|
fileFullPath := util.FullPath(dir).Child(name)
|
|
|
|
if resolvedPath, status := wfs.inodeToPath.GetPath(fh.inode); status == fuse.OK {
|
|
dir, name = resolvedPath.DirAndName()
|
|
fileFullPath = resolvedPath
|
|
}
|
|
|
|
wfs.flushMetadataWithRetry(fh, dir, name, fileFullPath)
|
|
}
|
|
}
|
|
|
|
glog.V(3).Infof("completeAsyncFlush done inode %d fh %d", fh.inode, fh.fh)
|
|
|
|
// Phase 3: Destroy the upload pipeline and free resources.
|
|
fh.ReleaseHandle()
|
|
}
|
|
|
|
// flushMetadataWithRetry attempts to flush file metadata to the filer, retrying
|
|
// with exponential backoff on transient errors. The chunk data is already on the
|
|
// volume servers at this point; only the filer metadata reference needs persisting.
|
|
func (wfs *WFS) flushMetadataWithRetry(fh *FileHandle, dir, name string, fileFullPath util.FullPath) {
|
|
err := retryMetadataFlush(func() error {
|
|
return wfs.flushMetadataToFiler(fh, dir, name, fh.asyncFlushUid, fh.asyncFlushGid)
|
|
}, func(nextAttempt, totalAttempts int, backoff time.Duration, err error) {
|
|
glog.Warningf("completeAsyncFlush %s: retrying metadata flush (attempt %d/%d) after %v: %v",
|
|
fileFullPath, nextAttempt, totalAttempts, backoff, err)
|
|
})
|
|
if err != nil {
|
|
glog.Errorf("completeAsyncFlush %s: metadata flush failed after %d attempts: %v - "+
|
|
"chunks are uploaded but NOT referenced in filer metadata; "+
|
|
"they will appear as orphans in volume.fsck",
|
|
fileFullPath, metadataFlushRetries+1, err)
|
|
}
|
|
}
|
|
|
|
// WaitForAsyncFlush waits for all pending background flush goroutines to complete.
|
|
// Called before unmount cleanup to ensure no data is lost.
|
|
func (wfs *WFS) WaitForAsyncFlush() {
|
|
wfs.asyncFlushWg.Wait()
|
|
}
|