* mount: async flush on close() when writebackCache is enabled When -writebackCache is enabled, defer data upload and metadata flush from Flush() (triggered by close()) to a background goroutine in Release(). This allows processes like rsync that write many small files to proceed to the next file immediately instead of blocking on two network round-trips (volume upload + filer metadata) per file. Fixes #8718 * mount: add retry with backoff for async metadata flush The metadata flush in completeAsyncFlush now retries up to 3 times with exponential backoff (1s, 2s, 4s) on transient gRPC errors. Since the chunk data is already safely on volume servers at this point, only the filer metadata reference needs persisting — retrying is both safe and effective. Data flush (FlushData) is not retried externally because UploadWithRetry already handles transient HTTP/gRPC errors internally; if it still fails, the chunk memory has been freed. * test: add integration tests for writebackCache async flush Add comprehensive FUSE integration tests for the writebackCache async flush feature (issue #8718): - Basic operations: write/read, sequential files, large files, empty files, overwrites - Fsync correctness: fsync forces synchronous flush even in writeback mode, immediate read-after-fsync - Concurrent small files: multi-worker parallel writes (rsync-like workload), multi-directory, rapid create/close - Data integrity: append after close, partial writes, file size correctness, binary data preservation - Performance comparison: writeback vs synchronous flush throughput - Stress test: 16 workers x 100 files with content verification - Mixed concurrent operations: reads, writes, creates running together Also fix pre-existing test infrastructure issues: - Rename framework.go to framework_test.go (fixes Go package conflict) - Fix undefined totalSize variable in concurrent_operations_test.go * ci: update fuse-integration workflow to run full test suite The workflow previously only ran placeholder tests (simple_test.go, working_demo_test.go) in a temp directory due to a Go module conflict. Now that framework.go is renamed to framework_test.go, the full test suite compiles and runs correctly from test/fuse_integration/. Changes: - Run go test directly in test/fuse_integration/ (no temp dir copy) - Install weed binary to /usr/local/bin for test framework discovery - Configure /etc/fuse.conf with user_allow_other for FUSE mounts - Install fuse3 for modern FUSE support - Stream test output to log file for artifact upload * mount: fix three P1 races in async flush P1-1: Reopen overwrites data still flushing in background ReleaseByHandle removes the old handle from fhMap before the deferred flush finishes. A reopen of the same inode during that window would build from stale filer metadata, overwriting the async flush. Fix: Track in-flight async flushes per inode via pendingAsyncFlush map. AcquireHandle now calls waitForPendingAsyncFlush(inode) to block until any pending flush completes before reading filer metadata. P1-2: Deferred flush races rename and unlink after close completeAsyncFlush captured the path once at entry, but rename or unlink after close() could cause metadata to be written under the wrong name or recreate a deleted file. Fix: Re-resolve path from inode via GetPath right before metadata flush. GetPath returns the current path (reflecting renames) or ENOENT (if unlinked), in which case we skip the metadata flush. P1-3: SIGINT/SIGTERM bypasses the async-flush drain grace.OnInterrupt runs hooks then calls os.Exit(0), so WaitForAsyncFlush after server.Serve() never executes on signal. Fix: Add WaitForAsyncFlush (with 10s timeout) to the WFS interrupt handler, before cache cleanup. The timeout prevents hanging on Ctrl-C when the filer is unreachable. * mount: fix P1 races — draining handle stays in fhMap P1-1: Reopen TOCTOU The gap between ReleaseByHandle removing from fhMap and submitAsyncFlush registering in pendingAsyncFlush allowed a concurrent AcquireHandle to slip through with stale metadata. Fix: Hold pendingAsyncFlushMu across both the counter decrement (ReleaseByHandle) and the pending registration. The handle is registered as pending before the lock is released, so waitForPendingAsyncFlush always sees it. P1-2: Rename/unlink can't find draining handle ReleaseByHandle deleted from fhMap immediately. Rename's FindFileHandle(inode) at line 251 could not find the handle to update entry.Name. Unlink could not coordinate either. Fix: When asyncFlushPending is true, ReleaseByHandle/ReleaseByInode leave the handle in fhMap (counter=0 but maps intact). The handle stays visible to FindFileHandle so rename can update entry.Name. completeAsyncFlush re-resolves the path from the inode (GetPath) right before metadata flush for correctness after rename/unlink. After drain, RemoveFileHandle cleans up the maps. Double-return prevention: ReleaseByHandle/ReleaseByInode return nil if counter is already <= 0, so Forget after Release doesn't start a second drain goroutine. P1-3: SIGINT deletes swap files under running goroutines After the 10s timeout, os.RemoveAll deleted the write cache dir (containing swap files) while FlushData goroutines were still reading from them. Fix: Increase timeout to 30s. If timeout expires, skip write cache dir removal so in-flight goroutines can finish reading swap files. The OS (or next mount) cleans them up. Read cache is always removed. * mount: never skip metadata flush when Forget drops inode mapping Forget removes the inode→path mapping when the kernel's lookup count reaches zero, but this does NOT mean the file was unlinked — it only means the kernel evicted its cache entry. completeAsyncFlush was treating GetPath failure as "file unlinked" and skipping the metadata flush, which orphaned the just-uploaded chunks for live files. Fix: Save dir and name at doFlush defer time. In completeAsyncFlush, try GetPath first to pick up renames; if the mapping is gone, fall back to the saved dir/name. Always attempt the metadata flush — the filer is the authority on whether the file exists, not the local inode cache. * mount: distinguish Forget from Unlink in async flush path fallback The saved-path fallback (from the previous fix) always flushed metadata when GetPath failed, which recreated files that were explicitly unlinked after close(). The same stale fallback could recreate the pre-rename path if Forget dropped the inode mapping after a rename. Root cause: GetPath failure has two meanings: 1. Forget — kernel evicted the cache entry (file still exists) 2. Unlink — file was explicitly deleted (should not recreate) Fix (three coordinated changes): Unlink (weedfs_file_mkrm.go): Before RemovePath, look up the inode and find any draining handle via FindFileHandle. Set fh.isDeleted = true so the async flush knows the file was explicitly removed. Rename (weedfs_rename.go): When renaming a file with a draining handle, update asyncFlushDir/asyncFlushName to the post-rename location. This keeps the saved-path fallback current so Forget after rename doesn't flush to the old (pre-rename) path. completeAsyncFlush (weedfs_async_flush.go): Check fh.isDeleted first — if true, skip metadata flush (file was unlinked, chunks become orphans for volume.fsck). Otherwise, try GetPath for the current path (renames); fall back to saved path if Forget dropped the mapping (file is live, just evicted from kernel cache). * test/ci: address PR review nitpicks concurrent_operations_test.go: - Restore precise totalSize assertion instead of info.Size() > 0 writeback_cache_test.go: - Check rand.Read errors in all 3 locations (lines 310, 512, 757) - Check os.MkdirAll error in stress test (line 752) - Remove dead verifyErrors variable (line 332) - Replace both time.Sleep(5s) with polling via waitForFileContent to avoid flaky tests under CI load (lines 638, 700) fuse-integration.yml: - Add set -o pipefail so go test failures propagate through tee * ci: fix fuse3/fuse package conflict on ubuntu-22.04 runner fuse3 is pre-installed on ubuntu-22.04 runners and conflicts with the legacy fuse package. Only install libfuse3-dev for the headers. * mount/page_writer: remove debug println statements Remove leftover debug println("read new data1/2") from ReadDataAt in MemChunk and SwapFileChunk. * test: fix findWeedBinary matching source directory instead of binary findWeedBinary() matched ../../weed (the source directory) via os.Stat before checking PATH, then tried to exec a directory which fails with "permission denied" on the CI runner. Fix: Check PATH first (reliable in CI where the binary is installed to /usr/local/bin). For relative paths, verify the candidate is a regular file (!info.IsDir()). Add ../../weed/weed as a candidate for in-tree builds. * test: fix framework — dynamic ports, output capture, data dirs The integration test framework was failing in CI because: 1. All tests used hardcoded ports (19333/18080/18888), so sequential tests could conflict when prior processes hadn't fully released their ports yet. 2. Data subdirectories (data/master, data/volume) were not created before starting processes. 3. Master was started with -peers=none which is not a valid address. 4. Process stdout/stderr was not captured, making failures opaque ("service not ready within timeout" with no diagnostics). 5. The unmount fallback used 'umount' instead of 'fusermount -u'. 6. The mount used -cacheSizeMB (nonexistent) instead of -cacheCapacityMB and was missing -allowOthers=false for unprivileged CI runners. Fixes: - Dynamic port allocation via freePort() (net.Listen ":0") - Explicit gRPC ports via -port.grpc to avoid default port conflicts - Create data/master and data/volume directories in Setup() - Remove invalid -peers=none and -raftBootstrap flags - Capture process output to logDir/*.log via startProcess() helper - dumpLog() prints tail of log file on service startup failure - Use fusermount3/fusermount -u for unmount - Fix mount flag names (-cacheCapacityMB, -allowOthers=false) * test: remove explicit -port.grpc flags from test framework SeaweedFS convention: gRPC port = HTTP port + 10000. Volume and filer discover the master gRPC port by this convention. Setting explicit -port.grpc on master/volume/filer broke inter-service communication because the volume server computed master gRPC as HTTP+10000 but the actual gRPC was on a different port. Remove all -port.grpc flags and let the default convention work. Dynamic HTTP ports already ensure uniqueness; the derived gRPC ports (HTTP+10000) will also be unique. --------- Co-authored-by: Copilot <copilot@github.com>
SeaweedFS FUSE Integration Testing Framework
Overview
This directory contains a comprehensive integration testing framework for SeaweedFS FUSE operations. The current SeaweedFS FUSE tests are primarily performance-focused (using FIO) but lack comprehensive functional testing. This framework addresses those gaps.
⚠️ Current Status
Note: Due to Go module conflicts between this test framework and the parent SeaweedFS module, the full test suite currently requires manual setup. The framework files are provided as a foundation for comprehensive FUSE testing once the module structure is resolved.
Working Components
- ✅ Framework design and architecture (
framework.go) - ✅ Individual test file structure and compilation
- ✅ Test methodology and comprehensive coverage
- ✅ Documentation and usage examples
- ⚠️ Full test suite execution (requires Go module isolation)
Verified Working Test
cd test/fuse_integration
go test -v simple_test.go
Current Testing Gaps Addressed
1. Limited Functional Coverage
- Current: Only basic FIO performance tests
- New: Comprehensive testing of all FUSE operations (create, read, write, delete, mkdir, rmdir, permissions, etc.)
2. No Concurrency Testing
- Current: Single-threaded performance tests
- New: Extensive concurrent operation tests, race condition detection, thread safety validation
3. Insufficient Error Handling
- Current: Basic error scenarios
- New: Comprehensive error condition testing, edge cases, failure recovery
4. Missing Edge Cases
- Current: Simple file operations
- New: Large files, sparse files, deep directory nesting, many small files, permission variations
Framework Architecture
Core Components
-
framework.go- Test infrastructure and utilitiesFuseTestFramework- Main test management struct- Automated SeaweedFS cluster setup/teardown
- FUSE mount/unmount management
- Helper functions for file operations and assertions
-
basic_operations_test.go- Fundamental FUSE operations- File create, read, write, delete
- File attributes and permissions
- Large file handling
- Sparse file operations
-
directory_operations_test.go- Directory-specific tests- Directory creation, deletion, listing
- Nested directory structures
- Directory permissions and rename operations
- Complex directory scenarios
-
concurrent_operations_test.go- Concurrency and stress testing- Concurrent file and directory operations
- Race condition detection
- High-frequency operations
- Stress testing scenarios
Key Features
Automated Test Environment
framework := NewFuseTestFramework(t, DefaultTestConfig())
defer framework.Cleanup()
require.NoError(t, framework.Setup(DefaultTestConfig()))
- Automatic cluster setup: Master, Volume, Filer servers
- FUSE mounting: Proper mount point management
- Cleanup: Automatic teardown of all resources
Configurable Test Parameters
config := &TestConfig{
Collection: "test",
Replication: "001",
ChunkSizeMB: 8,
CacheSizeMB: 200,
NumVolumes: 5,
EnableDebug: true,
MountOptions: []string{"-allowOthers"},
}
Rich Assertion Helpers
framework.AssertFileExists("path/to/file")
framework.AssertFileContent("file.txt", expectedContent)
framework.AssertFileMode("script.sh", 0755)
framework.CreateTestFile("test.txt", []byte("content"))
Test Categories
1. Basic File Operations
- Create/Read/Write/Delete: Fundamental file operations
- File Attributes: Size, timestamps, permissions
- Append Operations: File appending behavior
- Large Files: Files exceeding chunk size limits
- Sparse Files: Non-contiguous file data
2. Directory Operations
- Directory Lifecycle: Create, list, remove directories
- Nested Structures: Deep directory hierarchies
- Directory Permissions: Access control testing
- Directory Rename: Move operations
- Complex Scenarios: Many files, deep nesting
3. Concurrent Operations
- Multi-threaded Access: Simultaneous file operations
- Race Condition Detection: Concurrent read/write scenarios
- Directory Concurrency: Parallel directory operations
- Stress Testing: High-frequency operations
4. Error Handling & Edge Cases
- Permission Denied: Access control violations
- Disk Full: Storage limit scenarios
- Network Issues: Filer/Volume server failures
- Invalid Operations: Malformed requests
- Recovery Testing: Error recovery scenarios
Usage Examples
Basic Test Run
# Build SeaweedFS binary
make
# Run all FUSE tests
cd test/fuse_integration
go test -v
# Run specific test category
go test -v -run TestBasicFileOperations
go test -v -run TestConcurrentFileOperations
Custom Configuration
func TestCustomFUSE(t *testing.T) {
config := &TestConfig{
ChunkSizeMB: 16, // Larger chunks
CacheSizeMB: 500, // More cache
EnableDebug: true, // Debug output
SkipCleanup: true, // Keep files for inspection
}
framework := NewFuseTestFramework(t, config)
defer framework.Cleanup()
require.NoError(t, framework.Setup(config))
// Your tests here...
}
Debugging Failed Tests
config := &TestConfig{
EnableDebug: true, // Enable verbose logging
SkipCleanup: true, // Keep temp files for inspection
}
Advanced Features
Performance Benchmarking
func BenchmarkLargeFileWrite(b *testing.B) {
framework := NewFuseTestFramework(t, DefaultTestConfig())
defer framework.Cleanup()
require.NoError(t, framework.Setup(DefaultTestConfig()))
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Benchmark file operations
}
}
Custom Test Scenarios
func TestCustomWorkload(t *testing.T) {
framework := NewFuseTestFramework(t, DefaultTestConfig())
defer framework.Cleanup()
require.NoError(t, framework.Setup(DefaultTestConfig()))
// Simulate specific application workload
simulateWebServerWorkload(t, framework)
simulateDatabaseWorkload(t, framework)
simulateBackupWorkload(t, framework)
}
Integration with CI/CD
GitHub Actions Example
name: FUSE Integration Tests
on: [push, pull_request]
jobs:
fuse-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '1.24'
- name: Install FUSE
run: sudo apt-get install -y fuse
- name: Build SeaweedFS
run: make
- name: Run FUSE Tests
run: |
cd test/fuse_integration
go test -v -timeout 30m
Docker Testing
FROM golang:1.24
RUN apt-get update && apt-get install -y fuse
COPY . /seaweedfs
WORKDIR /seaweedfs
RUN make
CMD ["go", "test", "-v", "./test/fuse_integration/..."]
Comparison with Current Testing
| Aspect | Current Tests | New Framework |
|---|---|---|
| Operations Covered | Basic FIO read/write | All FUSE operations |
| Concurrency | Single-threaded | Multi-threaded stress tests |
| Error Scenarios | Limited | Comprehensive error handling |
| File Types | Regular files only | Large, sparse, many small files |
| Directory Testing | None | Complete directory operations |
| Setup Complexity | Manual Docker setup | Automated cluster management |
| Test Isolation | Shared environment | Isolated per-test environments |
| Debugging | Limited | Rich debugging and inspection |
Benefits
1. Comprehensive Coverage
- Tests all FUSE operations supported by SeaweedFS
- Covers edge cases and error conditions
- Validates behavior under concurrent access
2. Reliable Testing
- Isolated test environments prevent test interference
- Automatic cleanup ensures consistent state
- Deterministic test execution
3. Easy Maintenance
- Clear test organization and naming
- Rich helper functions reduce code duplication
- Configurable test parameters for different scenarios
4. Real-world Validation
- Tests actual FUSE filesystem behavior
- Validates integration between all SeaweedFS components
- Catches issues that unit tests might miss
Future Enhancements
1. Extended FUSE Features
- Extended attributes (xattr) testing
- Symbolic link operations
- Hard link behavior
- File locking mechanisms
2. Performance Profiling
- Built-in performance measurement
- Memory usage tracking
- Latency distribution analysis
- Throughput benchmarking
3. Fault Injection
- Network partition simulation
- Server failure scenarios
- Disk full conditions
- Memory pressure testing
4. Integration Testing
- Multi-filer configurations
- Cross-datacenter replication
- S3 API compatibility while mounted
- Backup/restore operations
Getting Started
-
Prerequisites
# Install FUSE sudo apt-get install fuse # Ubuntu/Debian brew install macfuse # macOS # Build SeaweedFS make -
Run Tests
cd test/fuse_integration go test -v -
View Results
- Test output shows detailed operation results
- Failed tests include specific error information
- Debug mode provides verbose logging
This framework represents a significant improvement in SeaweedFS FUSE testing capabilities, providing comprehensive coverage, real-world validation, and reliable automation that will help ensure the robustness and reliability of the FUSE implementation.