Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions internal/tigerfs/fuse/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,13 @@ func (a *FSAdapter) EntriesToDirEntries(entries []tigerfs.Entry) []gofuse.DirEnt
// which fails in-flight queries. We only lose the per-request
// cancellation channel, which the bug shows we don't actually want.
//
// Write paths intentionally do NOT use this -- partial writes have
// real consistency implications when interrupted, and the request-ctx
// cancellation surface is the right tool there.
// Applies to both read and write paths. Each tigerfs write op runs as a
// single auto-committed statement (or a single transaction wrapping a
// few atomic statements -- e.g., DeleteAndUpdate for POSIX rename-as-
// replace). Either the whole statement/transaction commits or none of
// it commits; there is no partial state to leak when we ignore the
// kernel's transient cancellation. The kernel's FUSE_INTERRUPT means
// "you may stop early if you want," not "abort, the syscall is dying."
func decoupleFromRequestCancel(ctx context.Context) context.Context {
return context.WithoutCancel(ctx)
}
Expand Down Expand Up @@ -305,6 +309,7 @@ func (a *FSAdapter) ReadFile(ctx context.Context, path string) ([]byte, syscall.
//
// Returns errno (0 on success).
func (a *FSAdapter) WriteFile(ctx context.Context, path string, data []byte) syscall.Errno {
ctx = decoupleFromRequestCancel(ctx)
fsErr := a.ops.WriteFile(ctx, path, data)
return a.ErrorToErrno(fsErr)
}
Expand All @@ -319,6 +324,7 @@ func (a *FSAdapter) WriteFile(ctx context.Context, path string, data []byte) sys
//
// Returns errno (0 on success).
func (a *FSAdapter) Delete(ctx context.Context, path string) syscall.Errno {
ctx = decoupleFromRequestCancel(ctx)
fsErr := a.ops.Delete(ctx, path)
return a.ErrorToErrno(fsErr)
}
Expand All @@ -334,6 +340,7 @@ func (a *FSAdapter) Delete(ctx context.Context, path string) syscall.Errno {
//
// Returns errno (0 on success).
func (a *FSAdapter) Mkdir(ctx context.Context, path string) syscall.Errno {
ctx = decoupleFromRequestCancel(ctx)
fsErr := a.ops.Mkdir(ctx, path)
return a.ErrorToErrno(fsErr)
}
Expand All @@ -350,6 +357,7 @@ func (a *FSAdapter) Mkdir(ctx context.Context, path string) syscall.Errno {
//
// Returns errno (0 on success).
func (a *FSAdapter) Rename(ctx context.Context, oldPath, newPath string) syscall.Errno {
ctx = decoupleFromRequestCancel(ctx)
fsErr := a.ops.Rename(ctx, oldPath, newPath)
return a.ErrorToErrno(fsErr)
}
81 changes: 81 additions & 0 deletions internal/tigerfs/fuse/adapter_ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,87 @@ func TestFSAdapter_ReadFile_DecouplesRequestCancel(t *testing.T) {
}
}

// Write-path coverage. Symmetric to the read-path tests: each FSAdapter
// write method must also decouple the request ctx so FUSE_INTERRUPT
// (typically from Go SIGURG goroutine preemption) doesn't surface as
// close-time EIO / rename ENOENT / delete ENOENT on otherwise valid ops.

func TestFSAdapter_WriteFile_DecouplesRequestCancel(t *testing.T) {
capturedCtx := captureCtxViaWriteFile(t, "/public/anything")
if capturedCtx == nil {
t.Fatal("Operations was not reached; FSAdapter short-circuited (regression)")
}
if capturedCtx.Err() != nil {
t.Errorf("FSAdapter forwarded cancelled ctx to DB layer: %v", capturedCtx.Err())
}
}

func TestFSAdapter_Delete_DecouplesRequestCancel(t *testing.T) {
capturedCtx := captureCtxViaDelete(t, "/public/anything")
if capturedCtx == nil {
t.Fatal("Operations was not reached; FSAdapter short-circuited (regression)")
}
if capturedCtx.Err() != nil {
t.Errorf("FSAdapter forwarded cancelled ctx to DB layer: %v", capturedCtx.Err())
}
}

func TestFSAdapter_Mkdir_DecouplesRequestCancel(t *testing.T) {
capturedCtx := captureCtxViaMkdir(t, "/public/anything")
if capturedCtx == nil {
t.Fatal("Operations was not reached; FSAdapter short-circuited (regression)")
}
if capturedCtx.Err() != nil {
t.Errorf("FSAdapter forwarded cancelled ctx to DB layer: %v", capturedCtx.Err())
}
}

func TestFSAdapter_Rename_DecouplesRequestCancel(t *testing.T) {
capturedCtx := captureCtxViaRename(t, "/public/old", "/public/new")
if capturedCtx == nil {
t.Fatal("Operations was not reached; FSAdapter short-circuited (regression)")
}
if capturedCtx.Err() != nil {
t.Errorf("FSAdapter forwarded cancelled ctx to DB layer: %v", capturedCtx.Err())
}
}

func captureCtxViaWriteFile(t *testing.T, path string) context.Context {
t.Helper()
adapter, captured := newAdapterWithCtxCapture()
ctx, cancel := context.WithCancel(context.Background())
cancel()
_ = adapter.WriteFile(ctx, path, []byte("data"))
return *captured
}

func captureCtxViaDelete(t *testing.T, path string) context.Context {
t.Helper()
adapter, captured := newAdapterWithCtxCapture()
ctx, cancel := context.WithCancel(context.Background())
cancel()
_ = adapter.Delete(ctx, path)
return *captured
}

func captureCtxViaMkdir(t *testing.T, path string) context.Context {
t.Helper()
adapter, captured := newAdapterWithCtxCapture()
ctx, cancel := context.WithCancel(context.Background())
cancel()
_ = adapter.Mkdir(ctx, path)
return *captured
}

func captureCtxViaRename(t *testing.T, oldPath, newPath string) context.Context {
t.Helper()
adapter, captured := newAdapterWithCtxCapture()
ctx, cancel := context.WithCancel(context.Background())
cancel()
_ = adapter.Rename(ctx, oldPath, newPath)
return *captured
}

// captureCtxViaReadDir constructs an FSAdapter wired to a MockDBClient
// that records the ctx given to GetCurrentSchema (the earliest DB call
// Operations.ReadDir makes), invokes ReadDir with an already-cancelled
Expand Down
Loading