mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-07 00:26:40 +00:00
ef109fe9e1
* operation: bound AssignVolume with a deadline AssignVolume ran on context.Background(), so when the filer is overwhelmed the RPC could block indefinitely and wedge every caller holding the connection. Give it a 30s deadline so a stuck assign fails and the caller's retry/error path runs instead of hanging forever. * mount: abort flush when the FUSE request is interrupted On close(), a killed process blocks in fuse_flush waiting for the mount to answer. doFlush ran its metadata CreateEntry on context.Background() and ignored the kernel interrupt channel, so against an overwhelmed filer the flush never completed and the process stayed in uninterruptible sleep -- making the pod un-killable. Derive a context from the FUSE cancel channel in Flush/Fsync and thread it through doFlush -> flushMetadataToFiler -> streamCreateEntry; the retry loop stops as soon as the context is cancelled. Release and the pre-rename flush keep a non-cancellable context since they must finish regardless. * operation: harden the AssignVolume timeout test Make the test double's signal send non-blocking and bound the receive with a timeout so a regression can't wedge the test instead of failing it.
128 lines
4.7 KiB
Go
128 lines
4.7 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// Regression: wfs_save.go used to wrap the gRPC error with fmt.Errorf(... %v ...)
|
|
// which stringified the status and made status.FromError fall through to the
|
|
// default EIO mapping. Wrapping with %w must preserve the code so that
|
|
// codes.Canceled from a closing filer connection surfaces as ETIMEDOUT (a
|
|
// retryable hint for FUSE callers) rather than EIO.
|
|
func TestGrpcErrorToFuseStatusUnwrapsCanceledThroughFmtErrorf(t *testing.T) {
|
|
grpcErr := status.Error(codes.Canceled, "grpc: the client connection is closing")
|
|
|
|
wrapped := fmt.Errorf("UpdateEntry dir /some/path: %w", grpcErr)
|
|
|
|
got := grpcErrorToFuseStatus(wrapped)
|
|
want := fuse.Status(syscall.ETIMEDOUT)
|
|
if got != want {
|
|
t.Fatalf("grpcErrorToFuseStatus(canceled wrapped with %%w) = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
// Guard against regressing the wrap verb: %v loses the gRPC status and the
|
|
// classifier must fall through to EIO. This test documents that behavior so
|
|
// anyone reverting the %w change sees the intent.
|
|
func TestGrpcErrorToFuseStatusDropsCanceledThroughPercentV(t *testing.T) {
|
|
grpcErr := status.Error(codes.Canceled, "grpc: the client connection is closing")
|
|
|
|
wrapped := fmt.Errorf("UpdateEntry dir /some/path: %v", grpcErr)
|
|
|
|
got := grpcErrorToFuseStatus(wrapped)
|
|
if got != fuse.EIO {
|
|
t.Fatalf("grpcErrorToFuseStatus(canceled wrapped with %%v) = %v, want EIO (regression guard)", got)
|
|
}
|
|
}
|
|
|
|
func TestIsRetryableFilerError(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
err error
|
|
want bool
|
|
}{
|
|
{"nil", nil, false},
|
|
{"canceled", status.Error(codes.Canceled, "grpc: the client connection is closing"), true},
|
|
{"unavailable", status.Error(codes.Unavailable, "connection refused"), true},
|
|
{"deadline_exceeded", status.Error(codes.DeadlineExceeded, "deadline exceeded"), true},
|
|
{"resource_exhausted", status.Error(codes.ResourceExhausted, "too many concurrent requests"), true},
|
|
{"internal", status.Error(codes.Internal, "server crashed"), true},
|
|
{"not_found", status.Error(codes.NotFound, "entry missing"), false},
|
|
{"already_exists", status.Error(codes.AlreadyExists, "duplicate"), false},
|
|
{"invalid_argument", status.Error(codes.InvalidArgument, "bad request"), false},
|
|
{"permission_denied", status.Error(codes.PermissionDenied, "no access"), false},
|
|
{"unauthenticated", status.Error(codes.Unauthenticated, "missing creds"), false},
|
|
{"failed_precondition", status.Error(codes.FailedPrecondition, "not empty"), false},
|
|
{"plain_error_retries", errors.New("random network glitch"), true},
|
|
{"wrapped_canceled_still_retries", fmt.Errorf("ctx: %w", status.Error(codes.Canceled, "closing")), true},
|
|
{"wrapped_not_found_still_skipped", fmt.Errorf("ctx: %w", status.Error(codes.NotFound, "gone")), false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := isRetryableFilerError(tc.err); got != tc.want {
|
|
t.Fatalf("isRetryableFilerError(%v) = %v, want %v", tc.err, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// retryMetadataFlushIf must short-circuit on non-retryable errors so that
|
|
// synchronous FUSE ops (chmod/utimes/xattr) don't hang for ~7s on ENOENT/
|
|
// EACCES/EINVAL.
|
|
func TestRetryMetadataFlushIfShortCircuitsOnPermanentError(t *testing.T) {
|
|
originalSleep := metadataFlushSleep
|
|
t.Cleanup(func() {
|
|
metadataFlushSleep = originalSleep
|
|
})
|
|
metadataFlushSleep = func(_ context.Context, _ time.Duration) {
|
|
t.Fatal("sleep should not be called when shouldRetry returns false")
|
|
}
|
|
|
|
attempts := 0
|
|
permanent := status.Error(codes.NotFound, "entry missing")
|
|
err := retryMetadataFlushIf(context.Background(), func() error {
|
|
attempts++
|
|
return permanent
|
|
}, isRetryableFilerError, nil)
|
|
|
|
if attempts != 1 {
|
|
t.Fatalf("attempts = %d, want 1 (permanent error should short-circuit)", attempts)
|
|
}
|
|
if !errors.Is(err, permanent) {
|
|
t.Fatalf("err = %v, want permanent sentinel", err)
|
|
}
|
|
}
|
|
|
|
// Transient errors must keep retrying up to the attempt cap even when a
|
|
// predicate is supplied.
|
|
func TestRetryMetadataFlushIfRetriesTransientErrors(t *testing.T) {
|
|
originalSleep := metadataFlushSleep
|
|
t.Cleanup(func() {
|
|
metadataFlushSleep = originalSleep
|
|
})
|
|
metadataFlushSleep = func(_ context.Context, _ time.Duration) {}
|
|
|
|
attempts := 0
|
|
transient := status.Error(codes.Canceled, "grpc: the client connection is closing")
|
|
err := retryMetadataFlushIf(context.Background(), func() error {
|
|
attempts++
|
|
return transient
|
|
}, isRetryableFilerError, nil)
|
|
|
|
if attempts != metadataFlushRetries+1 {
|
|
t.Fatalf("attempts = %d, want %d", attempts, metadataFlushRetries+1)
|
|
}
|
|
if !errors.Is(err, transient) {
|
|
t.Fatalf("err = %v, want transient sentinel", err)
|
|
}
|
|
}
|