mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-21 05:52:35 +00:00
Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fafd8493fb | ||
|
|
b270f1698d | ||
|
|
105257e9b5 | ||
|
|
4bd8e5dd59 | ||
|
|
aa12b51cbf | ||
|
|
e5cf2d2a19 | ||
|
|
6cf34f2376 | ||
|
|
ccc662b90b | ||
|
|
67a551fd62 | ||
|
|
94bfa2b340 | ||
|
|
29bdbb3c48 | ||
|
|
79f4a4579f | ||
|
|
e47054a7e7 | ||
|
|
28fe92065a | ||
|
|
0b3867dca3 | ||
|
|
152884eff2 | ||
|
|
2877febd73 | ||
|
|
7f3f61ea28 | ||
|
|
6c35a3724a | ||
|
|
cca1555cc7 | ||
|
|
805625d06e | ||
|
|
9cc26d09e8 | ||
|
|
3d872e86f8 | ||
|
|
2844c70ecf | ||
|
|
e5f72077ee | ||
|
|
c31e6b4684 | ||
|
|
6bf654c25c | ||
|
|
d5ee35c8df | ||
|
|
b3b7033fe1 | ||
|
|
ecadeddcbe | ||
|
|
156e1a6e64 | ||
|
|
2b6271469b | ||
|
|
963ec4c6e6 | ||
|
|
fb442a57d7 | ||
|
|
a080fbb495 | ||
|
|
d6479b6d4e | ||
|
|
17800f63a7 |
@@ -38,7 +38,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v2
|
||||
|
||||
- name: Set BUILD_TIME env
|
||||
run: echo BUILD_TIME=$(date -u +%Y%m%d-%H%M) >> ${GITHUB_ENV}
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v2
|
||||
|
||||
- name: Set BUILD_TIME env
|
||||
run: echo BUILD_TIME=$(date -u +%Y%m%d-%H%M) >> ${GITHUB_ENV}
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v2
|
||||
- name: Go Release Binaries Normal Volume Size
|
||||
uses: wangyoucao577/go-release-action@279495102627de7960cbc33434ab01a12bae144b # v1.22
|
||||
with:
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v2
|
||||
- name: Go Release Binaries Normal Volume Size
|
||||
uses: wangyoucao577/go-release-action@279495102627de7960cbc33434ab01a12bae144b # v1.22
|
||||
with:
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v2
|
||||
- name: Go Release Binaries Normal Volume Size
|
||||
uses: wangyoucao577/go-release-action@279495102627de7960cbc33434ab01a12bae144b # v1.22
|
||||
with:
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v2
|
||||
- name: Go Release Binaries Normal Volume Size
|
||||
uses: wangyoucao577/go-release-action@279495102627de7960cbc33434ab01a12bae144b # v1.22
|
||||
with:
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v2
|
||||
- name: Go Release Binaries Normal Volume Size
|
||||
uses: wangyoucao577/go-release-action@279495102627de7960cbc33434ab01a12bae144b # v1.22
|
||||
with:
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v2
|
||||
- name: Go Release Binaries Normal Volume Size
|
||||
uses: wangyoucao577/go-release-action@279495102627de7960cbc33434ab01a12bae144b # v1.22
|
||||
with:
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v2
|
||||
-
|
||||
name: Docker meta
|
||||
id: docker_meta
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v2
|
||||
|
||||
- name: Prepare Docker tag
|
||||
id: tag
|
||||
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48
|
||||
|
||||
@@ -77,4 +77,5 @@ jobs:
|
||||
name: fuse-integration-test-results
|
||||
path: |
|
||||
/tmp/fuse-test-output.log
|
||||
/tmp/seaweedfs-fuse-logs/
|
||||
retention-days: 7
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
name: "KMS Tests"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'weed/kms/**'
|
||||
- 'weed/s3api/s3_sse_*.go'
|
||||
- 'weed/s3api/s3api_object_handlers.go'
|
||||
- 'weed/s3api/s3api_object_handlers_put.go'
|
||||
- 'test/kms/**'
|
||||
- '.github/workflows/kms-tests.yml'
|
||||
push:
|
||||
branches: [ master, main ]
|
||||
paths:
|
||||
- 'weed/kms/**'
|
||||
- 'weed/s3api/s3_sse_*.go'
|
||||
- 'weed/s3api/s3api_object_handlers.go'
|
||||
- 'weed/s3api/s3api_object_handlers_put.go'
|
||||
- 'test/kms/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.head_ref || github.ref }}-kms-tests
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: weed
|
||||
|
||||
jobs:
|
||||
kms-provider-tests:
|
||||
name: KMS Provider Integration Tests
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
id: go
|
||||
|
||||
- name: Install SeaweedFS
|
||||
run: |
|
||||
go install -buildvcs=false
|
||||
|
||||
- name: Run KMS provider integration tests
|
||||
timeout-minutes: 15
|
||||
working-directory: test/kms
|
||||
run: |
|
||||
set -x
|
||||
echo "=== System Information ==="
|
||||
uname -a
|
||||
free -h
|
||||
docker --version
|
||||
|
||||
make test-provider-ci
|
||||
|
||||
- name: Show OpenBao logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== OpenBao Container Logs ==="
|
||||
docker logs openbao-ci 2>&1 | tail -50 || echo "No OpenBao container found"
|
||||
echo "=== Setup Logs ==="
|
||||
cat /tmp/openbao-ci-setup.log 2>/dev/null || echo "No setup log found"
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
working-directory: test/kms
|
||||
run: |
|
||||
make stop-openbao-ci || true
|
||||
|
||||
s3-kms-e2e-tests:
|
||||
name: S3 KMS End-to-End Tests
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
id: go
|
||||
|
||||
- name: Install SeaweedFS
|
||||
run: |
|
||||
go install -buildvcs=false
|
||||
|
||||
- name: Run S3 KMS end-to-end tests
|
||||
timeout-minutes: 20
|
||||
working-directory: test/kms
|
||||
run: |
|
||||
set -x
|
||||
echo "=== System Information ==="
|
||||
uname -a
|
||||
free -h
|
||||
docker --version
|
||||
aws --version
|
||||
|
||||
make test-s3-kms-ci
|
||||
|
||||
- name: Show logs on failure
|
||||
if: failure()
|
||||
working-directory: test/kms
|
||||
run: |
|
||||
echo "=== OpenBao Container Logs ==="
|
||||
docker logs openbao-ci 2>&1 | tail -50 || echo "No OpenBao container found"
|
||||
echo "=== SeaweedFS Server Logs ==="
|
||||
tail -100 /tmp/seaweedfs-kms-mini.log 2>/dev/null || echo "No server log found"
|
||||
echo "=== Setup Logs ==="
|
||||
cat /tmp/openbao-ci-setup.log 2>/dev/null || echo "No OpenBao setup log"
|
||||
cat /tmp/weed-kms-ci-setup.log 2>/dev/null || echo "No weed setup log"
|
||||
echo "=== Process Information ==="
|
||||
ps aux | grep -E "(weed|test)" || true
|
||||
|
||||
- name: Upload test logs on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: s3-kms-e2e-logs
|
||||
path: |
|
||||
/tmp/seaweedfs-kms-mini.log
|
||||
/tmp/openbao-ci-setup.log
|
||||
/tmp/weed-kms-ci-setup.log
|
||||
retention-days: 3
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
working-directory: test/kms
|
||||
run: |
|
||||
make stop-seaweedfs-ci || true
|
||||
make stop-openbao-ci || true
|
||||
make clean-ci || true
|
||||
Executable
+260
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Integration test: git clone & pull on a SeaweedFS FUSE mount.
|
||||
#
|
||||
# Verifies that the mount correctly supports git's file operations by:
|
||||
# 1. Creating a bare repo on the mount (acts as a remote)
|
||||
# 2. Cloning it, making commits, and pushing back to the mount
|
||||
# 3. Cloning from the mount into a working directory also on the mount
|
||||
# 4. Pushing additional commits to the bare repo
|
||||
# 5. Checking out an older revision in the on-mount clone
|
||||
# 6. Running git pull to fast-forward with real changes
|
||||
# 7. Verifying file content integrity at each step
|
||||
#
|
||||
# Usage:
|
||||
# bash test-git-on-mount.sh /path/to/mount/point
|
||||
#
|
||||
# The mount must already be running. All test artifacts are created under
|
||||
# <mount>/git-test-<pid> and cleaned up on exit (unless TEST_KEEP=1).
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
MOUNT_DIR="${1:?Usage: $0 <mount-dir>}"
|
||||
TEST_DIR="$MOUNT_DIR/git-test-$$"
|
||||
LOCAL_DIR=$(mktemp -d)
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
cleanup() {
|
||||
if [[ "${TEST_KEEP:-}" == "1" ]]; then
|
||||
echo "TEST_KEEP=1 — leaving artifacts:"
|
||||
echo " mount: $TEST_DIR"
|
||||
echo " local: $LOCAL_DIR"
|
||||
else
|
||||
rm -rf "$TEST_DIR" 2>/dev/null || true
|
||||
rm -rf "$LOCAL_DIR" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# --- helpers ---------------------------------------------------------------
|
||||
|
||||
pass() { PASS=$((PASS + 1)); echo " PASS: $1"; }
|
||||
fail() { FAIL=$((FAIL + 1)); echo " FAIL: $1"; }
|
||||
|
||||
assert_file_contains() {
|
||||
local file=$1 expected=$2 label=$3
|
||||
if [[ -f "$file" ]] && grep -qF "$expected" "$file" 2>/dev/null; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label (expected '$expected' in $file)"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_file_exists() {
|
||||
local file=$1 label=$2
|
||||
if [[ -f "$file" ]]; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label ($file not found)"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_file_not_exists() {
|
||||
local file=$1 label=$2
|
||||
if [[ ! -f "$file" ]]; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label ($file should not exist)"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_eq() {
|
||||
local actual=$1 expected=$2 label=$3
|
||||
if [[ "$actual" == "$expected" ]]; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label (expected '$expected', got '$actual')"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- setup -----------------------------------------------------------------
|
||||
|
||||
echo "========================================"
|
||||
echo " Git-on-mount integration test"
|
||||
echo "========================================"
|
||||
echo "Mount: $MOUNT_DIR"
|
||||
echo "Test: $TEST_DIR"
|
||||
echo "Local: $LOCAL_DIR"
|
||||
echo ""
|
||||
|
||||
if ! mountpoint -q "$MOUNT_DIR" 2>/dev/null && [[ ! -d "$MOUNT_DIR" ]]; then
|
||||
echo "ERROR: $MOUNT_DIR is not a valid directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$TEST_DIR"
|
||||
|
||||
# --- Phase 1: Create bare repo on mount -----------------------------------
|
||||
|
||||
echo "--- Phase 1: Create bare repo on mount ---"
|
||||
|
||||
BARE_REPO="$TEST_DIR/repo.git"
|
||||
git init --bare "$BARE_REPO" >/dev/null 2>&1
|
||||
pass "bare repo created on mount"
|
||||
|
||||
# --- Phase 2: Clone locally, make initial commits, push -------------------
|
||||
|
||||
echo "--- Phase 2: Clone locally, make initial commits, push ---"
|
||||
|
||||
LOCAL_CLONE="$LOCAL_DIR/clone1"
|
||||
git clone "$BARE_REPO" "$LOCAL_CLONE" >/dev/null 2>&1
|
||||
cd "$LOCAL_CLONE"
|
||||
git config user.email "test@seaweedfs.test"
|
||||
git config user.name "SeaweedFS Test"
|
||||
|
||||
# Commit 1: initial files
|
||||
echo "hello world" > README.md
|
||||
mkdir -p src
|
||||
echo 'package main; import "fmt"; func main() { fmt.Println("v1") }' > src/main.go
|
||||
git add -A && git commit -m "initial commit" >/dev/null 2>&1
|
||||
COMMIT1=$(git rev-parse HEAD)
|
||||
|
||||
# Commit 2: add more files
|
||||
mkdir -p data
|
||||
for i in $(seq 1 20); do
|
||||
printf "file-%03d: %s\n" "$i" "$(head -c 64 /dev/urandom | base64)" > "data/file-$(printf '%03d' $i).txt"
|
||||
done
|
||||
git add -A && git commit -m "add data files" >/dev/null 2>&1
|
||||
COMMIT2=$(git rev-parse HEAD)
|
||||
|
||||
# Commit 3: modify and add
|
||||
echo 'package main; import "fmt"; func main() { fmt.Println("v2") }' > src/main.go
|
||||
echo "# Updated readme" >> README.md
|
||||
mkdir -p docs
|
||||
echo "documentation content" > docs/guide.md
|
||||
git add -A && git commit -m "update src and add docs" >/dev/null 2>&1
|
||||
COMMIT3=$(git rev-parse HEAD)
|
||||
|
||||
git push origin master >/dev/null 2>&1 || git push origin main >/dev/null 2>&1
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
pass "3 commits pushed to mount bare repo (branch=$BRANCH)"
|
||||
|
||||
# --- Phase 3: Clone from mount bare repo to mount working dir -------------
|
||||
|
||||
echo "--- Phase 3: Clone from mount bare repo to on-mount working dir ---"
|
||||
|
||||
MOUNT_CLONE="$TEST_DIR/working"
|
||||
git clone "$BARE_REPO" "$MOUNT_CLONE" >/dev/null 2>&1
|
||||
|
||||
# Verify clone integrity
|
||||
assert_file_exists "$MOUNT_CLONE/README.md" "README.md exists after clone"
|
||||
assert_file_contains "$MOUNT_CLONE/README.md" "# Updated readme" "README.md has latest content"
|
||||
assert_file_contains "$MOUNT_CLONE/src/main.go" 'v2' "src/main.go has v2"
|
||||
assert_file_exists "$MOUNT_CLONE/docs/guide.md" "docs/guide.md exists"
|
||||
assert_file_exists "$MOUNT_CLONE/data/file-001.txt" "data files exist"
|
||||
assert_file_exists "$MOUNT_CLONE/data/file-020.txt" "data/file-020.txt exists"
|
||||
|
||||
CLONE_HEAD=$(cd "$MOUNT_CLONE" && git rev-parse HEAD)
|
||||
assert_eq "$CLONE_HEAD" "$COMMIT3" "on-mount clone HEAD matches commit 3"
|
||||
|
||||
# Count files
|
||||
FILE_COUNT=$(find "$MOUNT_CLONE/data" -name '*.txt' | wc -l | tr -d ' ')
|
||||
assert_eq "$FILE_COUNT" "20" "data/ has 20 files"
|
||||
|
||||
# --- Phase 4: Push more commits from local clone --------------------------
|
||||
|
||||
echo "--- Phase 4: Push more commits from local clone ---"
|
||||
|
||||
cd "$LOCAL_CLONE"
|
||||
|
||||
# Commit 4: larger changes
|
||||
for i in $(seq 21 50); do
|
||||
printf "file-%03d: %s\n" "$i" "$(head -c 128 /dev/urandom | base64)" > "data/file-$(printf '%03d' $i).txt"
|
||||
done
|
||||
echo 'package main; import "fmt"; func main() { fmt.Println("v3") }' > src/main.go
|
||||
git add -A && git commit -m "expand data and update to v3" >/dev/null 2>&1
|
||||
COMMIT4=$(git rev-parse HEAD)
|
||||
|
||||
# Commit 5: rename and delete
|
||||
git mv docs/guide.md docs/manual.md
|
||||
git rm data/file-001.txt >/dev/null 2>&1
|
||||
git commit -m "rename guide, remove file-001" >/dev/null 2>&1
|
||||
COMMIT5=$(git rev-parse HEAD)
|
||||
|
||||
git push origin "$BRANCH" >/dev/null 2>&1
|
||||
pass "2 more commits pushed (5 total)"
|
||||
|
||||
# --- Phase 5: Checkout older revision in on-mount clone -------------------
|
||||
|
||||
echo "--- Phase 5: Checkout older revision in on-mount clone ---"
|
||||
|
||||
cd "$MOUNT_CLONE"
|
||||
git checkout "$COMMIT2" >/dev/null 2>&1
|
||||
|
||||
# Verify we're at commit 2 state
|
||||
DETACHED_HEAD=$(git rev-parse HEAD)
|
||||
assert_eq "$DETACHED_HEAD" "$COMMIT2" "on-mount clone at commit 2 (detached)"
|
||||
assert_file_not_exists "$MOUNT_CLONE/docs/guide.md" "docs/guide.md not in commit 2"
|
||||
assert_file_contains "$MOUNT_CLONE/src/main.go" 'v1' "src/main.go has v1 at commit 2"
|
||||
|
||||
# --- Phase 6: Return to branch and pull -----------------------------------
|
||||
|
||||
echo "--- Phase 6: Return to branch and pull with real changes ---"
|
||||
|
||||
cd "$MOUNT_CLONE"
|
||||
git checkout "$BRANCH" >/dev/null 2>&1
|
||||
# At this point the on-mount clone is at commit 3, remote is at commit 5
|
||||
OLD_HEAD=$(git rev-parse HEAD)
|
||||
assert_eq "$OLD_HEAD" "$COMMIT3" "on-mount clone at commit 3 before pull"
|
||||
|
||||
git pull >/dev/null 2>&1
|
||||
NEW_HEAD=$(git rev-parse HEAD)
|
||||
assert_eq "$NEW_HEAD" "$COMMIT5" "HEAD matches commit 5 after pull"
|
||||
|
||||
# Verify commit 5 state
|
||||
assert_file_contains "$MOUNT_CLONE/src/main.go" 'v3' "src/main.go has v3 after pull"
|
||||
assert_file_exists "$MOUNT_CLONE/docs/manual.md" "docs/manual.md exists (renamed)"
|
||||
assert_file_not_exists "$MOUNT_CLONE/docs/guide.md" "docs/guide.md gone (renamed)"
|
||||
assert_file_not_exists "$MOUNT_CLONE/data/file-001.txt" "data/file-001.txt removed"
|
||||
assert_file_exists "$MOUNT_CLONE/data/file-050.txt" "data/file-050.txt exists"
|
||||
|
||||
FINAL_COUNT=$(find "$MOUNT_CLONE/data" -name '*.txt' | wc -l | tr -d ' ')
|
||||
assert_eq "$FINAL_COUNT" "49" "data/ has 49 files (50 added, 1 removed)"
|
||||
|
||||
# --- Phase 7: Verify git log integrity -----------------------------------
|
||||
|
||||
echo "--- Phase 7: Verify git log integrity ---"
|
||||
|
||||
cd "$MOUNT_CLONE"
|
||||
GIT_LOG=$(git log --format=%s)
|
||||
LOG_COUNT=$(echo "$GIT_LOG" | wc -l | tr -d ' ')
|
||||
assert_eq "$LOG_COUNT" "5" "git log shows 5 commits"
|
||||
|
||||
# Verify commit messages
|
||||
echo "$GIT_LOG" | grep -qF "initial commit" && pass "commit 1 message in log" || fail "commit 1 message missing"
|
||||
echo "$GIT_LOG" | grep -qF "expand data" && pass "commit 4 message in log" || fail "commit 4 message missing"
|
||||
echo "$GIT_LOG" | grep -qF "rename guide" && pass "commit 5 message in log" || fail "commit 5 message missing"
|
||||
|
||||
# --- Phase 8: Verify git status is clean ----------------------------------
|
||||
|
||||
echo "--- Phase 8: Verify git status is clean ---"
|
||||
|
||||
cd "$MOUNT_CLONE"
|
||||
STATUS=$(git status --porcelain)
|
||||
if [[ -z "$STATUS" ]]; then
|
||||
pass "git status is clean"
|
||||
else
|
||||
fail "git status has changes: $STATUS"
|
||||
fi
|
||||
|
||||
# --- Results ---------------------------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " Results: $PASS passed, $FAIL failed"
|
||||
echo "========================================"
|
||||
|
||||
if [[ "$FAIL" -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -46,8 +46,8 @@ require (
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/karlseguin/ccache/v2 v2.0.8
|
||||
github.com/klauspost/compress v1.18.4
|
||||
github.com/klauspost/reedsolomon v1.13.0
|
||||
github.com/klauspost/compress v1.18.5
|
||||
github.com/klauspost/reedsolomon v1.13.3
|
||||
github.com/kurin/blazer v0.5.3
|
||||
github.com/linxGnu/grocksdb v1.10.7
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
@@ -91,14 +91,14 @@ require (
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
gocloud.dev v0.45.0
|
||||
gocloud.dev/pubsub/natspubsub v0.45.0
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.44.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.45.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
|
||||
golang.org/x/image v0.36.0
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/net v0.51.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sys v0.42.0
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.267.0
|
||||
@@ -131,7 +131,7 @@ require (
|
||||
github.com/cognusion/imaging v1.0.2
|
||||
github.com/fluent/fluent-logger-golang v1.10.1
|
||||
github.com/getsentry/sentry-go v0.43.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-ldap/ldap/v3 v3.4.13
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/google/flatbuffers/go v0.0.0-20230108230133-3b8644d32c50
|
||||
github.com/hashicorp/raft v1.7.3
|
||||
@@ -149,7 +149,7 @@ require (
|
||||
github.com/rdleal/intervalst v1.5.0
|
||||
github.com/redis/go-redis/v9 v9.18.0
|
||||
github.com/schollz/progressbar/v3 v3.19.0
|
||||
github.com/seaweedfs/go-fuse/v2 v2.9.1
|
||||
github.com/seaweedfs/go-fuse/v2 v2.9.2
|
||||
github.com/shirou/gopsutil/v4 v4.26.2
|
||||
github.com/tarantool/go-tarantool/v2 v2.4.2
|
||||
github.com/testcontainers/testcontainers-go v0.40.0
|
||||
@@ -159,7 +159,7 @@ require (
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.125.3
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.7
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated
|
||||
google.golang.org/grpc/security/advancedtls v1.0.0
|
||||
)
|
||||
@@ -498,7 +498,7 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
golang.org/x/term v0.40.0 // indirect
|
||||
golang.org/x/term v0.41.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
|
||||
@@ -1083,8 +1083,8 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
|
||||
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
@@ -1475,15 +1475,15 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
|
||||
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
|
||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/reedsolomon v1.13.0 h1:E0Cmgf2kMuhZTj6eefnvpKC4/Q4jhCi9YIjcZjK4arc=
|
||||
github.com/klauspost/reedsolomon v1.13.0/go.mod h1:ggJT9lc71Vu+cSOPBlxGvBN6TfAS77qB4fp8vJ05NSA=
|
||||
github.com/klauspost/reedsolomon v1.13.3 h1:01GwnO2xoCSaM0ShP4qwl+FsHg3csFShC6Tu/RS1ji0=
|
||||
github.com/klauspost/reedsolomon v1.13.3/go.mod h1:yjqqjgMTQkBUHSG97/rm4zipffCNbCiZcB3kTqr++sQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/koofr/go-httpclient v0.0.0-20240520111329-e20f8f203988 h1:CjEMN21Xkr9+zwPmZPaJJw+apzVbjGL5uK/6g9Q2jGU=
|
||||
@@ -1840,6 +1840,8 @@ github.com/seaweedfs/cockroachdb-parser v0.0.0-20260225204133-2f342c5ea564 h1:Tg
|
||||
github.com/seaweedfs/cockroachdb-parser v0.0.0-20260225204133-2f342c5ea564/go.mod h1:JSKCh6uCHBz91lQYFYHCyTrSVIPge4SUFVn28iwMNB0=
|
||||
github.com/seaweedfs/go-fuse/v2 v2.9.1 h1:gnKmfrKreCRGJmekGz5WMnNZqXEf9s9+V2hdWQdvx88=
|
||||
github.com/seaweedfs/go-fuse/v2 v2.9.1/go.mod h1:zABdmWEa6A0bwaBeEOBUeUkGIZlxUhcdv+V1Dcc/U/I=
|
||||
github.com/seaweedfs/go-fuse/v2 v2.9.2 h1:IfP/yFjLGO4rALcJY2Gb39PlebHxLnj7dkIiQAjFres=
|
||||
github.com/seaweedfs/go-fuse/v2 v2.9.2/go.mod h1:zABdmWEa6A0bwaBeEOBUeUkGIZlxUhcdv+V1Dcc/U/I=
|
||||
github.com/seaweedfs/goexif v1.0.3 h1:ve/OjI7dxPW8X9YQsv3JuVMaxEyF9Rvfd04ouL+Bz30=
|
||||
github.com/seaweedfs/goexif v1.0.3/go.mod h1:Oni780Z236sXpIQzk1XoJlTwqrJ02smEin9zQeff7Fk=
|
||||
github.com/seaweedfs/raft v1.1.7 h1:3mVJZ2p4rdvBtbbrHROPjYKtH+q5qjMBc56G6VRu1kA=
|
||||
@@ -2184,8 +2186,8 @@ gocloud.dev v0.45.0 h1:WknIK8IbRdmynDvara3Q7G6wQhmEiOGwpgJufbM39sY=
|
||||
gocloud.dev v0.45.0/go.mod h1:0kXKmkCLG6d31N7NyLZWzt7jDSQura9zD/mWgiB6THI=
|
||||
gocloud.dev/pubsub/natspubsub v0.45.0 h1:kfCupVejeynIQcS1GaIvkY3Nj/acLlM5yPWyxZN8wJ8=
|
||||
gocloud.dev/pubsub/natspubsub v0.45.0/go.mod h1:WU5SMDWuF7CCr2U8UpbrEONYYFOmvigCAvTpOOpUvYE=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.44.0 h1:MpRIO6XJ/JTqrlUWt3CxwDe1LvaiXUVu4sS5cv4f/AM=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.44.0/go.mod h1:BB9+qT3r6g4M5+4asiXaEeqw4QAOzsWusO5krYaqkdA=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.45.0 h1:hlejKjBFYQBUAj7ZsHKgW0JOER/3XUqrMFFxBOFUIOU=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.45.0/go.mod h1:Kqq6gppGY/RTrRQkKSHZq+/qbCufPEoQ0E1V3X389zM=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -2208,8 +2210,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -2344,8 +2346,8 @@ golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -2397,8 +2399,8 @@ golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -2523,8 +2525,8 @@ golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -2545,8 +2547,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
@@ -15,6 +15,7 @@ metadata:
|
||||
{{- toYaml .Values.s3.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.s3.service.type }}
|
||||
internalTrafficPolicy: {{ .Values.s3.internalTrafficPolicy | default "Cluster" }}
|
||||
{{- $td := .Values.s3.trafficDistribution | default .Values.filer.s3.trafficDistribution }}
|
||||
{{- if and (semverCompare ">=1.31-0" .Capabilities.KubeVersion.GitVersion) $td }}
|
||||
|
||||
@@ -15,6 +15,7 @@ metadata:
|
||||
{{- toYaml .Values.sftp.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.sftp.service.type }}
|
||||
internalTrafficPolicy: {{ .Values.sftp.internalTrafficPolicy | default "Cluster" }}
|
||||
ports:
|
||||
- name: "swfs-sftp"
|
||||
@@ -31,4 +32,4 @@ spec:
|
||||
app.kubernetes.io/name: {{ template "seaweedfs.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: sftp
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1104,6 +1104,10 @@ s3:
|
||||
annotations: {}
|
||||
tls: []
|
||||
|
||||
# Service settings
|
||||
service:
|
||||
type: ClusterIP
|
||||
|
||||
icebergIngress:
|
||||
enabled: false
|
||||
className: ""
|
||||
@@ -1185,6 +1189,10 @@ sftp:
|
||||
failureThreshold: 100
|
||||
timeoutSeconds: 10
|
||||
|
||||
# Service settings
|
||||
service:
|
||||
type: ClusterIP
|
||||
|
||||
admin:
|
||||
enabled: false
|
||||
imageOverride: null
|
||||
|
||||
@@ -33,6 +33,9 @@ service SeaweedFiler {
|
||||
rpc StreamRenameEntry (StreamRenameEntryRequest) returns (stream StreamRenameEntryResponse) {
|
||||
}
|
||||
|
||||
rpc StreamMutateEntry (stream StreamMutateEntryRequest) returns (stream StreamMutateEntryResponse) {
|
||||
}
|
||||
|
||||
rpc AssignVolume (AssignVolumeRequest) returns (AssignVolumeResponse) {
|
||||
}
|
||||
|
||||
@@ -203,9 +206,21 @@ message CreateEntryRequest {
|
||||
bool skip_check_parent_directory = 6;
|
||||
}
|
||||
|
||||
// Structured error codes for filer entry operations.
|
||||
// Values are stable — do not reorder or reuse numbers.
|
||||
enum FilerError {
|
||||
OK = 0;
|
||||
ENTRY_NAME_TOO_LONG = 1; // name exceeds max_file_name_length
|
||||
PARENT_IS_FILE = 2; // parent path component is a file, not a directory
|
||||
EXISTING_IS_DIRECTORY = 3; // cannot overwrite directory with file
|
||||
EXISTING_IS_FILE = 4; // cannot overwrite file with directory
|
||||
ENTRY_ALREADY_EXISTS = 5; // O_EXCL and entry already exists
|
||||
}
|
||||
|
||||
message CreateEntryResponse {
|
||||
string error = 1;
|
||||
string error = 1; // kept for human readability + backward compat
|
||||
SubscribeMetadataResponse metadata_event = 2;
|
||||
FilerError error_code = 3; // machine-readable error code
|
||||
}
|
||||
|
||||
message UpdateEntryRequest {
|
||||
@@ -523,3 +538,32 @@ message TransferLocksRequest {
|
||||
}
|
||||
message TransferLocksResponse {
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// StreamMutateEntry: ordered bidirectional streaming for all filer mutations.
|
||||
// All create/update/delete/rename operations from a single mount go through
|
||||
// one stream, preserving mutation ordering and eliminating per-request
|
||||
// connection overhead.
|
||||
|
||||
message StreamMutateEntryRequest {
|
||||
uint64 request_id = 1;
|
||||
oneof request {
|
||||
CreateEntryRequest create_request = 2;
|
||||
UpdateEntryRequest update_request = 3;
|
||||
DeleteEntryRequest delete_request = 4;
|
||||
StreamRenameEntryRequest rename_request = 5;
|
||||
}
|
||||
}
|
||||
|
||||
message StreamMutateEntryResponse {
|
||||
uint64 request_id = 1;
|
||||
bool is_last = 2; // always true except for rename, which sends multiple events
|
||||
oneof response {
|
||||
CreateEntryResponse create_response = 3;
|
||||
UpdateEntryResponse update_response = 4;
|
||||
DeleteEntryResponse delete_response = 5;
|
||||
StreamRenameEntryResponse rename_response = 6;
|
||||
}
|
||||
string error = 7; // human-readable error message when the operation failed
|
||||
int32 errno = 8; // POSIX errno (e.g. ENOENT=2, ENOTEMPTY=66) for direct FUSE status mapping
|
||||
}
|
||||
|
||||
@@ -15,25 +15,21 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// FuseTestFramework provides utilities for FUSE integration testing
|
||||
// FuseTestFramework provides utilities for FUSE integration testing.
|
||||
// It starts a single "weed mini" process (master+volume+filer in one)
|
||||
// and a separate "weed mount" process for the FUSE filesystem.
|
||||
type FuseTestFramework struct {
|
||||
t *testing.T
|
||||
tempDir string
|
||||
mountPoint string
|
||||
dataDir string
|
||||
logDir string
|
||||
masterProcess *os.Process
|
||||
volumeProcess *os.Process
|
||||
filerProcess *os.Process
|
||||
mountProcess *os.Process
|
||||
masterAddr string
|
||||
volumeAddr string
|
||||
filerAddr string
|
||||
masterPort int
|
||||
volumePort int
|
||||
filerPort int
|
||||
weedBinary string
|
||||
isSetup bool
|
||||
t *testing.T
|
||||
tempDir string
|
||||
mountPoint string
|
||||
dataDir string
|
||||
logDir string
|
||||
miniProcess *os.Process
|
||||
mountProcess *os.Process
|
||||
filerAddr string
|
||||
filerPort int
|
||||
weedBinary string
|
||||
isSetup bool
|
||||
}
|
||||
|
||||
// TestConfig holds configuration for FUSE tests
|
||||
@@ -63,8 +59,6 @@ func DefaultTestConfig() *TestConfig {
|
||||
}
|
||||
|
||||
// NewFuseTestFramework creates a new FUSE testing framework.
|
||||
// Each instance allocates its own free ports so multiple tests can run
|
||||
// sequentially without port conflicts from slow cleanup.
|
||||
func NewFuseTestFramework(t *testing.T, config *TestConfig) *FuseTestFramework {
|
||||
if config == nil {
|
||||
config = DefaultTestConfig()
|
||||
@@ -73,8 +67,6 @@ func NewFuseTestFramework(t *testing.T, config *TestConfig) *FuseTestFramework {
|
||||
tempDir, err := os.MkdirTemp("", "seaweedfs_fuse_test_")
|
||||
require.NoError(t, err)
|
||||
|
||||
masterPort := freePort(t)
|
||||
volumePort := freePort(t)
|
||||
filerPort := freePort(t)
|
||||
|
||||
return &FuseTestFramework{
|
||||
@@ -83,11 +75,7 @@ func NewFuseTestFramework(t *testing.T, config *TestConfig) *FuseTestFramework {
|
||||
mountPoint: filepath.Join(tempDir, "mount"),
|
||||
dataDir: filepath.Join(tempDir, "data"),
|
||||
logDir: filepath.Join(tempDir, "logs"),
|
||||
masterPort: masterPort,
|
||||
volumePort: volumePort,
|
||||
filerPort: filerPort,
|
||||
masterAddr: fmt.Sprintf("127.0.0.1:%d", masterPort),
|
||||
volumeAddr: fmt.Sprintf("127.0.0.1:%d", volumePort),
|
||||
filerAddr: fmt.Sprintf("127.0.0.1:%d", filerPort),
|
||||
weedBinary: findWeedBinary(),
|
||||
isSetup: false,
|
||||
@@ -97,63 +85,50 @@ func NewFuseTestFramework(t *testing.T, config *TestConfig) *FuseTestFramework {
|
||||
// freePort asks the OS for a free TCP port.
|
||||
func freePort(t *testing.T) int {
|
||||
t.Helper()
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
port := l.Addr().(*net.TCPAddr).Port
|
||||
l.Close()
|
||||
return port
|
||||
const (
|
||||
minServicePort = 20000
|
||||
maxServicePort = 55535 // SeaweedFS gRPC service uses httpPort + 10000.
|
||||
)
|
||||
|
||||
portCount := maxServicePort - minServicePort + 1
|
||||
start := minServicePort + int(time.Now().UnixNano()%int64(portCount))
|
||||
|
||||
for attempt := 0; attempt < 512; attempt++ {
|
||||
port := minServicePort + (start-minServicePort+attempt)%portCount
|
||||
l, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
l.Close()
|
||||
return port
|
||||
}
|
||||
|
||||
t.Fatalf("failed to allocate port <= %d after repeated attempts", maxServicePort)
|
||||
return 0
|
||||
}
|
||||
|
||||
// Setup starts SeaweedFS cluster and mounts FUSE filesystem
|
||||
// Setup starts "weed mini" and mounts the FUSE filesystem.
|
||||
func (f *FuseTestFramework) Setup(config *TestConfig) error {
|
||||
if f.isSetup {
|
||||
return fmt.Errorf("framework already setup")
|
||||
}
|
||||
|
||||
// Create all required directories upfront
|
||||
dirs := []string{
|
||||
f.mountPoint,
|
||||
f.logDir,
|
||||
filepath.Join(f.dataDir, "master"),
|
||||
filepath.Join(f.dataDir, "volume"),
|
||||
}
|
||||
dirs := []string{f.mountPoint, f.logDir, f.dataDir}
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start master
|
||||
if err := f.startMaster(config); err != nil {
|
||||
return fmt.Errorf("failed to start master: %v", err)
|
||||
// Start weed mini (master + volume + filer in one process)
|
||||
if err := f.startMini(config); err != nil {
|
||||
return fmt.Errorf("failed to start weed mini: %v", err)
|
||||
}
|
||||
|
||||
// Wait for master to be ready
|
||||
if err := f.waitForService(f.masterAddr, 30*time.Second); err != nil {
|
||||
f.dumpLog("master")
|
||||
return fmt.Errorf("master not ready: %v", err)
|
||||
}
|
||||
|
||||
// Start volume servers
|
||||
if err := f.startVolumeServers(config); err != nil {
|
||||
return fmt.Errorf("failed to start volume servers: %v", err)
|
||||
}
|
||||
|
||||
// Wait for volume server to be ready
|
||||
if err := f.waitForService(f.volumeAddr, 30*time.Second); err != nil {
|
||||
f.dumpLog("volume")
|
||||
return fmt.Errorf("volume server not ready: %v", err)
|
||||
}
|
||||
|
||||
// Start filer
|
||||
if err := f.startFiler(config); err != nil {
|
||||
return fmt.Errorf("failed to start filer: %v", err)
|
||||
}
|
||||
|
||||
// Wait for filer to be ready
|
||||
// Wait for filer to be ready (mini starts all services on filerPort)
|
||||
if err := f.waitForService(f.filerAddr, 30*time.Second); err != nil {
|
||||
f.dumpLog("filer")
|
||||
return fmt.Errorf("filer not ready: %v", err)
|
||||
f.dumpLog("mini")
|
||||
return fmt.Errorf("weed mini not ready: %v", err)
|
||||
}
|
||||
|
||||
// Mount FUSE filesystem
|
||||
@@ -171,27 +146,39 @@ func (f *FuseTestFramework) Setup(config *TestConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup stops all processes and removes temporary files
|
||||
// Cleanup stops all processes and removes temporary files.
|
||||
// If the test failed, it dumps logs automatically.
|
||||
func (f *FuseTestFramework) Cleanup() {
|
||||
if f.t.Failed() {
|
||||
f.DumpLogs()
|
||||
}
|
||||
|
||||
if f.mountProcess != nil {
|
||||
f.unmountFuse()
|
||||
}
|
||||
|
||||
// Stop processes in reverse order
|
||||
processes := []*os.Process{f.mountProcess, f.filerProcess, f.volumeProcess, f.masterProcess}
|
||||
for _, proc := range processes {
|
||||
for _, proc := range []*os.Process{f.mountProcess, f.miniProcess} {
|
||||
if proc != nil {
|
||||
proc.Signal(syscall.SIGTERM)
|
||||
proc.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove temp directory
|
||||
f.copyLogsForCI()
|
||||
|
||||
if !DefaultTestConfig().SkipCleanup {
|
||||
os.RemoveAll(f.tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
// DumpLogs prints the tail of all SeaweedFS process logs to test output.
|
||||
func (f *FuseTestFramework) DumpLogs() {
|
||||
for _, name := range []string{"mini", "mount"} {
|
||||
f.dumpLog(name)
|
||||
}
|
||||
}
|
||||
|
||||
// GetMountPoint returns the FUSE mount point path
|
||||
func (f *FuseTestFramework) GetMountPoint() string {
|
||||
return f.mountPoint
|
||||
@@ -223,82 +210,56 @@ func (f *FuseTestFramework) startProcess(name string, args []string) (*os.Proces
|
||||
}
|
||||
|
||||
// dumpLog prints the last lines of a process log file to the test output
|
||||
// for debugging when a service fails to start.
|
||||
// for debugging when a service fails to start or a test fails.
|
||||
func (f *FuseTestFramework) dumpLog(name string) {
|
||||
data, err := os.ReadFile(filepath.Join(f.logDir, name+".log"))
|
||||
if err != nil {
|
||||
f.t.Logf("[%s log] (not available: %v)", name, err)
|
||||
return
|
||||
}
|
||||
// Truncate to last 2KB to keep output manageable
|
||||
if len(data) > 2048 {
|
||||
data = data[len(data)-2048:]
|
||||
// Show last 16KB on failure for meaningful context.
|
||||
const maxTail = 16 * 1024
|
||||
if len(data) > maxTail {
|
||||
data = data[len(data)-maxTail:]
|
||||
}
|
||||
f.t.Logf("[%s log tail]\n%s", name, string(data))
|
||||
f.t.Logf("[%s log tail (%d bytes)]\n%s", name, len(data), string(data))
|
||||
}
|
||||
|
||||
// startMaster starts the SeaweedFS master server
|
||||
func (f *FuseTestFramework) startMaster(config *TestConfig) error {
|
||||
// Do NOT set -port.grpc explicitly. SeaweedFS convention is gRPC = HTTP + 10000.
|
||||
// Volume/filer discover the master gRPC port by this convention, so overriding
|
||||
// it breaks inter-service communication.
|
||||
// copyLogsForCI copies SeaweedFS process logs to /tmp/seaweedfs-fuse-logs/
|
||||
// so the CI workflow can upload them as artifacts.
|
||||
func (f *FuseTestFramework) copyLogsForCI() {
|
||||
ciLogDir := "/tmp/seaweedfs-fuse-logs"
|
||||
os.MkdirAll(ciLogDir, 0755)
|
||||
for _, name := range []string{"mini", "mount"} {
|
||||
src := filepath.Join(f.logDir, name+".log")
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
os.WriteFile(filepath.Join(ciLogDir, name+".log"), data, 0644)
|
||||
}
|
||||
}
|
||||
|
||||
// startMini starts "weed mini" which runs master+volume+filer in one process.
|
||||
func (f *FuseTestFramework) startMini(config *TestConfig) error {
|
||||
args := []string{
|
||||
"master",
|
||||
"mini",
|
||||
"-dir=" + f.dataDir,
|
||||
"-ip=127.0.0.1",
|
||||
"-port=" + strconv.Itoa(f.masterPort),
|
||||
"-mdir=" + filepath.Join(f.dataDir, "master"),
|
||||
"-filer.port=" + strconv.Itoa(f.filerPort),
|
||||
"-s3=false",
|
||||
"-webdav=false",
|
||||
"-admin.ui=false",
|
||||
}
|
||||
if config.EnableDebug {
|
||||
args = append(args, "-v=4")
|
||||
}
|
||||
|
||||
proc, err := f.startProcess("master", args)
|
||||
proc, err := f.startProcess("mini", args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.masterProcess = proc
|
||||
return nil
|
||||
}
|
||||
|
||||
// startVolumeServers starts SeaweedFS volume servers
|
||||
func (f *FuseTestFramework) startVolumeServers(config *TestConfig) error {
|
||||
args := []string{
|
||||
"volume",
|
||||
"-master=127.0.0.1:" + strconv.Itoa(f.masterPort),
|
||||
"-ip=127.0.0.1",
|
||||
"-port=" + strconv.Itoa(f.volumePort),
|
||||
"-dir=" + filepath.Join(f.dataDir, "volume"),
|
||||
fmt.Sprintf("-max=%d", config.NumVolumes),
|
||||
}
|
||||
if config.EnableDebug {
|
||||
args = append(args, "-v=4")
|
||||
}
|
||||
|
||||
proc, err := f.startProcess("volume", args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.volumeProcess = proc
|
||||
return nil
|
||||
}
|
||||
|
||||
// startFiler starts the SeaweedFS filer server
|
||||
func (f *FuseTestFramework) startFiler(config *TestConfig) error {
|
||||
args := []string{
|
||||
"filer",
|
||||
"-master=127.0.0.1:" + strconv.Itoa(f.masterPort),
|
||||
"-ip=127.0.0.1",
|
||||
"-port=" + strconv.Itoa(f.filerPort),
|
||||
}
|
||||
if config.EnableDebug {
|
||||
args = append(args, "-v=4")
|
||||
}
|
||||
|
||||
proc, err := f.startProcess("filer", args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.filerProcess = proc
|
||||
f.miniProcess = proc
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -371,9 +332,7 @@ func (f *FuseTestFramework) waitForService(addr string, timeout time.Duration) e
|
||||
func (f *FuseTestFramework) waitForMount(timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
// Check if mount point is accessible
|
||||
if _, err := os.Stat(f.mountPoint); err == nil {
|
||||
// Try to list directory
|
||||
if _, err := os.ReadDir(f.mountPoint); err == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -384,19 +343,13 @@ func (f *FuseTestFramework) waitForMount(timeout time.Duration) error {
|
||||
}
|
||||
|
||||
// findWeedBinary locates the weed binary.
|
||||
// Checks PATH first (most reliable in CI where the binary is installed to
|
||||
// /usr/local/bin), then falls back to relative paths. Each candidate is
|
||||
// verified to be a regular file so that a source directory named "weed"
|
||||
// is never mistaken for the binary.
|
||||
func findWeedBinary() string {
|
||||
// PATH lookup first — works in CI and when weed is installed globally.
|
||||
if p, err := exec.LookPath("weed"); err == nil {
|
||||
return p
|
||||
}
|
||||
|
||||
// Relative paths for local development (run from test/fuse_integration/).
|
||||
candidates := []string{
|
||||
"../../weed/weed", // built in-tree: weed/weed
|
||||
"../../weed/weed",
|
||||
"./weed",
|
||||
"../weed",
|
||||
}
|
||||
@@ -407,7 +360,6 @@ func findWeedBinary() string {
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback — will fail with a clear "not found" at exec time.
|
||||
return "weed"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
package fuse_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestGitOperations exercises git clone, checkout, and pull on a FUSE mount.
|
||||
//
|
||||
// The test creates a bare repo on the mount (acting as a remote), clones it,
|
||||
// makes commits, pushes, then clones from the mount into an on-mount working
|
||||
// directory. It pushes additional commits, checks out an older revision in the
|
||||
// on-mount clone, and runs git pull to fast-forward with real changes —
|
||||
// verifying file content integrity at each step.
|
||||
func TestGitOperations(t *testing.T) {
|
||||
framework := NewFuseTestFramework(t, DefaultTestConfig())
|
||||
defer framework.Cleanup()
|
||||
|
||||
require.NoError(t, framework.Setup(DefaultTestConfig()))
|
||||
|
||||
mountPoint := framework.GetMountPoint()
|
||||
|
||||
// We need a local scratch dir (not on the mount) for the "developer" clone.
|
||||
localDir, err := os.MkdirTemp("", "git_ops_local_")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(localDir)
|
||||
|
||||
t.Run("CloneAndPull", func(t *testing.T) {
|
||||
testGitCloneAndPull(t, mountPoint, localDir)
|
||||
})
|
||||
}
|
||||
|
||||
func testGitCloneAndPull(t *testing.T, mountPoint, localDir string) {
|
||||
bareRepo := filepath.Join(mountPoint, "repo.git")
|
||||
localClone := filepath.Join(localDir, "clone")
|
||||
mountClone := filepath.Join(mountPoint, "working")
|
||||
|
||||
// ---- Phase 1: Create bare repo on the mount ----
|
||||
t.Log("Phase 1: create bare repo on mount")
|
||||
gitRun(t, "", "init", "--bare", bareRepo)
|
||||
|
||||
// ---- Phase 2: Clone locally, make initial commits, push ----
|
||||
t.Log("Phase 2: clone locally, commit, push")
|
||||
gitRun(t, "", "clone", bareRepo, localClone)
|
||||
gitRun(t, localClone, "config", "user.email", "test@seaweedfs.test")
|
||||
gitRun(t, localClone, "config", "user.name", "Test")
|
||||
|
||||
// Commit 1
|
||||
writeFile(t, localClone, "README.md", "hello world\n")
|
||||
mkdirAll(t, localClone, "src")
|
||||
writeFile(t, localClone, "src/main.go", `package main; import "fmt"; func main() { fmt.Println("v1") }`)
|
||||
gitRun(t, localClone, "add", "-A")
|
||||
gitRun(t, localClone, "commit", "-m", "initial commit")
|
||||
commit1 := gitOutput(t, localClone, "rev-parse", "HEAD")
|
||||
|
||||
// Commit 2: bulk files
|
||||
mkdirAll(t, localClone, "data")
|
||||
for i := 1; i <= 20; i++ {
|
||||
name := filepath.Join("data", "file-"+leftPad(i, 3)+".txt")
|
||||
writeFile(t, localClone, name, "content-"+strconv.Itoa(i)+"\n")
|
||||
}
|
||||
gitRun(t, localClone, "add", "-A")
|
||||
gitRun(t, localClone, "commit", "-m", "add data files")
|
||||
commit2 := gitOutput(t, localClone, "rev-parse", "HEAD")
|
||||
|
||||
// Commit 3: modify + new dir
|
||||
writeFile(t, localClone, "src/main.go", `package main; import "fmt"; func main() { fmt.Println("v2") }`)
|
||||
writeFile(t, localClone, "README.md", "hello world\n# Updated\n")
|
||||
mkdirAll(t, localClone, "docs")
|
||||
writeFile(t, localClone, "docs/guide.md", "documentation\n")
|
||||
gitRun(t, localClone, "add", "-A")
|
||||
gitRun(t, localClone, "commit", "-m", "update src and add docs")
|
||||
commit3 := gitOutput(t, localClone, "rev-parse", "HEAD")
|
||||
|
||||
branch := gitOutput(t, localClone, "rev-parse", "--abbrev-ref", "HEAD")
|
||||
gitRun(t, localClone, "push", "origin", branch)
|
||||
|
||||
// ---- Phase 3: Clone from mount bare repo into on-mount working dir ----
|
||||
t.Log("Phase 3: clone from mount bare repo to on-mount working dir")
|
||||
gitRun(t, "", "clone", bareRepo, mountClone)
|
||||
|
||||
assertFileContains(t, filepath.Join(mountClone, "README.md"), "# Updated")
|
||||
assertFileContains(t, filepath.Join(mountClone, "src/main.go"), "v2")
|
||||
assertFileExists(t, filepath.Join(mountClone, "docs/guide.md"))
|
||||
assertFileExists(t, filepath.Join(mountClone, "data/file-020.txt"))
|
||||
|
||||
head := gitOutput(t, mountClone, "rev-parse", "HEAD")
|
||||
assert.Equal(t, commit3, head, "on-mount clone HEAD should be commit 3")
|
||||
|
||||
dataFiles := countFiles(t, filepath.Join(mountClone, "data"))
|
||||
assert.Equal(t, 20, dataFiles, "data/ should have 20 files")
|
||||
|
||||
// ---- Phase 4: Push more commits from the local clone ----
|
||||
t.Log("Phase 4: push more commits")
|
||||
|
||||
for i := 21; i <= 50; i++ {
|
||||
name := filepath.Join("data", "file-"+leftPad(i, 3)+".txt")
|
||||
writeFile(t, localClone, name, "content-"+strconv.Itoa(i)+"\n")
|
||||
}
|
||||
writeFile(t, localClone, "src/main.go", `package main; import "fmt"; func main() { fmt.Println("v3") }`)
|
||||
gitRun(t, localClone, "add", "-A")
|
||||
gitRun(t, localClone, "commit", "-m", "expand data and update to v3")
|
||||
commit4 := gitOutput(t, localClone, "rev-parse", "HEAD")
|
||||
_ = commit4
|
||||
|
||||
gitRun(t, localClone, "mv", "docs/guide.md", "docs/manual.md")
|
||||
gitRun(t, localClone, "rm", "data/file-001.txt")
|
||||
gitRun(t, localClone, "commit", "-m", "rename guide, remove file-001")
|
||||
commit5 := gitOutput(t, localClone, "rev-parse", "HEAD")
|
||||
|
||||
gitRun(t, localClone, "push", "origin", branch)
|
||||
|
||||
// ---- Phase 5: Reset to older revision in on-mount clone ----
|
||||
t.Log("Phase 5: reset to older revision on mount clone")
|
||||
ensureMountClone(t, bareRepo, mountClone)
|
||||
gitRun(t, mountClone, "reset", "--hard", commit2)
|
||||
|
||||
resetHead := gitOutput(t, mountClone, "rev-parse", "HEAD")
|
||||
assert.Equal(t, commit2, resetHead, "should be at commit 2")
|
||||
assertFileContains(t, filepath.Join(mountClone, "src/main.go"), "v1")
|
||||
assertFileNotExists(t, filepath.Join(mountClone, "docs/guide.md"))
|
||||
|
||||
// ---- Phase 6: Pull with real changes ----
|
||||
t.Log("Phase 6: pull with real fast-forward changes")
|
||||
|
||||
ensureMountClone(t, bareRepo, mountClone)
|
||||
|
||||
// After git reset --hard, give the FUSE mount a moment to settle its
|
||||
// metadata cache. On slow CI, the working directory can briefly appear
|
||||
// missing to a new subprocess (git pull → unpack-objects).
|
||||
waitForDir(t, mountClone)
|
||||
|
||||
oldHead := gitOutput(t, mountClone, "rev-parse", "HEAD")
|
||||
assert.Equal(t, commit2, oldHead, "should be at commit 2 before pull")
|
||||
|
||||
gitRun(t, mountClone, "pull")
|
||||
|
||||
newHead := gitOutput(t, mountClone, "rev-parse", "HEAD")
|
||||
assert.Equal(t, commit5, newHead, "HEAD should be commit 5 after pull")
|
||||
|
||||
assertFileContains(t, filepath.Join(mountClone, "src/main.go"), "v3")
|
||||
assertFileExists(t, filepath.Join(mountClone, "docs/manual.md"))
|
||||
assertFileNotExists(t, filepath.Join(mountClone, "docs/guide.md"))
|
||||
assertFileNotExists(t, filepath.Join(mountClone, "data/file-001.txt"))
|
||||
assertFileExists(t, filepath.Join(mountClone, "data/file-050.txt"))
|
||||
|
||||
finalCount := countFiles(t, filepath.Join(mountClone, "data"))
|
||||
assert.Equal(t, 49, finalCount, "data/ should have 49 files after pull")
|
||||
|
||||
// ---- Phase 7: Verify git log and status ----
|
||||
t.Log("Phase 7: verify log and status")
|
||||
logOutput := gitOutput(t, mountClone, "log", "--format=%s")
|
||||
lines := strings.Split(strings.TrimSpace(logOutput), "\n")
|
||||
assert.Equal(t, 5, len(lines), "should have 5 commits in log")
|
||||
|
||||
assert.Contains(t, logOutput, "initial commit")
|
||||
assert.Contains(t, logOutput, "expand data")
|
||||
assert.Contains(t, logOutput, "rename guide")
|
||||
|
||||
status := gitOutput(t, mountClone, "status", "--porcelain")
|
||||
assert.Empty(t, status, "git status should be clean")
|
||||
|
||||
_ = commit1 // used for documentation; not needed in assertions
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func gitRun(t *testing.T, dir string, args ...string) {
|
||||
t.Helper()
|
||||
gitRunWithRetry(t, dir, args...)
|
||||
}
|
||||
|
||||
func gitOutput(t *testing.T, dir string, args ...string) string {
|
||||
t.Helper()
|
||||
return gitRunWithRetry(t, dir, args...)
|
||||
}
|
||||
|
||||
// gitRunWithRetry runs a git command with retries to handle transient FUSE
|
||||
// I/O errors on slow CI runners (e.g. "Could not write new index file",
|
||||
// "failed to stat", "unpack-objects failed").
|
||||
func gitRunWithRetry(t *testing.T, dir string, args ...string) string {
|
||||
t.Helper()
|
||||
const (
|
||||
maxRetries = 6
|
||||
dirWait = 10 * time.Second
|
||||
)
|
||||
var out []byte
|
||||
var err error
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
if dir != "" && !waitForDirEventually(t, dir, dirWait) {
|
||||
out = []byte("directory missing: " + dir)
|
||||
err = &os.PathError{Op: "stat", Path: dir, Err: os.ErrNotExist}
|
||||
} else {
|
||||
cmd := exec.Command("git", args...)
|
||||
if dir != "" {
|
||||
cmd.Dir = dir
|
||||
}
|
||||
out, err = cmd.CombinedOutput()
|
||||
}
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
if i < maxRetries-1 {
|
||||
t.Logf("git %s attempt %d failed (retrying): %s", strings.Join(args, " "), i+1, string(out))
|
||||
if dir != "" {
|
||||
refreshDirEntry(t, dir)
|
||||
}
|
||||
if repoPath := extractGitRepoPath(string(out)); repoPath != "" {
|
||||
_ = exec.Command("git", "init", "--bare", repoPath).Run()
|
||||
waitForBareRepoEventually(t, repoPath, 5*time.Second)
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
require.NoError(t, err, "git %s failed after %d attempts: %s", strings.Join(args, " "), maxRetries, string(out))
|
||||
return ""
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, base, rel, content string) {
|
||||
t.Helper()
|
||||
p := filepath.Join(base, rel)
|
||||
require.NoError(t, os.WriteFile(p, []byte(content), 0644))
|
||||
}
|
||||
|
||||
func mkdirAll(t *testing.T, base, rel string) {
|
||||
t.Helper()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(base, rel), 0755))
|
||||
}
|
||||
|
||||
func assertFileExists(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
_, err := os.Stat(path)
|
||||
require.NoError(t, err, "expected file to exist: %s", path)
|
||||
}
|
||||
|
||||
func assertFileNotExists(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
_, err := os.Stat(path)
|
||||
require.True(t, os.IsNotExist(err), "expected file not to exist: %s", path)
|
||||
}
|
||||
|
||||
func assertFileContains(t *testing.T, path, substr string) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
require.NoError(t, err, "failed to read %s", path)
|
||||
assert.Contains(t, string(data), substr, "file %s should contain %q", path, substr)
|
||||
}
|
||||
|
||||
func countFiles(t *testing.T, dir string) int {
|
||||
t.Helper()
|
||||
entries, err := os.ReadDir(dir)
|
||||
require.NoError(t, err, "failed to read dir %s", dir)
|
||||
count := 0
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func waitForDir(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
if !waitForDirEventually(t, dir, 10*time.Second) {
|
||||
t.Fatalf("directory %s did not appear within 10s", dir)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForDirEventually(t *testing.T, dir string, timeout time.Duration) bool {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if _, err := os.Stat(dir); err == nil {
|
||||
return true
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func refreshDirEntry(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
parent := filepath.Dir(dir)
|
||||
_, _ = os.ReadDir(parent)
|
||||
}
|
||||
|
||||
func waitForBareRepoEventually(t *testing.T, bareRepo string, timeout time.Duration) bool {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if isBareRepo(bareRepo) {
|
||||
return true
|
||||
}
|
||||
refreshDirEntry(t, bareRepo)
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isBareRepo(bareRepo string) bool {
|
||||
required := []string{
|
||||
filepath.Join(bareRepo, "HEAD"),
|
||||
filepath.Join(bareRepo, "config"),
|
||||
}
|
||||
for _, p := range required {
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ensureMountClone(t *testing.T, bareRepo, mountClone string) {
|
||||
t.Helper()
|
||||
if _, err := os.Stat(mountClone); err == nil {
|
||||
return
|
||||
} else if !os.IsNotExist(err) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
t.Logf("mount clone missing, re-cloning from %s", bareRepo)
|
||||
gitRun(t, "", "clone", bareRepo, mountClone)
|
||||
}
|
||||
|
||||
var gitRepoPathRe = regexp.MustCompile(`'([^']+)' does not appear to be a git repository`)
|
||||
|
||||
func extractGitRepoPath(output string) string {
|
||||
if match := gitRepoPathRe.FindStringSubmatch(output); len(match) > 1 {
|
||||
return match[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func leftPad(n, width int) string {
|
||||
s := strconv.Itoa(n)
|
||||
for len(s) < width {
|
||||
s = "0" + s
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,765 @@
|
||||
package fuse_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// --- fcntl helpers (used by both test process and subprocess) ---
|
||||
|
||||
func fcntlSetLk(f *os.File, typ int16, start, len int64) error {
|
||||
flk := syscall.Flock_t{
|
||||
Type: typ,
|
||||
Whence: 0, // SEEK_SET
|
||||
Start: start,
|
||||
Len: len, // 0 means to EOF
|
||||
}
|
||||
return syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, &flk)
|
||||
}
|
||||
|
||||
func fcntlSetLkw(f *os.File, typ int16, start, len int64) error {
|
||||
flk := syscall.Flock_t{
|
||||
Type: typ,
|
||||
Whence: 0,
|
||||
Start: start,
|
||||
Len: len,
|
||||
}
|
||||
return syscall.FcntlFlock(f.Fd(), syscall.F_SETLKW, &flk)
|
||||
}
|
||||
|
||||
func fcntlGetLk(f *os.File, typ int16, start, len int64) (syscall.Flock_t, error) {
|
||||
flk := syscall.Flock_t{
|
||||
Type: typ,
|
||||
Whence: 0,
|
||||
Start: start,
|
||||
Len: len,
|
||||
}
|
||||
err := syscall.FcntlFlock(f.Fd(), syscall.F_GETLK, &flk)
|
||||
return flk, err
|
||||
}
|
||||
|
||||
// --- Subprocess entry point for fcntl lock tests ---
|
||||
//
|
||||
// POSIX fcntl locks are per-process (the kernel uses the process's files_struct
|
||||
// as lock owner). To test inter-process lock conflicts over FUSE, contending
|
||||
// locks must come from separate processes.
|
||||
//
|
||||
// Actions:
|
||||
// hold — acquire F_SETLK, print "LOCKED\n", wait for stdin close, exit
|
||||
// hold-blocking — print "WAITING\n", acquire F_SETLKW, print "LOCKED\n", wait for stdin close, exit
|
||||
// try — try F_SETLK, print "OK\n" or "EAGAIN\n", exit
|
||||
// getlk — do F_GETLK, print lock type as integer, exit
|
||||
|
||||
func TestFcntlLockHelper(t *testing.T) {
|
||||
action := os.Getenv("LOCK_ACTION")
|
||||
if action == "" {
|
||||
t.Skip("subprocess helper — not invoked directly")
|
||||
}
|
||||
|
||||
filePath := os.Getenv("LOCK_FILE")
|
||||
lockType, err := parseLockType(os.Getenv("LOCK_TYPE"))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "lock type: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
start, _ := strconv.ParseInt(os.Getenv("LOCK_START"), 10, 64)
|
||||
length, _ := strconv.ParseInt(os.Getenv("LOCK_LEN"), 10, 64)
|
||||
|
||||
f, err := os.OpenFile(filePath, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "open: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
switch action {
|
||||
case "hold":
|
||||
if err := fcntlSetLk(f, lockType, start, length); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "setlk: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("LOCKED")
|
||||
os.Stdout.Sync()
|
||||
io.ReadAll(os.Stdin) // block until parent closes pipe
|
||||
|
||||
case "hold-blocking":
|
||||
fmt.Println("WAITING")
|
||||
os.Stdout.Sync()
|
||||
if err := fcntlSetLkw(f, lockType, start, length); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "setlkw: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("LOCKED")
|
||||
os.Stdout.Sync()
|
||||
io.ReadAll(os.Stdin)
|
||||
|
||||
case "try":
|
||||
if err := fcntlSetLk(f, lockType, start, length); err != nil {
|
||||
if errors.Is(err, syscall.EAGAIN) || errors.Is(err, syscall.EACCES) {
|
||||
fmt.Println("EAGAIN")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "unexpected setlk error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("OK")
|
||||
}
|
||||
os.Stdout.Sync()
|
||||
|
||||
case "getlk":
|
||||
flk := syscall.Flock_t{Type: lockType, Whence: 0, Start: start, Len: length}
|
||||
if err := syscall.FcntlFlock(f.Fd(), syscall.F_GETLK, &flk); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "getlk: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(flk.Type)
|
||||
os.Stdout.Sync()
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown action: %s\n", action)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func parseLockType(s string) (int16, error) {
|
||||
switch s {
|
||||
case "F_RDLCK":
|
||||
return syscall.F_RDLCK, nil
|
||||
case "F_WRLCK":
|
||||
return syscall.F_WRLCK, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown lock type %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Subprocess coordination helpers ---
|
||||
|
||||
// lockHolder is a subprocess holding an fcntl lock. A background goroutine
|
||||
// reads stdout and signals the waiting/locked channels as the subprocess
|
||||
// progresses through the lock lifecycle.
|
||||
type lockHolder struct {
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
waiting chan struct{} // closed when subprocess prints "WAITING"
|
||||
locked chan struct{} // closed when subprocess prints "LOCKED"
|
||||
}
|
||||
|
||||
// startLockHolder launches a subprocess that acquires an fcntl lock and holds
|
||||
// it until Release is called.
|
||||
func startLockHolder(t *testing.T, path string, lockType string, start, length int64) *lockHolder {
|
||||
t.Helper()
|
||||
return startLockProcess(t, "hold", path, lockType, start, length)
|
||||
}
|
||||
|
||||
// startBlockingLockHolder launches a subprocess that tries F_SETLKW.
|
||||
// It blocks until the lock is available, then holds it until Release is called.
|
||||
func startBlockingLockHolder(t *testing.T, path string, lockType string, start, length int64) *lockHolder {
|
||||
t.Helper()
|
||||
return startLockProcess(t, "hold-blocking", path, lockType, start, length)
|
||||
}
|
||||
|
||||
func startLockProcess(t *testing.T, action, path, lockType string, start, length int64) *lockHolder {
|
||||
t.Helper()
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^TestFcntlLockHelper$")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"LOCK_ACTION="+action,
|
||||
"LOCK_FILE="+path,
|
||||
"LOCK_TYPE="+lockType,
|
||||
fmt.Sprintf("LOCK_START=%d", start),
|
||||
fmt.Sprintf("LOCK_LEN=%d", length),
|
||||
)
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
require.NoError(t, err)
|
||||
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd.Stderr = os.Stderr
|
||||
require.NoError(t, cmd.Start())
|
||||
|
||||
waiting := make(chan struct{})
|
||||
locked := make(chan struct{})
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdoutPipe)
|
||||
for scanner.Scan() {
|
||||
switch strings.TrimSpace(scanner.Text()) {
|
||||
case "WAITING":
|
||||
select {
|
||||
case <-waiting:
|
||||
default:
|
||||
close(waiting)
|
||||
}
|
||||
case "LOCKED":
|
||||
select {
|
||||
case <-locked:
|
||||
default:
|
||||
close(locked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return &lockHolder{cmd: cmd, stdin: stdin, waiting: waiting, locked: locked}
|
||||
}
|
||||
|
||||
// WaitWaiting waits for the subprocess to print "WAITING" before trying to
|
||||
// assert that a blocking lock request is still blocked.
|
||||
func (h *lockHolder) WaitWaiting(t *testing.T, timeout time.Duration) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-h.waiting:
|
||||
// OK
|
||||
case <-time.After(timeout):
|
||||
t.Fatal("timed out waiting for subprocess to start blocking lock attempt")
|
||||
}
|
||||
}
|
||||
|
||||
// WaitLocked waits for the subprocess to acquire the lock and signal "LOCKED".
|
||||
func (h *lockHolder) WaitLocked(t *testing.T, timeout time.Duration) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-h.locked:
|
||||
// OK
|
||||
case <-time.After(timeout):
|
||||
t.Fatal("timed out waiting for subprocess to acquire lock")
|
||||
}
|
||||
}
|
||||
|
||||
// IsLocked checks whether the subprocess has signaled "LOCKED" within 200ms.
|
||||
func (h *lockHolder) IsLocked() bool {
|
||||
select {
|
||||
case <-h.waiting:
|
||||
// The subprocess has reached the blocking lock attempt.
|
||||
case <-h.locked:
|
||||
return true
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
return false
|
||||
}
|
||||
|
||||
select {
|
||||
case <-h.locked:
|
||||
return true
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Release signals the subprocess to exit and waits for it.
|
||||
func (h *lockHolder) Release(t *testing.T) {
|
||||
t.Helper()
|
||||
h.stdin.Close()
|
||||
_ = h.cmd.Wait()
|
||||
}
|
||||
|
||||
// tryLockInSubprocess tries a non-blocking fcntl lock in a subprocess.
|
||||
// Returns nil if the lock was acquired, syscall.EAGAIN if it was denied.
|
||||
func tryLockInSubprocess(t *testing.T, path, lockType string, start, length int64) error {
|
||||
t.Helper()
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^TestFcntlLockHelper$")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"LOCK_ACTION=try",
|
||||
"LOCK_FILE="+path,
|
||||
"LOCK_TYPE="+lockType,
|
||||
fmt.Sprintf("LOCK_START=%d", start),
|
||||
fmt.Sprintf("LOCK_LEN=%d", length),
|
||||
)
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
out, err := cmd.Output()
|
||||
require.NoError(t, err, "subprocess failed to run")
|
||||
|
||||
result := strings.TrimSpace(string(out))
|
||||
if result == "EAGAIN" {
|
||||
return syscall.EAGAIN
|
||||
}
|
||||
require.Equal(t, "OK", result, "unexpected subprocess output")
|
||||
return nil
|
||||
}
|
||||
|
||||
// getLkInSubprocess does F_GETLK in a subprocess and returns the lock type.
|
||||
func getLkInSubprocess(t *testing.T, path, lockType string, start, length int64) int16 {
|
||||
t.Helper()
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^TestFcntlLockHelper$")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"LOCK_ACTION=getlk",
|
||||
"LOCK_FILE="+path,
|
||||
"LOCK_TYPE="+lockType,
|
||||
fmt.Sprintf("LOCK_START=%d", start),
|
||||
fmt.Sprintf("LOCK_LEN=%d", length),
|
||||
)
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
out, err := cmd.Output()
|
||||
require.NoError(t, err, "subprocess failed to run")
|
||||
|
||||
val, err := strconv.ParseInt(strings.TrimSpace(string(out)), 10, 16)
|
||||
require.NoError(t, err, "failed to parse lock type from subprocess")
|
||||
return int16(val)
|
||||
}
|
||||
|
||||
// --- Test runner ---
|
||||
|
||||
func TestPosixFileLocking(t *testing.T) {
|
||||
framework := NewFuseTestFramework(t, DefaultTestConfig())
|
||||
defer framework.Cleanup()
|
||||
|
||||
require.NoError(t, framework.Setup(DefaultTestConfig()))
|
||||
|
||||
t.Run("FlockExclusive", func(t *testing.T) {
|
||||
testFlockExclusive(t, framework)
|
||||
})
|
||||
t.Run("FlockShared", func(t *testing.T) {
|
||||
testFlockShared(t, framework)
|
||||
})
|
||||
t.Run("FlockUpgrade", func(t *testing.T) {
|
||||
testFlockUpgrade(t, framework)
|
||||
})
|
||||
t.Run("FlockReleaseOnClose", func(t *testing.T) {
|
||||
testFlockReleaseOnClose(t, framework)
|
||||
})
|
||||
t.Run("FcntlWriteLockConflict", func(t *testing.T) {
|
||||
testFcntlWriteLockConflict(t, framework)
|
||||
})
|
||||
t.Run("FcntlReadLocksShared", func(t *testing.T) {
|
||||
testFcntlReadLocksShared(t, framework)
|
||||
})
|
||||
t.Run("FcntlGetLk", func(t *testing.T) {
|
||||
testFcntlGetLk(t, framework)
|
||||
})
|
||||
t.Run("FcntlByteRangePartial", func(t *testing.T) {
|
||||
testFcntlByteRangePartial(t, framework)
|
||||
})
|
||||
t.Run("FcntlSetLkwBlocks", func(t *testing.T) {
|
||||
testFcntlSetLkwBlocks(t, framework)
|
||||
})
|
||||
t.Run("FcntlReleaseOnClose", func(t *testing.T) {
|
||||
testFcntlReleaseOnClose(t, framework)
|
||||
})
|
||||
t.Run("ConcurrentLockContention", func(t *testing.T) {
|
||||
testConcurrentLockContention(t, framework)
|
||||
})
|
||||
}
|
||||
|
||||
// --- flock() tests (same-process is fine — flock uses per-fd owners) ---
|
||||
|
||||
func testFlockExclusive(t *testing.T, fw *FuseTestFramework) {
|
||||
path := filepath.Join(fw.GetMountPoint(), "flock_exclusive.txt")
|
||||
require.NoError(t, os.WriteFile(path, []byte("data"), 0644))
|
||||
|
||||
f1, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
defer f1.Close()
|
||||
|
||||
f2, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
defer f2.Close()
|
||||
|
||||
require.NoError(t, syscall.Flock(int(f1.Fd()), syscall.LOCK_EX|syscall.LOCK_NB))
|
||||
|
||||
err = syscall.Flock(int(f2.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
|
||||
assert.ErrorIs(t, err, syscall.EWOULDBLOCK, "second exclusive flock should fail")
|
||||
|
||||
require.NoError(t, syscall.Flock(int(f1.Fd()), syscall.LOCK_UN))
|
||||
require.NoError(t, syscall.Flock(int(f2.Fd()), syscall.LOCK_EX|syscall.LOCK_NB))
|
||||
require.NoError(t, syscall.Flock(int(f2.Fd()), syscall.LOCK_UN))
|
||||
}
|
||||
|
||||
func testFlockShared(t *testing.T, fw *FuseTestFramework) {
|
||||
path := filepath.Join(fw.GetMountPoint(), "flock_shared.txt")
|
||||
require.NoError(t, os.WriteFile(path, []byte("data"), 0644))
|
||||
|
||||
f1, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
defer f1.Close()
|
||||
|
||||
f2, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
defer f2.Close()
|
||||
|
||||
require.NoError(t, syscall.Flock(int(f1.Fd()), syscall.LOCK_SH|syscall.LOCK_NB))
|
||||
require.NoError(t, syscall.Flock(int(f2.Fd()), syscall.LOCK_SH|syscall.LOCK_NB))
|
||||
|
||||
f3, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
defer f3.Close()
|
||||
err = syscall.Flock(int(f3.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
|
||||
assert.ErrorIs(t, err, syscall.EWOULDBLOCK)
|
||||
|
||||
require.NoError(t, syscall.Flock(int(f1.Fd()), syscall.LOCK_UN))
|
||||
require.NoError(t, syscall.Flock(int(f2.Fd()), syscall.LOCK_UN))
|
||||
}
|
||||
|
||||
func testFlockUpgrade(t *testing.T, fw *FuseTestFramework) {
|
||||
path := filepath.Join(fw.GetMountPoint(), "flock_upgrade.txt")
|
||||
require.NoError(t, os.WriteFile(path, []byte("data"), 0644))
|
||||
|
||||
f1, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
defer f1.Close()
|
||||
|
||||
require.NoError(t, syscall.Flock(int(f1.Fd()), syscall.LOCK_SH|syscall.LOCK_NB))
|
||||
require.NoError(t, syscall.Flock(int(f1.Fd()), syscall.LOCK_EX|syscall.LOCK_NB))
|
||||
|
||||
f2, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
defer f2.Close()
|
||||
err = syscall.Flock(int(f2.Fd()), syscall.LOCK_SH|syscall.LOCK_NB)
|
||||
assert.ErrorIs(t, err, syscall.EWOULDBLOCK)
|
||||
|
||||
require.NoError(t, syscall.Flock(int(f1.Fd()), syscall.LOCK_UN))
|
||||
}
|
||||
|
||||
func testFlockReleaseOnClose(t *testing.T, fw *FuseTestFramework) {
|
||||
path := filepath.Join(fw.GetMountPoint(), "flock_close.txt")
|
||||
require.NoError(t, os.WriteFile(path, []byte("data"), 0644))
|
||||
|
||||
f1, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, syscall.Flock(int(f1.Fd()), syscall.LOCK_EX|syscall.LOCK_NB))
|
||||
f1.Close()
|
||||
|
||||
f2, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
defer f2.Close()
|
||||
require.NoError(t, syscall.Flock(int(f2.Fd()), syscall.LOCK_EX|syscall.LOCK_NB))
|
||||
require.NoError(t, syscall.Flock(int(f2.Fd()), syscall.LOCK_UN))
|
||||
}
|
||||
|
||||
// --- fcntl() POSIX lock tests (use subprocesses for inter-process semantics) ---
|
||||
|
||||
// testFcntlWriteLockConflict: test process holds a write lock, subprocess
|
||||
// contender is denied, then succeeds after release.
|
||||
func testFcntlWriteLockConflict(t *testing.T, fw *FuseTestFramework) {
|
||||
path := filepath.Join(fw.GetMountPoint(), "fcntl_write.txt")
|
||||
require.NoError(t, os.WriteFile(path, []byte("0123456789"), 0644))
|
||||
|
||||
f1, err := os.OpenFile(path, os.O_RDWR, 0)
|
||||
require.NoError(t, err)
|
||||
defer f1.Close()
|
||||
|
||||
// Test process locks [0, 10).
|
||||
require.NoError(t, fcntlSetLk(f1, syscall.F_WRLCK, 0, 10))
|
||||
|
||||
// Subprocess: overlapping write lock should fail with EAGAIN.
|
||||
err = tryLockInSubprocess(t, path, "F_WRLCK", 5, 10)
|
||||
assert.ErrorIs(t, err, syscall.EAGAIN, "overlapping write lock should fail with EAGAIN")
|
||||
|
||||
// Unlock, then subprocess should succeed.
|
||||
require.NoError(t, fcntlSetLk(f1, syscall.F_UNLCK, 0, 10))
|
||||
|
||||
err = tryLockInSubprocess(t, path, "F_WRLCK", 5, 10)
|
||||
assert.NoError(t, err, "lock should succeed after release")
|
||||
}
|
||||
|
||||
// testFcntlReadLocksShared: test process holds a read lock, subprocess read
|
||||
// lock succeeds (shared), subprocess write lock is denied.
|
||||
func testFcntlReadLocksShared(t *testing.T, fw *FuseTestFramework) {
|
||||
path := filepath.Join(fw.GetMountPoint(), "fcntl_shared.txt")
|
||||
require.NoError(t, os.WriteFile(path, []byte("0123456789"), 0644))
|
||||
|
||||
f1, err := os.OpenFile(path, os.O_RDWR, 0)
|
||||
require.NoError(t, err)
|
||||
defer f1.Close()
|
||||
|
||||
// Test process holds a read lock.
|
||||
require.NoError(t, fcntlSetLk(f1, syscall.F_RDLCK, 0, 10))
|
||||
|
||||
// Subprocess: read lock should succeed (shared).
|
||||
err = tryLockInSubprocess(t, path, "F_RDLCK", 0, 10)
|
||||
assert.NoError(t, err, "shared read lock should succeed")
|
||||
|
||||
// Subprocess: write lock should fail.
|
||||
err = tryLockInSubprocess(t, path, "F_WRLCK", 0, 10)
|
||||
assert.ErrorIs(t, err, syscall.EAGAIN, "write lock should fail with EAGAIN while read lock is held")
|
||||
|
||||
require.NoError(t, fcntlSetLk(f1, syscall.F_UNLCK, 0, 10))
|
||||
}
|
||||
|
||||
// testFcntlGetLk: test process holds a write lock, subprocess queries via
|
||||
// F_GETLK and sees the conflict.
|
||||
func testFcntlGetLk(t *testing.T, fw *FuseTestFramework) {
|
||||
path := filepath.Join(fw.GetMountPoint(), "fcntl_getlk.txt")
|
||||
require.NoError(t, os.WriteFile(path, []byte("0123456789"), 0644))
|
||||
|
||||
f1, err := os.OpenFile(path, os.O_RDWR, 0)
|
||||
require.NoError(t, err)
|
||||
defer f1.Close()
|
||||
|
||||
// Test process takes a write lock on [0, 5).
|
||||
require.NoError(t, fcntlSetLk(f1, syscall.F_WRLCK, 0, 5))
|
||||
|
||||
// Subprocess: F_GETLK on overlapping range should report conflict.
|
||||
typ := getLkInSubprocess(t, path, "F_WRLCK", 0, 10)
|
||||
assert.NotEqual(t, int16(syscall.F_UNLCK), typ, "should report a conflict")
|
||||
|
||||
// Subprocess: F_GETLK on non-overlapping range should report no conflict.
|
||||
typ = getLkInSubprocess(t, path, "F_WRLCK", 5, 5)
|
||||
assert.Equal(t, int16(syscall.F_UNLCK), typ, "no conflict expected on non-overlapping range")
|
||||
|
||||
require.NoError(t, fcntlSetLk(f1, syscall.F_UNLCK, 0, 5))
|
||||
}
|
||||
|
||||
// testFcntlByteRangePartial: test process and subprocess lock non-overlapping
|
||||
// ranges independently; extending into the other's range is denied.
|
||||
func testFcntlByteRangePartial(t *testing.T, fw *FuseTestFramework) {
|
||||
path := filepath.Join(fw.GetMountPoint(), "fcntl_range.txt")
|
||||
require.NoError(t, os.WriteFile(path, []byte("0123456789ABCDEF"), 0644))
|
||||
|
||||
f1, err := os.OpenFile(path, os.O_RDWR, 0)
|
||||
require.NoError(t, err)
|
||||
defer f1.Close()
|
||||
|
||||
// Test process locks [0, 8).
|
||||
require.NoError(t, fcntlSetLk(f1, syscall.F_WRLCK, 0, 8))
|
||||
|
||||
// Subprocess locks [8, 16) — non-overlapping, should succeed.
|
||||
holder := startLockHolder(t, path, "F_WRLCK", 8, 8)
|
||||
holder.WaitLocked(t, 5*time.Second)
|
||||
|
||||
// Subprocess: extending into test process's range should fail.
|
||||
err = tryLockInSubprocess(t, path, "F_WRLCK", 4, 8)
|
||||
assert.ErrorIs(t, err, syscall.EAGAIN, "extending into another process's locked range should fail with EAGAIN")
|
||||
|
||||
holder.Release(t)
|
||||
require.NoError(t, fcntlSetLk(f1, syscall.F_UNLCK, 0, 8))
|
||||
}
|
||||
|
||||
// testFcntlSetLkwBlocks: test process holds exclusive lock, subprocess blocks
|
||||
// on F_SETLKW, then unblocks when lock is released.
|
||||
func testFcntlSetLkwBlocks(t *testing.T, fw *FuseTestFramework) {
|
||||
path := filepath.Join(fw.GetMountPoint(), "fcntl_setlkw.txt")
|
||||
require.NoError(t, os.WriteFile(path, []byte("data"), 0644))
|
||||
|
||||
f1, err := os.OpenFile(path, os.O_RDWR, 0)
|
||||
require.NoError(t, err)
|
||||
defer f1.Close()
|
||||
|
||||
// Test process takes exclusive lock.
|
||||
require.NoError(t, fcntlSetLk(f1, syscall.F_WRLCK, 0, 0))
|
||||
|
||||
// Launch subprocess that will block on F_SETLKW.
|
||||
blocker := startBlockingLockHolder(t, path, "F_WRLCK", 0, 0)
|
||||
|
||||
// Verify it has reached the blocking lock attempt but has not acquired it yet.
|
||||
blocker.WaitWaiting(t, 5*time.Second)
|
||||
assert.False(t, blocker.IsLocked(), "F_SETLKW should be blocking")
|
||||
|
||||
// Release our lock to unblock the subprocess.
|
||||
require.NoError(t, fcntlSetLk(f1, syscall.F_UNLCK, 0, 0))
|
||||
|
||||
// Subprocess should now acquire and signal "LOCKED".
|
||||
blocker.WaitLocked(t, 5*time.Second)
|
||||
blocker.Release(t)
|
||||
}
|
||||
|
||||
// testFcntlReleaseOnClose: subprocess holds a lock then exits (fd closes),
|
||||
// test process verifies it can now acquire the lock.
|
||||
func testFcntlReleaseOnClose(t *testing.T, fw *FuseTestFramework) {
|
||||
path := filepath.Join(fw.GetMountPoint(), "fcntl_close.txt")
|
||||
require.NoError(t, os.WriteFile(path, []byte("data"), 0644))
|
||||
|
||||
// Subprocess acquires exclusive lock and holds it.
|
||||
holder := startLockHolder(t, path, "F_WRLCK", 0, 0)
|
||||
holder.WaitLocked(t, 5*time.Second)
|
||||
|
||||
// Another subprocess trying to lock should be denied while holder is active.
|
||||
err := tryLockInSubprocess(t, path, "F_WRLCK", 0, 0)
|
||||
assert.ErrorIs(t, err, syscall.EAGAIN, "lock should be held by subprocess")
|
||||
|
||||
// Release subprocess → fd closes → lock released.
|
||||
// The FUSE server may not process the Release immediately, so retry.
|
||||
holder.Release(t)
|
||||
|
||||
f2, err := os.OpenFile(path, os.O_RDWR, 0)
|
||||
require.NoError(t, err)
|
||||
defer f2.Close()
|
||||
var lockErr error
|
||||
for attempt := 0; attempt < 50; attempt++ {
|
||||
lockErr = fcntlSetLk(f2, syscall.F_WRLCK, 0, 0)
|
||||
if lockErr == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
require.NoError(t, lockErr, "lock should succeed after subprocess exits")
|
||||
require.NoError(t, fcntlSetLk(f2, syscall.F_UNLCK, 0, 0))
|
||||
}
|
||||
|
||||
// --- Concurrency test (uses flock which has per-fd owners) ---
|
||||
|
||||
// testConcurrentLockContention has multiple goroutines competing for an
|
||||
// exclusive flock, each appending to the file while holding it.
|
||||
// Uses non-blocking flock with retry to avoid exhausting the go-fuse server's
|
||||
// reader goroutines (blocking FUSE SETLKW can starve unlock processing).
|
||||
func testConcurrentLockContention(t *testing.T, fw *FuseTestFramework) {
|
||||
path := filepath.Join(fw.GetMountPoint(), "lock_contention.txt")
|
||||
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
numWorkers := 8
|
||||
writesPerWorker := 5
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
var errs []error
|
||||
var activeHolders atomic.Int32
|
||||
var nextSlot atomic.Int64
|
||||
const recordFormat = "worker %02d write %02d\n"
|
||||
recordSize := len(fmt.Sprintf(recordFormat, 0, 0))
|
||||
|
||||
addError := func(err error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
openWithRetry := func(flags int) (*os.File, error) {
|
||||
var openErr error
|
||||
for attempt := 0; attempt < 400; attempt++ {
|
||||
file, err := os.OpenFile(path, flags, 0)
|
||||
if err == nil {
|
||||
return file, nil
|
||||
}
|
||||
openErr = err
|
||||
if !errors.Is(err, os.ErrNotExist) && !errors.Is(err, syscall.ENOENT) {
|
||||
return nil, err
|
||||
}
|
||||
// Refresh parent directory entries in case the mount cache is stale.
|
||||
_, _ = os.ReadDir(filepath.Dir(path))
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
return nil, openErr
|
||||
}
|
||||
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
f, err := openWithRetry(os.O_RDWR)
|
||||
if err != nil {
|
||||
addError(fmt.Errorf("worker %d open: %v", id, err))
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
for j := 0; j < writesPerWorker; j++ {
|
||||
// Non-blocking flock with retry to avoid FUSE server thread starvation.
|
||||
locked := false
|
||||
for attempt := 0; attempt < 3000; attempt++ {
|
||||
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err == nil {
|
||||
locked = true
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if !locked {
|
||||
addError(fmt.Errorf("worker %d: failed to acquire flock after retries", id))
|
||||
return
|
||||
}
|
||||
|
||||
if holders := activeHolders.Add(1); holders != 1 {
|
||||
activeHolders.Add(-1)
|
||||
syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
|
||||
addError(fmt.Errorf("worker %d: flock overlap detected with %d holders", id, holders))
|
||||
return
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
msg := fmt.Sprintf(recordFormat, id, j)
|
||||
if len(msg) != recordSize {
|
||||
activeHolders.Add(-1)
|
||||
syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
|
||||
addError(fmt.Errorf("worker %d: unexpected record size %d", id, len(msg)))
|
||||
return
|
||||
}
|
||||
|
||||
slot := nextSlot.Add(1) - 1
|
||||
offset := slot * int64(recordSize)
|
||||
n, err := f.WriteAt([]byte(msg), offset)
|
||||
if err != nil {
|
||||
activeHolders.Add(-1)
|
||||
syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
|
||||
addError(fmt.Errorf("worker %d write: %v", id, err))
|
||||
return
|
||||
}
|
||||
if n != len(msg) {
|
||||
activeHolders.Add(-1)
|
||||
syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
|
||||
addError(fmt.Errorf("worker %d short write: wrote %d of %d bytes", id, n, len(msg)))
|
||||
return
|
||||
}
|
||||
|
||||
activeHolders.Add(-1)
|
||||
syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
if runtime.GOOS == "darwin" {
|
||||
for _, err := range errs {
|
||||
if err != nil && strings.Contains(err.Error(), "no such file or directory") {
|
||||
t.Skip("lock contention file disappeared on darwin FUSE; skipping flaky check")
|
||||
}
|
||||
}
|
||||
}
|
||||
require.Empty(t, errs, "concurrent lock contention errors: %v", errs)
|
||||
|
||||
flush := func() {
|
||||
verify, err := openWithRetry(os.O_RDWR)
|
||||
if err != nil && runtime.GOOS == "darwin" && strings.Contains(err.Error(), "no such file or directory") {
|
||||
t.Skip("lock contention file disappeared on darwin FUSE; skipping flaky check")
|
||||
}
|
||||
require.NoError(t, err)
|
||||
defer verify.Close()
|
||||
require.NoError(t, verify.Sync())
|
||||
}
|
||||
flush()
|
||||
|
||||
expectedLines := numWorkers * writesPerWorker
|
||||
expectedBytes := expectedLines * recordSize
|
||||
var data []byte
|
||||
require.Eventually(t, func() bool {
|
||||
verify, err := openWithRetry(os.O_RDONLY)
|
||||
if err != nil {
|
||||
if runtime.GOOS == "darwin" && strings.Contains(err.Error(), "no such file or directory") {
|
||||
t.Skip("lock contention file disappeared on darwin FUSE; skipping flaky check")
|
||||
}
|
||||
return false
|
||||
}
|
||||
defer verify.Close()
|
||||
|
||||
data, err = io.ReadAll(verify)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return len(data) == expectedBytes
|
||||
}, 15*time.Second, 100*time.Millisecond, "file should eventually contain exactly %d records from all workers", expectedLines)
|
||||
actualLines := bytes.Count(data, []byte("\n"))
|
||||
assert.Equal(t, expectedLines, actualLines,
|
||||
"file should contain exactly %d lines from all workers", expectedLines)
|
||||
}
|
||||
@@ -27,17 +27,19 @@ func TestOffsetManagement(t *testing.T) {
|
||||
t.Logf("Running offset management tests with mock backend - offsets are in-memory only")
|
||||
}
|
||||
|
||||
topic := testutil.GenerateUniqueTopicName("offset-management")
|
||||
topic1 := testutil.GenerateUniqueTopicName("offset-management")
|
||||
topic2 := testutil.GenerateUniqueTopicName("offset-resumption")
|
||||
groupID := testutil.GenerateUniqueGroupID("offset-test-group")
|
||||
|
||||
gateway.AddTestTopic(topic)
|
||||
gateway.AddTestTopic(topic1)
|
||||
gateway.AddTestTopic(topic2)
|
||||
|
||||
t.Run("BasicOffsetCommitFetch", func(t *testing.T) {
|
||||
testBasicOffsetCommitFetch(t, addr, topic, groupID)
|
||||
testBasicOffsetCommitFetch(t, addr, topic1, groupID)
|
||||
})
|
||||
|
||||
t.Run("ConsumerGroupResumption", func(t *testing.T) {
|
||||
testConsumerGroupResumption(t, addr, topic, groupID+"2")
|
||||
testConsumerGroupResumption(t, addr, topic2, groupID+"2")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+141
-4
@@ -7,6 +7,14 @@ SEAWEEDFS_S3_ENDPOINT ?= http://127.0.0.1:8333
|
||||
TEST_TIMEOUT ?= 5m
|
||||
DOCKER_COMPOSE ?= docker-compose
|
||||
|
||||
# CI configuration (build weed from source, run natively)
|
||||
SEAWEEDFS_BINARY ?= weed
|
||||
S3_PORT ?= 8333
|
||||
ACCESS_KEY ?= some_access_key1
|
||||
SECRET_KEY ?= some_secret_key1
|
||||
TEST_DIR := $(shell pwd)
|
||||
SEAWEEDFS_ROOT := $(shell cd ../.. && pwd)
|
||||
|
||||
# Colors for output
|
||||
BLUE := \033[36m
|
||||
GREEN := \033[32m
|
||||
@@ -14,7 +22,10 @@ YELLOW := \033[33m
|
||||
RED := \033[31m
|
||||
NC := \033[0m # No Color
|
||||
|
||||
.PHONY: help setup test test-unit test-integration test-e2e clean logs status
|
||||
.PHONY: help setup test test-unit test-integration test-e2e clean logs status \
|
||||
build-weed check-binary start-openbao-ci stop-openbao-ci \
|
||||
start-seaweedfs-ci stop-seaweedfs-ci clean-ci \
|
||||
test-provider-ci test-s3-kms-ci
|
||||
|
||||
help: ## Show this help message
|
||||
@echo "$(BLUE)SeaweedFS KMS Integration Testing$(NC)"
|
||||
@@ -124,16 +135,142 @@ check-env: ## Check test environment setup
|
||||
@echo "TEST_TIMEOUT: $(TEST_TIMEOUT)"
|
||||
@make install-deps
|
||||
|
||||
# CI targets
|
||||
ci-test: ## Run tests in CI environment
|
||||
# CI targets (docker-compose based)
|
||||
ci-test: ## Run tests in CI environment (docker-compose)
|
||||
@echo "$(YELLOW)Running CI tests...$(NC)"
|
||||
@make setup
|
||||
@make test-unit
|
||||
@make test-integration
|
||||
@make clean
|
||||
|
||||
ci-e2e: ## Run end-to-end tests in CI
|
||||
ci-e2e: ## Run end-to-end tests in CI (docker-compose)
|
||||
@echo "$(YELLOW)Running CI end-to-end tests...$(NC)"
|
||||
@make setup-seaweedfs
|
||||
@make test-e2e
|
||||
@make clean
|
||||
|
||||
####################################################
|
||||
# GitHub Actions CI targets (build weed from source)
|
||||
####################################################
|
||||
|
||||
build-weed: ## Build SeaweedFS binary from source
|
||||
@echo "Building SeaweedFS binary..."
|
||||
@cd $(SEAWEEDFS_ROOT)/weed && go install -buildvcs=false
|
||||
@echo "$(GREEN)SeaweedFS binary built successfully$(NC)"
|
||||
|
||||
check-binary:
|
||||
@if ! command -v $(SEAWEEDFS_BINARY) > /dev/null 2>&1; then \
|
||||
echo "$(RED)Error: SeaweedFS binary '$(SEAWEEDFS_BINARY)' not found in PATH$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
start-openbao-ci: ## Start OpenBao via Docker for CI
|
||||
@echo "Starting OpenBao for CI..."
|
||||
@docker rm -f openbao-ci 2>/dev/null || true
|
||||
@docker run -d --name openbao-ci \
|
||||
-p 8200:8200 \
|
||||
-e BAO_DEV_ROOT_TOKEN_ID=$(OPENBAO_TOKEN) \
|
||||
-e BAO_DEV_LISTEN_ADDRESS=0.0.0.0:8200 \
|
||||
ghcr.io/openbao/openbao:latest \
|
||||
bao server -dev -dev-root-token-id=$(OPENBAO_TOKEN) -dev-listen-address=0.0.0.0:8200
|
||||
@echo "Waiting for OpenBao to be ready..."
|
||||
@for i in $$(seq 1 30); do \
|
||||
if curl -s $(OPENBAO_ADDR)/v1/sys/health > /dev/null 2>&1; then \
|
||||
echo "$(GREEN)OpenBao is ready$(NC)"; \
|
||||
break; \
|
||||
fi; \
|
||||
if [ $$i -eq 30 ]; then \
|
||||
echo "$(RED)Timeout waiting for OpenBao$(NC)"; \
|
||||
docker logs openbao-ci 2>&1 | tail -20; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
sleep 2; \
|
||||
done
|
||||
@chmod +x setup_openbao.sh
|
||||
@OPENBAO_ADDR=$(OPENBAO_ADDR) OPENBAO_TOKEN=$(OPENBAO_TOKEN) ./setup_openbao.sh
|
||||
|
||||
stop-openbao-ci: ## Stop OpenBao CI container
|
||||
@echo "Stopping OpenBao..."
|
||||
@docker stop openbao-ci 2>/dev/null || true
|
||||
@docker rm openbao-ci 2>/dev/null || true
|
||||
|
||||
start-seaweedfs-ci: check-binary ## Start weed mini with OpenBao KMS config
|
||||
@echo "Starting SeaweedFS with OpenBao KMS..."
|
||||
@mkdir -p /tmp/seaweedfs-test-kms
|
||||
@rm -f /tmp/seaweedfs-kms-*.log || true
|
||||
@sed -e 's/ACCESS_KEY_PLACEHOLDER/$(ACCESS_KEY)/g' \
|
||||
-e 's/SECRET_KEY_PLACEHOLDER/$(SECRET_KEY)/g' \
|
||||
-e 's|OPENBAO_ADDR_PLACEHOLDER|$(OPENBAO_ADDR)|g' \
|
||||
-e 's/OPENBAO_TOKEN_PLACEHOLDER/$(OPENBAO_TOKEN)/g' \
|
||||
s3-config-openbao-template.json > /tmp/seaweedfs-kms-s3.json
|
||||
@AWS_ACCESS_KEY_ID=$(ACCESS_KEY) AWS_SECRET_ACCESS_KEY=$(SECRET_KEY) GLOG_v=4 $(SEAWEEDFS_BINARY) mini \
|
||||
-dir=/tmp/seaweedfs-test-kms \
|
||||
-s3.port=$(S3_PORT) \
|
||||
-s3.config=/tmp/seaweedfs-kms-s3.json \
|
||||
-ip=127.0.0.1 \
|
||||
> /tmp/seaweedfs-kms-mini.log 2>&1 & echo $$! > /tmp/weed-kms-mini.pid
|
||||
@echo "Checking S3 service is ready..."
|
||||
@for i in $$(seq 1 30); do \
|
||||
if curl -s http://127.0.0.1:$(S3_PORT) > /dev/null 2>&1; then \
|
||||
echo "$(GREEN)S3 service is ready$(NC)"; \
|
||||
break; \
|
||||
fi; \
|
||||
if [ $$i -eq 30 ]; then \
|
||||
echo "$(RED)Timeout waiting for S3 service$(NC)"; \
|
||||
tail -20 /tmp/seaweedfs-kms-mini.log 2>/dev/null || true; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
sleep 1; \
|
||||
done
|
||||
|
||||
stop-seaweedfs-ci: ## Stop weed mini (CI-safe)
|
||||
@echo "Stopping SeaweedFS..."
|
||||
@if [ -f /tmp/weed-kms-mini.pid ]; then \
|
||||
kill $$(cat /tmp/weed-kms-mini.pid) 2>/dev/null || true; \
|
||||
rm -f /tmp/weed-kms-mini.pid; \
|
||||
fi
|
||||
@if command -v lsof >/dev/null 2>&1; then \
|
||||
lsof -ti :$(S3_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
fi
|
||||
@sleep 2
|
||||
|
||||
clean-ci: ## Clean up CI test artifacts
|
||||
@rm -rf /tmp/seaweedfs-test-kms
|
||||
@rm -f /tmp/seaweedfs-kms-*.log /tmp/seaweedfs-kms-s3.json /tmp/weed-kms-mini.pid
|
||||
|
||||
# Run KMS provider Go integration tests (GitHub Actions compatible)
|
||||
test-provider-ci: build-weed ## Run KMS provider tests with OpenBao in CI
|
||||
@echo "$(YELLOW)Starting KMS provider integration tests...$(NC)"
|
||||
@if $(MAKE) start-openbao-ci > /tmp/openbao-ci-setup.log 2>&1; then \
|
||||
echo "$(GREEN)OpenBao started and configured$(NC)"; \
|
||||
trap '$(MAKE) -C $(TEST_DIR) stop-openbao-ci || true' EXIT; \
|
||||
cd $(SEAWEEDFS_ROOT) && go test -v -timeout=$(TEST_TIMEOUT) ./test/kms/... || exit 1; \
|
||||
echo "$(GREEN)KMS provider tests completed successfully$(NC)"; \
|
||||
else \
|
||||
echo "$(RED)Failed to start OpenBao$(NC)"; \
|
||||
cat /tmp/openbao-ci-setup.log 2>/dev/null || true; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Run S3 KMS end-to-end tests (GitHub Actions compatible)
|
||||
test-s3-kms-ci: build-weed ## Run S3 KMS e2e tests with weed mini + OpenBao in CI
|
||||
@echo "$(YELLOW)Starting S3 KMS end-to-end tests...$(NC)"
|
||||
@if $(MAKE) start-openbao-ci > /tmp/openbao-ci-setup.log 2>&1; then \
|
||||
echo "$(GREEN)OpenBao started and configured$(NC)"; \
|
||||
trap '$(MAKE) -C $(TEST_DIR) stop-seaweedfs-ci || true; $(MAKE) -C $(TEST_DIR) stop-openbao-ci || true; $(MAKE) -C $(TEST_DIR) clean-ci || true' EXIT; \
|
||||
if $(MAKE) start-seaweedfs-ci > /tmp/weed-kms-ci-setup.log 2>&1; then \
|
||||
echo "$(GREEN)SeaweedFS started with OpenBao KMS$(NC)"; \
|
||||
sleep 3; \
|
||||
chmod +x test_s3_kms.sh; \
|
||||
SEAWEEDFS_S3_ENDPOINT=http://127.0.0.1:$(S3_PORT) ACCESS_KEY=$(ACCESS_KEY) SECRET_KEY=$(SECRET_KEY) ./test_s3_kms.sh || exit 1; \
|
||||
echo "$(GREEN)S3 KMS e2e tests completed successfully$(NC)"; \
|
||||
else \
|
||||
echo "$(RED)Failed to start SeaweedFS$(NC)"; \
|
||||
cat /tmp/weed-kms-ci-setup.log 2>/dev/null || true; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
else \
|
||||
echo "$(RED)Failed to start OpenBao$(NC)"; \
|
||||
cat /tmp/openbao-ci-setup.log 2>/dev/null || true; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"identities": [
|
||||
{
|
||||
"name": "admin",
|
||||
"credentials": [
|
||||
{
|
||||
"accessKey": "ACCESS_KEY_PLACEHOLDER",
|
||||
"secretKey": "SECRET_KEY_PLACEHOLDER"
|
||||
}
|
||||
],
|
||||
"actions": ["Admin", "Read", "List", "Tagging", "Write"]
|
||||
}
|
||||
],
|
||||
"kms": {
|
||||
"default_provider": "openbao",
|
||||
"providers": {
|
||||
"openbao": {
|
||||
"type": "openbao",
|
||||
"address": "OPENBAO_ADDR_PLACEHOLDER",
|
||||
"token": "OPENBAO_TOKEN_PLACEHOLDER",
|
||||
"transit_path": "transit",
|
||||
"cache_enabled": true,
|
||||
"cache_ttl": "20m"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package metadata_subscribe
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
@@ -27,6 +28,8 @@ import (
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const slowConsumerMetadataPayloadSize = 4096
|
||||
|
||||
// TestMetadataSubscribeBasic tests basic metadata subscription functionality
|
||||
func TestMetadataSubscribeBasic(t *testing.T) {
|
||||
if testing.Short() {
|
||||
@@ -680,6 +683,108 @@ func TestMetadataSubscribeMillionUpdates(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMetadataSubscribeSlowConsumerKeepsProgressing(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
testDir, err := os.MkdirTemp("", "seaweedfs_slow_consumer_test_")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
cluster, err := startSeaweedFSCluster(ctx, testDir)
|
||||
require.NoError(t, err)
|
||||
defer cluster.Stop()
|
||||
|
||||
require.NoError(t, waitForHTTPServer("http://127.0.0.1:9333", 30*time.Second))
|
||||
require.NoError(t, waitForHTTPServer("http://127.0.0.1:8080", 30*time.Second))
|
||||
require.NoError(t, waitForHTTPServer("http://127.0.0.1:8888", 30*time.Second))
|
||||
|
||||
t.Logf("Cluster started for slow consumer regression test")
|
||||
|
||||
t.Run("single_filer_slow_consumer", func(t *testing.T) {
|
||||
var receivedCount int64
|
||||
phaseOneEntries := 6000
|
||||
phaseTwoEntries := 14000
|
||||
totalEntries := phaseOneEntries + phaseTwoEntries
|
||||
minExpected := int64(12000)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
subCtx, subCancel := context.WithCancel(ctx)
|
||||
defer subCancel()
|
||||
|
||||
go func() {
|
||||
err := followMetadataSlowly(
|
||||
subCtx,
|
||||
"127.0.0.1:8888",
|
||||
"/slow-consumer/",
|
||||
time.Now().Add(-5*time.Second).UnixNano(),
|
||||
time.Millisecond,
|
||||
func(resp *filer_pb.SubscribeMetadataResponse) {
|
||||
if resp.GetEventNotification() == nil {
|
||||
return
|
||||
}
|
||||
entry := resp.GetEventNotification().GetNewEntry()
|
||||
if entry == nil || entry.IsDirectory {
|
||||
return
|
||||
}
|
||||
atomic.AddInt64(&receivedCount, 1)
|
||||
},
|
||||
)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
errChan <- err
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
payload := bytes.Repeat([]byte("x"), slowConsumerMetadataPayloadSize)
|
||||
startTime := time.Now()
|
||||
require.NoError(t, createMetadataEntries(ctx, "127.0.0.1:8888", 0, phaseOneEntries, payload))
|
||||
t.Logf("Created phase 1 with %d entries in %v", phaseOneEntries, time.Since(startTime))
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
require.NoError(t, createMetadataEntries(ctx, "127.0.0.1:8888", phaseOneEntries, phaseTwoEntries, payload))
|
||||
t.Logf("Created phase 2 with %d entries", phaseTwoEntries)
|
||||
|
||||
checkTicker := time.NewTicker(2 * time.Second)
|
||||
defer checkTicker.Stop()
|
||||
deadline := time.NewTimer(45 * time.Second)
|
||||
defer deadline.Stop()
|
||||
|
||||
lastReceived := int64(-1)
|
||||
stableChecks := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-errChan:
|
||||
t.Fatalf("slow consumer subscription error: %v", err)
|
||||
case <-deadline.C:
|
||||
t.Fatalf("timed out waiting for slow consumer progress, received %d/%d", atomic.LoadInt64(&receivedCount), totalEntries)
|
||||
case <-checkTicker.C:
|
||||
received := atomic.LoadInt64(&receivedCount)
|
||||
t.Logf("Slow consumer progress: %d/%d", received, totalEntries)
|
||||
if received >= minExpected {
|
||||
return
|
||||
}
|
||||
if received == lastReceived {
|
||||
stableChecks++
|
||||
if stableChecks >= 4 {
|
||||
t.Fatalf("slow consumer stalled at %d/%d after writes completed", received, totalEntries)
|
||||
}
|
||||
} else {
|
||||
stableChecks = 0
|
||||
}
|
||||
lastReceived = received
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Helper types and functions
|
||||
|
||||
type TestCluster struct {
|
||||
@@ -915,3 +1020,106 @@ func subscribeToMetadataWithOptions(ctx context.Context, filerGrpcAddress, pathP
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func followMetadataSlowly(
|
||||
ctx context.Context,
|
||||
filerGrpcAddress, pathPrefix string,
|
||||
sinceNs int64,
|
||||
delay time.Duration,
|
||||
onEvent func(resp *filer_pb.SubscribeMetadataResponse),
|
||||
) error {
|
||||
grpcDialOption := security.LoadClientTLS(util.GetViper(), "grpc.client")
|
||||
if grpcDialOption == nil {
|
||||
grpcDialOption = grpc.WithTransportCredentials(insecure.NewCredentials())
|
||||
}
|
||||
|
||||
option := &pb.MetadataFollowOption{
|
||||
ClientName: "slow_consumer_test",
|
||||
ClientId: util.RandomInt32(),
|
||||
ClientEpoch: int32(time.Now().Unix()),
|
||||
PathPrefix: pathPrefix,
|
||||
StartTsNs: sinceNs,
|
||||
EventErrorType: pb.DontLogError,
|
||||
}
|
||||
|
||||
return pb.FollowMetadata(pb.ServerAddress(filerGrpcAddress), grpcDialOption, option, func(resp *filer_pb.SubscribeMetadataResponse) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
onEvent(resp)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(delay):
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func createMetadataEntries(ctx context.Context, filerGrpcAddress string, startIndex, total int, payload []byte) error {
|
||||
const workers = 10
|
||||
|
||||
grpcDialOption := grpc.WithTransportCredentials(insecure.NewCredentials())
|
||||
errCh := make(chan error, workers)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for workerID := 0; workerID < workers; workerID++ {
|
||||
wg.Add(1)
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
err := pb.WithFilerClient(false, 0, pb.ServerAddress(filerGrpcAddress), grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
|
||||
for idx := startIndex + workerID; idx < startIndex+total; idx += workers {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
dir := fmt.Sprintf("/slow-consumer/bucket-%02d", idx%6)
|
||||
name := fmt.Sprintf("entry-%05d", idx)
|
||||
|
||||
_, err := client.CreateEntry(ctx, &filer_pb.CreateEntryRequest{
|
||||
Directory: dir,
|
||||
Entry: &filer_pb.Entry{
|
||||
Name: name,
|
||||
IsDirectory: false,
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileSize: uint64(len(payload)),
|
||||
Mtime: time.Now().Unix(),
|
||||
FileMode: 0644,
|
||||
Uid: 1000,
|
||||
Gid: 1000,
|
||||
},
|
||||
Extended: map[string][]byte{
|
||||
"payload": payload,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}(workerID)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
|
||||
for err := range errCh {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -590,17 +590,14 @@ func TestCORSCaching(t *testing.T) {
|
||||
Bucket: aws.String(bucketName),
|
||||
CORSConfiguration: corsConfig1,
|
||||
})
|
||||
assert.NoError(t, err, "Should be able to put initial CORS configuration")
|
||||
|
||||
// Wait for metadata subscription to update cache
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
require.NoError(t, err, "Should be able to put initial CORS configuration")
|
||||
|
||||
// Get the configuration
|
||||
getResp1, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
assert.NoError(t, err, "Should be able to get initial CORS configuration")
|
||||
assert.Len(t, getResp1.CORSRules, 1, "Should have one CORS rule")
|
||||
require.NoError(t, err, "Should be able to get initial CORS configuration")
|
||||
require.Len(t, getResp1.CORSRules, 1, "Should have one CORS rule")
|
||||
|
||||
// Update the configuration
|
||||
corsConfig2 := &types.CORSConfiguration{
|
||||
@@ -618,17 +615,14 @@ func TestCORSCaching(t *testing.T) {
|
||||
Bucket: aws.String(bucketName),
|
||||
CORSConfiguration: corsConfig2,
|
||||
})
|
||||
assert.NoError(t, err, "Should be able to update CORS configuration")
|
||||
|
||||
// Wait for metadata subscription to update cache
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
require.NoError(t, err, "Should be able to update CORS configuration")
|
||||
|
||||
// Get the updated configuration (should reflect the changes)
|
||||
getResp2, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
assert.NoError(t, err, "Should be able to get updated CORS configuration")
|
||||
assert.Len(t, getResp2.CORSRules, 1, "Should have one CORS rule")
|
||||
require.NoError(t, err, "Should be able to get updated CORS configuration")
|
||||
require.Len(t, getResp2.CORSRules, 1, "Should have one CORS rule")
|
||||
|
||||
rule := getResp2.CORSRules[0]
|
||||
assert.Equal(t, []string{"Content-Type"}, rule.AllowedHeaders, "Should have updated headers")
|
||||
|
||||
@@ -99,6 +99,10 @@ func TestS3Integration(t *testing.T) {
|
||||
testDeleteObject(t, cluster)
|
||||
})
|
||||
|
||||
t.Run("DeleteDirectoryMarkerWithChildren", func(t *testing.T) {
|
||||
testDeleteDirectoryMarkerWithChildren(t, cluster)
|
||||
})
|
||||
|
||||
t.Run("DeleteBucket", func(t *testing.T) {
|
||||
testDeleteBucket(t, cluster)
|
||||
})
|
||||
@@ -754,6 +758,47 @@ func testDeleteObject(t *testing.T, cluster *TestCluster) {
|
||||
t.Logf("✓ Deleted object: %s/%s", bucketName, objectKey)
|
||||
}
|
||||
|
||||
func testDeleteDirectoryMarkerWithChildren(t *testing.T, cluster *TestCluster) {
|
||||
bucketName := createTestBucket(t, cluster, "test-delete-dir-marker-")
|
||||
childKey := "test-content/file1.txt"
|
||||
directoryMarkerKey := "test-content/"
|
||||
|
||||
_, err := cluster.s3Client.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(childKey),
|
||||
Body: bytes.NewReader([]byte("child")),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = cluster.s3Client.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(directoryMarkerKey),
|
||||
Body: bytes.NewReader(nil),
|
||||
ContentType: aws.String("application/octet-stream"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = cluster.s3Client.DeleteObject(&s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(directoryMarkerKey),
|
||||
})
|
||||
require.NoError(t, err, "Deleting a directory marker should succeed even when children exist")
|
||||
|
||||
listResp, err := cluster.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucketName),
|
||||
Prefix: aws.String("test-content/"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
foundKeys := make(map[string]bool)
|
||||
for _, obj := range listResp.Contents {
|
||||
foundKeys[aws.StringValue(obj.Key)] = true
|
||||
}
|
||||
|
||||
assert.True(t, foundKeys[childKey], "Child object should remain after deleting the directory marker")
|
||||
assert.False(t, foundKeys[directoryMarkerKey], "Directory marker should no longer be listed after deletion")
|
||||
}
|
||||
|
||||
func testDeleteBucket(t *testing.T, cluster *TestCluster) {
|
||||
bucketName := "test-delete-bucket-" + randomString(8)
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -45,6 +47,39 @@ func UploadBytes(t testing.TB, client *http.Client, volumeURL, fid string, data
|
||||
return DoRequest(t, client, req)
|
||||
}
|
||||
|
||||
// CorruptIndexFile appends garbage bytes to a volume's .idx file on disk so
|
||||
// that CheckIndexFile detects a size mismatch during scrub.
|
||||
func CorruptIndexFile(t testing.TB, baseDir string, volumeID uint32) {
|
||||
t.Helper()
|
||||
idxPath := filepath.Join(baseDir, "volume", fmt.Sprintf("%d.idx", volumeID))
|
||||
f, err := os.OpenFile(idxPath, os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("open idx file for corruption: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := f.Write([]byte{0xDE, 0xAD}); err != nil {
|
||||
t.Fatalf("corrupt idx file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// EnableMaintenanceMode puts the volume server into maintenance mode.
|
||||
func EnableMaintenanceMode(t testing.TB, ctx context.Context, client volume_server_pb.VolumeServerClient) {
|
||||
t.Helper()
|
||||
stateResp, err := client.GetState(ctx, &volume_server_pb.GetStateRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("GetState failed: %v", err)
|
||||
}
|
||||
_, err = client.SetState(ctx, &volume_server_pb.SetStateRequest{
|
||||
State: &volume_server_pb.VolumeServerState{
|
||||
Maintenance: true,
|
||||
Version: stateResp.GetState().GetVersion(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SetState maintenance=true failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ReadBytes(t testing.TB, client *http.Client, volumeURL, fid string) *http.Response {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -97,9 +97,13 @@ func TestVolumeMarkReadonlyWritableErrorPaths(t *testing.T) {
|
||||
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
||||
defer conn.Close()
|
||||
|
||||
const volumeID = uint32(75)
|
||||
framework.AllocateVolume(t, grpcClient, volumeID, "")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// non-existent volumes should return "not found"
|
||||
_, err := grpcClient.VolumeMarkReadonly(ctx, &volume_server_pb.VolumeMarkReadonlyRequest{VolumeId: 98771, Persist: true})
|
||||
if err == nil || !strings.Contains(err.Error(), "not found") {
|
||||
t.Fatalf("VolumeMarkReadonly missing-volume error mismatch: %v", err)
|
||||
@@ -110,29 +114,31 @@ func TestVolumeMarkReadonlyWritableErrorPaths(t *testing.T) {
|
||||
t.Fatalf("VolumeMarkWritable missing-volume error mismatch: %v", err)
|
||||
}
|
||||
|
||||
stateResp, err := grpcClient.GetState(ctx, &volume_server_pb.GetStateRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("GetState failed: %v", err)
|
||||
}
|
||||
_, err = grpcClient.SetState(ctx, &volume_server_pb.SetStateRequest{
|
||||
State: &volume_server_pb.VolumeServerState{
|
||||
Maintenance: true,
|
||||
Version: stateResp.GetState().GetVersion(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SetState maintenance=true failed: %v", err)
|
||||
}
|
||||
// enter maintenance mode
|
||||
framework.EnableMaintenanceMode(t, ctx, grpcClient)
|
||||
|
||||
_, err = grpcClient.VolumeMarkReadonly(ctx, &volume_server_pb.VolumeMarkReadonlyRequest{VolumeId: 1, Persist: true})
|
||||
// existing volume in maintenance mode should return "maintenance mode" error
|
||||
_, err = grpcClient.VolumeMarkReadonly(ctx, &volume_server_pb.VolumeMarkReadonlyRequest{VolumeId: volumeID, Persist: true})
|
||||
if err == nil || !strings.Contains(err.Error(), "maintenance mode") {
|
||||
t.Fatalf("VolumeMarkReadonly maintenance error mismatch: %v", err)
|
||||
}
|
||||
|
||||
_, err = grpcClient.VolumeMarkWritable(ctx, &volume_server_pb.VolumeMarkWritableRequest{VolumeId: 1})
|
||||
_, err = grpcClient.VolumeMarkWritable(ctx, &volume_server_pb.VolumeMarkWritableRequest{VolumeId: volumeID})
|
||||
if err == nil || !strings.Contains(err.Error(), "maintenance mode") {
|
||||
t.Fatalf("VolumeMarkWritable maintenance error mismatch: %v", err)
|
||||
}
|
||||
|
||||
// non-existent volume in maintenance mode should still return "not found"
|
||||
// (volume lookup happens before maintenance check)
|
||||
_, err = grpcClient.VolumeMarkReadonly(ctx, &volume_server_pb.VolumeMarkReadonlyRequest{VolumeId: 98773, Persist: true})
|
||||
if err == nil || !strings.Contains(err.Error(), "not found") {
|
||||
t.Fatalf("VolumeMarkReadonly missing-volume in maintenance error mismatch: %v", err)
|
||||
}
|
||||
|
||||
_, err = grpcClient.VolumeMarkWritable(ctx, &volume_server_pb.VolumeMarkWritableRequest{VolumeId: 98774})
|
||||
if err == nil || !strings.Contains(err.Error(), "not found") {
|
||||
t.Fatalf("VolumeMarkWritable missing-volume in maintenance error mismatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteCollectionRemovesVolumeAndIsIdempotent(t *testing.T) {
|
||||
|
||||
@@ -385,3 +385,149 @@ func TestQueryCookieMismatchReturnsEOFNoResults(t *testing.T) {
|
||||
t.Fatalf("Query cookie mismatch expected EOF with no streamed records, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrubVolumeMarkBrokenReadonlyHealthyVolume(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
clusterHarness := framework.StartSingleVolumeCluster(t, matrix.P1())
|
||||
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
||||
defer conn.Close()
|
||||
|
||||
const volumeID = uint32(76)
|
||||
framework.AllocateVolume(t, grpcClient, volumeID, "")
|
||||
|
||||
httpClient := framework.NewHTTPClient()
|
||||
framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(volumeID, 1, 1), []byte("healthy data"))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := grpcClient.ScrubVolume(ctx, &volume_server_pb.ScrubVolumeRequest{
|
||||
VolumeIds: []uint32{volumeID},
|
||||
Mode: volume_server_pb.VolumeScrubMode_INDEX,
|
||||
MarkBrokenVolumesReadonly: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ScrubVolume with MarkBrokenVolumesReadonly on healthy volume failed: %v", err)
|
||||
}
|
||||
if len(resp.GetBrokenVolumeIds()) != 0 {
|
||||
t.Fatalf("expected no broken volumes, got %v", resp.GetBrokenVolumeIds())
|
||||
}
|
||||
|
||||
statusResp, err := grpcClient.VolumeStatus(ctx, &volume_server_pb.VolumeStatusRequest{VolumeId: volumeID})
|
||||
if err != nil {
|
||||
t.Fatalf("VolumeStatus failed: %v", err)
|
||||
}
|
||||
if statusResp.GetIsReadOnly() {
|
||||
t.Fatalf("healthy volume should not be read-only after scrub with MarkBrokenVolumesReadonly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrubVolumeMarkBrokenReadonlyCorruptVolume(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
clusterHarness := framework.StartSingleVolumeCluster(t, matrix.P1())
|
||||
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
||||
defer conn.Close()
|
||||
|
||||
const volumeID = uint32(77)
|
||||
framework.AllocateVolume(t, grpcClient, volumeID, "")
|
||||
|
||||
httpClient := framework.NewHTTPClient()
|
||||
framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(volumeID, 1, 1), []byte("test data"))
|
||||
|
||||
framework.CorruptIndexFile(t, clusterHarness.BaseDir(), volumeID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// scrub without the flag: broken volume reported but not marked read-only
|
||||
resp, err := grpcClient.ScrubVolume(ctx, &volume_server_pb.ScrubVolumeRequest{
|
||||
VolumeIds: []uint32{volumeID},
|
||||
Mode: volume_server_pb.VolumeScrubMode_INDEX,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ScrubVolume without flag failed: %v", err)
|
||||
}
|
||||
if len(resp.GetBrokenVolumeIds()) == 0 {
|
||||
t.Fatalf("expected broken volume after corruption")
|
||||
}
|
||||
|
||||
statusResp, err := grpcClient.VolumeStatus(ctx, &volume_server_pb.VolumeStatusRequest{VolumeId: volumeID})
|
||||
if err != nil {
|
||||
t.Fatalf("VolumeStatus after scrub without flag failed: %v", err)
|
||||
}
|
||||
if statusResp.GetIsReadOnly() {
|
||||
t.Fatalf("volume should not be read-only when MarkBrokenVolumesReadonly is false")
|
||||
}
|
||||
|
||||
// scrub with the flag: broken volume should now be marked read-only
|
||||
resp, err = grpcClient.ScrubVolume(ctx, &volume_server_pb.ScrubVolumeRequest{
|
||||
VolumeIds: []uint32{volumeID},
|
||||
Mode: volume_server_pb.VolumeScrubMode_INDEX,
|
||||
MarkBrokenVolumesReadonly: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ScrubVolume with MarkBrokenVolumesReadonly failed: %v", err)
|
||||
}
|
||||
if len(resp.GetBrokenVolumeIds()) == 0 {
|
||||
t.Fatalf("expected broken volume after corruption with flag")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, d := range resp.GetDetails() {
|
||||
if strings.Contains(d, "is now read-only") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected 'is now read-only' in details, got: %v", resp.GetDetails())
|
||||
}
|
||||
|
||||
statusResp, err = grpcClient.VolumeStatus(ctx, &volume_server_pb.VolumeStatusRequest{VolumeId: volumeID})
|
||||
if err != nil {
|
||||
t.Fatalf("VolumeStatus after MarkBrokenVolumesReadonly scrub failed: %v", err)
|
||||
}
|
||||
if !statusResp.GetIsReadOnly() {
|
||||
t.Fatalf("broken volume should be read-only after MarkBrokenVolumesReadonly scrub")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrubVolumeMarkBrokenReadonlyInMaintenanceMode(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
clusterHarness := framework.StartSingleVolumeCluster(t, matrix.P1())
|
||||
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
||||
defer conn.Close()
|
||||
|
||||
const volumeID = uint32(78)
|
||||
framework.AllocateVolume(t, grpcClient, volumeID, "")
|
||||
|
||||
httpClient := framework.NewHTTPClient()
|
||||
framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(volumeID, 1, 1), []byte("test data"))
|
||||
|
||||
framework.CorruptIndexFile(t, clusterHarness.BaseDir(), volumeID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
framework.EnableMaintenanceMode(t, ctx, grpcClient)
|
||||
|
||||
// scrub with the flag in maintenance mode: makeVolumeReadonly should fail
|
||||
// and ScrubVolume should propagate the error
|
||||
_, err := grpcClient.ScrubVolume(ctx, &volume_server_pb.ScrubVolumeRequest{
|
||||
VolumeIds: []uint32{volumeID},
|
||||
Mode: volume_server_pb.VolumeScrubMode_INDEX,
|
||||
MarkBrokenVolumesReadonly: true,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "maintenance mode") {
|
||||
t.Fatalf("ScrubVolume with MarkBrokenVolumesReadonly in maintenance mode error mismatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,10 +48,11 @@ type ObjectStoreUser struct {
|
||||
}
|
||||
|
||||
type ObjectStoreUsersData struct {
|
||||
Username string `json:"username"`
|
||||
Users []ObjectStoreUser `json:"users"`
|
||||
TotalUsers int `json:"total_users"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
Username string `json:"username"`
|
||||
Users []ObjectStoreUser `json:"users"`
|
||||
TotalUsers int `json:"total_users"`
|
||||
HasAnonymousUser bool `json:"has_anonymous_user"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
|
||||
// User management request structures
|
||||
|
||||
@@ -311,10 +311,19 @@ func (h *UserHandlers) getObjectStoreUsersData(r *http.Request) dash.ObjectStore
|
||||
}
|
||||
}
|
||||
|
||||
hasAnonymous := false
|
||||
for _, u := range users {
|
||||
if u.Username == "anonymous" {
|
||||
hasAnonymous = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return dash.ObjectStoreUsersData{
|
||||
Username: username,
|
||||
Users: users,
|
||||
TotalUsers: len(users),
|
||||
LastUpdated: time.Now(),
|
||||
Username: username,
|
||||
Users: users,
|
||||
TotalUsers: len(users),
|
||||
HasAnonymousUser: hasAnonymous,
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,10 +141,12 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
data-action="edit-user" data-username={ user.Username }>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-action="manage-access-keys" data-username={ user.Username }>
|
||||
<i class="fas fa-key"></i>
|
||||
</button>
|
||||
if user.Username != "anonymous" {
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-action="manage-access-keys" data-username={ user.Username }>
|
||||
<i class="fas fa-key"></i>
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
data-action="delete-user" data-username={ user.Username }>
|
||||
<i class="fas fa-trash"></i>
|
||||
@@ -197,7 +199,17 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
<form id="createUserForm">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username *</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
if !data.HasAnonymousUser {
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input mt-0 me-1" type="checkbox" id="anonymousCheck" onchange="toggleAnonymousUser()">
|
||||
<label class="form-check-label small" for="anonymousCheck">Anonymous</label>
|
||||
</div>
|
||||
</div>
|
||||
} else {
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
@@ -669,6 +681,24 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle anonymous user checkbox
|
||||
function toggleAnonymousUser() {
|
||||
const checkbox = document.getElementById('anonymousCheck');
|
||||
const usernameInput = document.getElementById('username');
|
||||
const generateKey = document.getElementById('generateKey');
|
||||
if (checkbox.checked) {
|
||||
usernameInput.value = 'anonymous';
|
||||
usernameInput.readOnly = true;
|
||||
generateKey.checked = false;
|
||||
generateKey.disabled = true;
|
||||
} else {
|
||||
usernameInput.value = '';
|
||||
usernameInput.readOnly = false;
|
||||
generateKey.disabled = false;
|
||||
generateKey.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate bucket selection dropdowns
|
||||
function populateBucketSelections() {
|
||||
const createSelect = document.getElementById('selectedBuckets');
|
||||
@@ -1055,6 +1085,8 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('createUserModal'));
|
||||
modal.hide();
|
||||
form.reset();
|
||||
document.getElementById('username').readOnly = false;
|
||||
document.getElementById('generateKey').disabled = false;
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ package command
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
nethttp "net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -14,26 +15,30 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/security"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util/http"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type FilerBackupOptions struct {
|
||||
isActivePassive *bool
|
||||
filer *string
|
||||
path *string
|
||||
excludePaths *string
|
||||
excludeFileName *string
|
||||
debug *bool
|
||||
proxyByFiler *bool
|
||||
doDeleteFiles *bool
|
||||
disableErrorRetry *bool
|
||||
ignore404Error *bool
|
||||
timeAgo *time.Duration
|
||||
retentionDays *int
|
||||
isActivePassive *bool
|
||||
filer *string
|
||||
path *string
|
||||
excludePaths *string
|
||||
excludeFileName *string // deprecated: use excludeFileNames
|
||||
excludeFileNames *string
|
||||
excludePathPatterns *string
|
||||
debug *bool
|
||||
proxyByFiler *bool
|
||||
doDeleteFiles *bool
|
||||
disableErrorRetry *bool
|
||||
ignore404Error *bool
|
||||
timeAgo *time.Duration
|
||||
retentionDays *int
|
||||
}
|
||||
|
||||
var (
|
||||
filerBackupOptions FilerBackupOptions
|
||||
filerBackupOptions FilerBackupOptions
|
||||
ignorable404ErrString = fmt.Sprintf("%d %s: %s", nethttp.StatusNotFound, nethttp.StatusText(nethttp.StatusNotFound), http.ErrNotFound.Error())
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -41,7 +46,9 @@ func init() {
|
||||
filerBackupOptions.filer = cmdFilerBackup.Flag.String("filer", "localhost:8888", "filer of one SeaweedFS cluster")
|
||||
filerBackupOptions.path = cmdFilerBackup.Flag.String("filerPath", "/", "directory to sync on filer")
|
||||
filerBackupOptions.excludePaths = cmdFilerBackup.Flag.String("filerExcludePaths", "", "exclude directories to sync on filer")
|
||||
filerBackupOptions.excludeFileName = cmdFilerBackup.Flag.String("filerExcludeFileName", "", "exclude file names that match the regexp to sync on filer")
|
||||
filerBackupOptions.excludeFileName = cmdFilerBackup.Flag.String("filerExcludeFileName", "", "[DEPRECATED: use -filerExcludeFileNames] exclude file names that match the regexp")
|
||||
filerBackupOptions.excludeFileNames = cmdFilerBackup.Flag.String("filerExcludeFileNames", "", "comma-separated wildcard patterns to exclude file names, e.g., \"*.tmp,._*\"")
|
||||
filerBackupOptions.excludePathPatterns = cmdFilerBackup.Flag.String("filerExcludePathPatterns", "", "comma-separated wildcard patterns to exclude paths where any component matches, e.g., \".snapshot,temp*\"")
|
||||
filerBackupOptions.proxyByFiler = cmdFilerBackup.Flag.Bool("filerProxy", false, "read and write file chunks by filer instead of volume servers")
|
||||
filerBackupOptions.doDeleteFiles = cmdFilerBackup.Flag.Bool("doDeleteFiles", false, "delete files on the destination")
|
||||
filerBackupOptions.debug = cmdFilerBackup.Flag.Bool("debug", false, "debug mode to print out received files")
|
||||
@@ -70,6 +77,15 @@ func runFilerBackup(cmd *Command, args []string) bool {
|
||||
util.LoadSecurityConfiguration()
|
||||
util.LoadConfiguration("replication", true)
|
||||
|
||||
// Compile exclude patterns once before the retry loop — these are
|
||||
// configuration errors and must not be retried.
|
||||
reExcludeFileName, err := compileExcludePattern(*filerBackupOptions.excludeFileName, "exclude file name")
|
||||
if err != nil {
|
||||
glog.Fatalf("invalid -filerExcludeFileName: %v", err)
|
||||
}
|
||||
excludeFileNames := wildcard.CompileWildcardMatchers(*filerBackupOptions.excludeFileNames)
|
||||
excludePathPatterns := wildcard.CompileWildcardMatchers(*filerBackupOptions.excludePathPatterns)
|
||||
|
||||
grpcDialOption := security.LoadClientTLS(util.GetViper(), "grpc.client")
|
||||
|
||||
clientId := util.RandomInt32()
|
||||
@@ -77,7 +93,7 @@ func runFilerBackup(cmd *Command, args []string) bool {
|
||||
|
||||
for {
|
||||
clientEpoch++
|
||||
err := doFilerBackup(grpcDialOption, &filerBackupOptions, clientId, clientEpoch)
|
||||
err := doFilerBackup(grpcDialOption, &filerBackupOptions, reExcludeFileName, excludeFileNames, excludePathPatterns, clientId, clientEpoch)
|
||||
if err != nil {
|
||||
glog.Errorf("backup from %s: %v", *filerBackupOptions.filer, err)
|
||||
time.Sleep(1747 * time.Millisecond)
|
||||
@@ -89,7 +105,7 @@ const (
|
||||
BackupKeyPrefix = "backup."
|
||||
)
|
||||
|
||||
func doFilerBackup(grpcDialOption grpc.DialOption, backupOption *FilerBackupOptions, clientId int32, clientEpoch int32) error {
|
||||
func doFilerBackup(grpcDialOption grpc.DialOption, backupOption *FilerBackupOptions, reExcludeFileName *regexp.Regexp, excludeFileNames []*wildcard.WildcardMatcher, excludePathPatterns []*wildcard.WildcardMatcher, clientId int32, clientEpoch int32) error {
|
||||
|
||||
// find data sink
|
||||
dataSink := findSink(util.GetViper())
|
||||
@@ -100,13 +116,6 @@ func doFilerBackup(grpcDialOption grpc.DialOption, backupOption *FilerBackupOpti
|
||||
sourceFiler := pb.ServerAddress(*backupOption.filer)
|
||||
sourcePath := *backupOption.path
|
||||
excludePaths := util.StringSplit(*backupOption.excludePaths, ",")
|
||||
var reExcludeFileName *regexp.Regexp
|
||||
if *backupOption.excludeFileName != "" {
|
||||
var err error
|
||||
if reExcludeFileName, err = regexp.Compile(*backupOption.excludeFileName); err != nil {
|
||||
return fmt.Errorf("error compile regexp %v for exclude file name: %+v", *backupOption.excludeFileName, err)
|
||||
}
|
||||
}
|
||||
timeAgo := *backupOption.timeAgo
|
||||
targetPath := dataSink.GetSinkToDirectory()
|
||||
debug := *backupOption.debug
|
||||
@@ -138,27 +147,20 @@ func doFilerBackup(grpcDialOption grpc.DialOption, backupOption *FilerBackupOpti
|
||||
|
||||
var processEventFn func(*filer_pb.SubscribeMetadataResponse) error
|
||||
if *backupOption.ignore404Error {
|
||||
processEventFnGenerated := genProcessFunction(sourcePath, targetPath, excludePaths, reExcludeFileName, dataSink, *backupOption.doDeleteFiles, debug)
|
||||
processEventFnGenerated := genProcessFunction(sourcePath, targetPath, excludePaths, reExcludeFileName, excludeFileNames, excludePathPatterns, dataSink, *backupOption.doDeleteFiles, debug)
|
||||
processEventFn = func(resp *filer_pb.SubscribeMetadataResponse) error {
|
||||
err := processEventFnGenerated(resp)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// ignore HTTP 404 from remote reads
|
||||
if errors.Is(err, http.ErrNotFound) {
|
||||
if isIgnorable404(err) {
|
||||
glog.V(0).Infof("got 404 error for %s, ignore it: %s", getSourceKey(resp), err.Error())
|
||||
return nil
|
||||
}
|
||||
// also ignore missing volume/lookup errors coming from LookupFileId or vid map
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "LookupFileId") || (strings.Contains(errStr, "volume id") && strings.Contains(errStr, "not found")) {
|
||||
glog.V(0).Infof("got missing-volume error for %s, ignore it: %s", getSourceKey(resp), errStr)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
processEventFn = genProcessFunction(sourcePath, targetPath, excludePaths, reExcludeFileName, dataSink, *backupOption.doDeleteFiles, debug)
|
||||
processEventFn = genProcessFunction(sourcePath, targetPath, excludePaths, reExcludeFileName, excludeFileNames, excludePathPatterns, dataSink, *backupOption.doDeleteFiles, debug)
|
||||
}
|
||||
|
||||
processEventFnWithOffset := pb.AddOffsetFunc(processEventFn, 3*time.Second, func(counter int64, lastTsNs int64) error {
|
||||
@@ -218,3 +220,19 @@ func getSourceKey(resp *filer_pb.SubscribeMetadataResponse) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// isIgnorable404 returns true if the error represents a 404/not-found condition
|
||||
// that should be silently ignored during backup. This covers:
|
||||
// - errors wrapping http.ErrNotFound (direct volume server 404 via non-S3 sinks)
|
||||
// - errors containing the "404 Not Found: not found" status string (S3 sink path
|
||||
// where AWS SDK breaks the errors.Is unwrap chain)
|
||||
// - LookupFileId or volume-id-not-found errors from the volume id map
|
||||
func isIgnorable404(err error) bool {
|
||||
if errors.Is(err, http.ErrNotFound) {
|
||||
return true
|
||||
}
|
||||
errStr := err.Error()
|
||||
return strings.Contains(errStr, ignorable404ErrString) ||
|
||||
strings.Contains(errStr, "LookupFileId") ||
|
||||
(strings.Contains(errStr, "volume id") && strings.Contains(errStr, "not found"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
util_http.InitGlobalHttpClient()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// readUrlError starts a test HTTP server returning the given status code
|
||||
// and returns the error produced by ReadUrlAsStream.
|
||||
//
|
||||
// The error format is defined in ReadUrlAsStream:
|
||||
// https://github.com/seaweedfs/seaweedfs/blob/3a765df2ff90839acb9acf910b73513417fa84d1/weed/util/http/http_global_client_util.go#L353
|
||||
func readUrlError(t *testing.T, statusCode int) error {
|
||||
t.Helper()
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, http.StatusText(statusCode), statusCode)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, err := util_http.ReadUrlAsStream(context.Background(),
|
||||
server.URL+"/437,03f591a3a2b95e?readDeleted=true", "",
|
||||
nil, false, true, 0, 1024, func(data []byte) {})
|
||||
if err == nil {
|
||||
t.Fatal("expected error from ReadUrlAsStream, got nil")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func TestIsIgnorable404_WrappedErrNotFound(t *testing.T) {
|
||||
readErr := readUrlError(t, http.StatusNotFound)
|
||||
// genProcessFunction wraps sink errors with %w:
|
||||
// https://github.com/seaweedfs/seaweedfs/blob/3a765df2ff90839acb9acf910b73513417fa84d1/weed/command/filer_sync.go#L496
|
||||
genErr := fmt.Errorf("create entry1 : %w", readErr)
|
||||
|
||||
if !isIgnorable404(genErr) {
|
||||
t.Errorf("expected ignorable, got not: %v", genErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsIgnorable404_BrokenUnwrapChain(t *testing.T) {
|
||||
readErr := readUrlError(t, http.StatusNotFound)
|
||||
// AWS SDK v1 wraps transport errors via awserr.New which uses origErr.Error()
|
||||
// instead of %w, so errors.Is cannot unwrap through it:
|
||||
// https://github.com/aws/aws-sdk-go/blob/v1.55.8/aws/corehandlers/handlers.go#L173
|
||||
// https://github.com/aws/aws-sdk-go/blob/v1.55.8/aws/awserr/types.go#L15
|
||||
awsSdkErr := fmt.Errorf("RequestError: send request failed\n"+
|
||||
"caused by: Put \"https://s3.amazonaws.com/bucket/key\": %s", readErr.Error())
|
||||
genErr := fmt.Errorf("create entry1 : %w", awsSdkErr)
|
||||
|
||||
if !isIgnorable404(genErr) {
|
||||
t.Errorf("expected ignorable, got not: %v", genErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsIgnorable404_NonIgnorableError(t *testing.T) {
|
||||
readErr := readUrlError(t, http.StatusForbidden)
|
||||
genErr := fmt.Errorf("create entry1 : %w", readErr)
|
||||
|
||||
if isIgnorable404(genErr) {
|
||||
t.Errorf("expected not ignorable, got ignorable: %v", genErr)
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
statsCollect "github.com/seaweedfs/seaweedfs/weed/stats"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util/grace"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -304,7 +305,7 @@ func doSubscribeFilerMetaChanges(clientId int32, clientEpoch int32, grpcDialOpti
|
||||
filerSink.SetChunkConcurrency(chunkConcurrency)
|
||||
filerSink.SetSourceFiler(filerSource)
|
||||
|
||||
persistEventFn := genProcessFunction(sourcePath, targetPath, sourceExcludePaths, nil, filerSink, doDeleteFiles, debug)
|
||||
persistEventFn := genProcessFunction(sourcePath, targetPath, sourceExcludePaths, nil, nil, nil, filerSink, doDeleteFiles, debug)
|
||||
|
||||
processEventFn := func(resp *filer_pb.SubscribeMetadataResponse) error {
|
||||
message := resp.EventNotification
|
||||
@@ -439,7 +440,7 @@ func setOffset(grpcDialOption grpc.DialOption, filer pb.ServerAddress, signature
|
||||
|
||||
}
|
||||
|
||||
func genProcessFunction(sourcePath string, targetPath string, excludePaths []string, reExcludeFileName *regexp.Regexp, dataSink sink.ReplicationSink, doDeleteFiles bool, debug bool) func(resp *filer_pb.SubscribeMetadataResponse) error {
|
||||
func genProcessFunction(sourcePath string, targetPath string, excludePaths []string, reExcludeFileName *regexp.Regexp, excludeFileNames []*wildcard.WildcardMatcher, excludePathPatterns []*wildcard.WildcardMatcher, dataSink sink.ReplicationSink, doDeleteFiles bool, debug bool) func(resp *filer_pb.SubscribeMetadataResponse) error {
|
||||
// process function
|
||||
processEventFn := func(resp *filer_pb.SubscribeMetadataResponse) error {
|
||||
message := resp.EventNotification
|
||||
@@ -468,9 +469,24 @@ func genProcessFunction(sourcePath string, targetPath string, excludePaths []str
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if reExcludeFileName != nil && reExcludeFileName.MatchString(message.NewEntry.Name) {
|
||||
// Compute per-side exclusion so that rename events crossing an
|
||||
// exclude boundary are handled as delete + create rather than
|
||||
// being entirely skipped.
|
||||
oldExcluded := isEntryExcluded(resp.Directory, message.OldEntry, reExcludeFileName, excludeFileNames, excludePathPatterns)
|
||||
newExcluded := isEntryExcluded(message.NewParentPath, message.NewEntry, reExcludeFileName, excludeFileNames, excludePathPatterns)
|
||||
|
||||
if oldExcluded && newExcluded {
|
||||
return nil
|
||||
}
|
||||
if oldExcluded {
|
||||
// Old side is excluded — treat as pure create of new entry.
|
||||
message.OldEntry = nil
|
||||
}
|
||||
if newExcluded {
|
||||
// New side is excluded — treat as pure delete of old entry.
|
||||
message.NewEntry = nil
|
||||
sourceNewKey = ""
|
||||
}
|
||||
if dataSink.IsIncremental() {
|
||||
doDeleteFiles = false
|
||||
}
|
||||
@@ -578,3 +594,75 @@ func buildKey(dataSink sink.ReplicationSink, message *filer_pb.EventNotification
|
||||
|
||||
return escapeKey(key)
|
||||
}
|
||||
|
||||
// isEntryExcluded checks whether a single side (old or new) of an event is excluded
|
||||
// by the deprecated filename regexp, the wildcard file-name matchers, or the
|
||||
// wildcard path-pattern matchers.
|
||||
func isEntryExcluded(dir string, entry *filer_pb.Entry, reExcludeFileName *regexp.Regexp, excludeFileNames []*wildcard.WildcardMatcher, excludePathPatterns []*wildcard.WildcardMatcher) bool {
|
||||
if entry == nil {
|
||||
return false
|
||||
}
|
||||
// deprecated regexp-based filename exclusion
|
||||
if reExcludeFileName != nil && reExcludeFileName.MatchString(entry.Name) {
|
||||
return true
|
||||
}
|
||||
// wildcard-based filename exclusion
|
||||
if len(excludeFileNames) > 0 && matchesAnyWildcard(excludeFileNames, entry.Name) {
|
||||
return true
|
||||
}
|
||||
// wildcard-based path-pattern exclusion: match against each directory
|
||||
// component and the entry name itself
|
||||
if len(excludePathPatterns) > 0 {
|
||||
if pathContainsWildcardMatch(dir, excludePathPatterns) {
|
||||
return true
|
||||
}
|
||||
if matchesAnyWildcard(excludePathPatterns, entry.Name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// compileExcludePattern compiles a regexp pattern string, returning nil if empty.
|
||||
func compileExcludePattern(pattern string, label string) (*regexp.Regexp, error) {
|
||||
if pattern == "" {
|
||||
return nil, nil
|
||||
}
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error compile regexp %v for %s: %+v", pattern, label, err)
|
||||
}
|
||||
return re, nil
|
||||
}
|
||||
|
||||
// matchesAnyWildcard returns true if any matcher matches the value.
|
||||
// Returns false when matchers is empty (unlike wildcard.MatchesAnyWildcard
|
||||
// which returns true for empty matchers).
|
||||
func matchesAnyWildcard(matchers []*wildcard.WildcardMatcher, value string) bool {
|
||||
for _, m := range matchers {
|
||||
if m != nil && m.Match(value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pathContainsWildcardMatch checks if any component of the given path matches
|
||||
// any of the wildcard matchers, without allocating a slice.
|
||||
func pathContainsWildcardMatch(path string, matchers []*wildcard.WildcardMatcher) bool {
|
||||
for path != "" {
|
||||
i := strings.IndexByte(path, '/')
|
||||
var component string
|
||||
if i < 0 {
|
||||
component = path
|
||||
path = ""
|
||||
} else {
|
||||
component = path[:i]
|
||||
path = path[i+1:]
|
||||
}
|
||||
if component != "" && matchesAnyWildcard(matchers, component) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
+164
-70
@@ -1,6 +1,8 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"path"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
@@ -10,26 +12,154 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
// tsMinHeap implements heap.Interface for int64 timestamps.
|
||||
type tsMinHeap []int64
|
||||
|
||||
func (h tsMinHeap) Len() int { return len(h) }
|
||||
func (h tsMinHeap) Less(i, j int) bool { return h[i] < h[j] }
|
||||
func (h tsMinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
||||
func (h *tsMinHeap) Push(x any) { *h = append(*h, x.(int64)) }
|
||||
func (h *tsMinHeap) Pop() any {
|
||||
old := *h
|
||||
n := len(old)
|
||||
x := old[n-1]
|
||||
*h = old[:n-1]
|
||||
return x
|
||||
}
|
||||
|
||||
type syncJobPaths struct {
|
||||
path util.FullPath
|
||||
newPath util.FullPath // empty for non-renames
|
||||
isDirectory bool
|
||||
}
|
||||
|
||||
type MetadataProcessor struct {
|
||||
activeJobs map[int64]*filer_pb.SubscribeMetadataResponse
|
||||
activeJobsLock sync.Mutex
|
||||
activeJobsCond *sync.Cond
|
||||
concurrencyLimit int
|
||||
fn pb.ProcessMetadataFunc
|
||||
activeJobs map[int64]*syncJobPaths
|
||||
activeJobsLock sync.Mutex
|
||||
activeJobsCond *sync.Cond
|
||||
concurrencyLimit int
|
||||
fn pb.ProcessMetadataFunc
|
||||
processedTsWatermark atomic.Int64
|
||||
|
||||
// Indexes for O(depth) conflict detection, replacing O(n) linear scan.
|
||||
// activeFilePaths counts active file jobs at each exact path.
|
||||
activeFilePaths map[util.FullPath]int
|
||||
// activeDirPaths counts active directory jobs at each exact path.
|
||||
activeDirPaths map[util.FullPath]int
|
||||
// descendantCount counts active jobs (file or dir) strictly under each directory.
|
||||
descendantCount map[util.FullPath]int
|
||||
|
||||
// tsHeap is a min-heap of active job timestamps with lazy deletion,
|
||||
// used for O(log n) amortized watermark tracking.
|
||||
tsHeap tsMinHeap
|
||||
}
|
||||
|
||||
func NewMetadataProcessor(fn pb.ProcessMetadataFunc, concurrency int, offsetTsNs int64) *MetadataProcessor {
|
||||
t := &MetadataProcessor{
|
||||
fn: fn,
|
||||
activeJobs: make(map[int64]*filer_pb.SubscribeMetadataResponse),
|
||||
activeJobs: make(map[int64]*syncJobPaths),
|
||||
concurrencyLimit: concurrency,
|
||||
activeFilePaths: make(map[util.FullPath]int),
|
||||
activeDirPaths: make(map[util.FullPath]int),
|
||||
descendantCount: make(map[util.FullPath]int),
|
||||
}
|
||||
t.processedTsWatermark.Store(offsetTsNs)
|
||||
t.activeJobsCond = sync.NewCond(&t.activeJobsLock)
|
||||
return t
|
||||
}
|
||||
|
||||
// pathAncestors returns all proper ancestor directories of p.
|
||||
// For "/a/b/c", returns ["/a/b", "/a", "/"].
|
||||
func pathAncestors(p util.FullPath) []util.FullPath {
|
||||
var ancestors []util.FullPath
|
||||
s := string(p)
|
||||
for {
|
||||
parent := path.Dir(s)
|
||||
if parent == s {
|
||||
break
|
||||
}
|
||||
ancestors = append(ancestors, util.FullPath(parent))
|
||||
s = parent
|
||||
}
|
||||
return ancestors
|
||||
}
|
||||
|
||||
// addPathToIndex registers a path in the conflict detection indexes.
|
||||
// Must be called under activeJobsLock.
|
||||
func (t *MetadataProcessor) addPathToIndex(p util.FullPath, isDirectory bool) {
|
||||
if isDirectory {
|
||||
t.activeDirPaths[p]++
|
||||
} else {
|
||||
t.activeFilePaths[p]++
|
||||
}
|
||||
for _, ancestor := range pathAncestors(p) {
|
||||
t.descendantCount[ancestor]++
|
||||
}
|
||||
}
|
||||
|
||||
// removePathFromIndex unregisters a path from the conflict detection indexes.
|
||||
// Must be called under activeJobsLock.
|
||||
func (t *MetadataProcessor) removePathFromIndex(p util.FullPath, isDirectory bool) {
|
||||
if isDirectory {
|
||||
if t.activeDirPaths[p] <= 1 {
|
||||
delete(t.activeDirPaths, p)
|
||||
} else {
|
||||
t.activeDirPaths[p]--
|
||||
}
|
||||
} else {
|
||||
if t.activeFilePaths[p] <= 1 {
|
||||
delete(t.activeFilePaths, p)
|
||||
} else {
|
||||
t.activeFilePaths[p]--
|
||||
}
|
||||
}
|
||||
for _, ancestor := range pathAncestors(p) {
|
||||
if t.descendantCount[ancestor] <= 1 {
|
||||
delete(t.descendantCount, ancestor)
|
||||
} else {
|
||||
t.descendantCount[ancestor]--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pathConflicts checks if a single path conflicts with any active job.
|
||||
// Conflict rules match pairShouldWaitFor:
|
||||
// - file vs file: exact same path
|
||||
// - file vs dir: file.IsUnder(dir)
|
||||
// - dir vs file: file.IsUnder(dir)
|
||||
// - dir vs dir: either IsUnder the other
|
||||
func (t *MetadataProcessor) pathConflicts(p util.FullPath, isDirectory bool) bool {
|
||||
if isDirectory {
|
||||
// Any active job (file or dir) strictly under this directory?
|
||||
if t.descendantCount[p] > 0 {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// Exact same file already active?
|
||||
if t.activeFilePaths[p] > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Any active directory that is a proper ancestor of p?
|
||||
for _, ancestor := range pathAncestors(p) {
|
||||
if t.activeDirPaths[ancestor] > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *MetadataProcessor) conflictsWith(resp *filer_pb.SubscribeMetadataResponse) bool {
|
||||
p, newPath, isDirectory := extractPathsFromMetadata(resp)
|
||||
if t.pathConflicts(p, isDirectory) {
|
||||
return true
|
||||
}
|
||||
if newPath != "" && t.pathConflicts(newPath, isDirectory) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *MetadataProcessor) AddSyncJob(resp *filer_pb.SubscribeMetadataResponse) {
|
||||
if filer_pb.IsEmpty(resp) {
|
||||
return
|
||||
@@ -41,7 +171,18 @@ func (t *MetadataProcessor) AddSyncJob(resp *filer_pb.SubscribeMetadataResponse)
|
||||
for len(t.activeJobs) >= t.concurrencyLimit || t.conflictsWith(resp) {
|
||||
t.activeJobsCond.Wait()
|
||||
}
|
||||
t.activeJobs[resp.TsNs] = resp
|
||||
|
||||
p, newPath, isDirectory := extractPathsFromMetadata(resp)
|
||||
jobPaths := &syncJobPaths{path: p, newPath: newPath, isDirectory: isDirectory}
|
||||
|
||||
t.activeJobs[resp.TsNs] = jobPaths
|
||||
t.addPathToIndex(p, isDirectory)
|
||||
if newPath != "" {
|
||||
t.addPathToIndex(newPath, isDirectory)
|
||||
}
|
||||
|
||||
heap.Push(&t.tsHeap, resp.TsNs)
|
||||
|
||||
go func() {
|
||||
|
||||
if err := util.Retry("metadata processor", func() error {
|
||||
@@ -54,95 +195,48 @@ func (t *MetadataProcessor) AddSyncJob(resp *filer_pb.SubscribeMetadataResponse)
|
||||
defer t.activeJobsLock.Unlock()
|
||||
|
||||
delete(t.activeJobs, resp.TsNs)
|
||||
t.removePathFromIndex(jobPaths.path, jobPaths.isDirectory)
|
||||
if jobPaths.newPath != "" {
|
||||
t.removePathFromIndex(jobPaths.newPath, jobPaths.isDirectory)
|
||||
}
|
||||
|
||||
// if is the oldest job, write down the watermark
|
||||
isOldest := true
|
||||
for t := range t.activeJobs {
|
||||
if resp.TsNs > t {
|
||||
isOldest = false
|
||||
// Lazy-clean stale entries from heap top (already-completed jobs).
|
||||
// Each entry is pushed once and popped once: O(log n) amortized.
|
||||
for t.tsHeap.Len() > 0 {
|
||||
if _, active := t.activeJobs[t.tsHeap[0]]; active {
|
||||
break
|
||||
}
|
||||
heap.Pop(&t.tsHeap)
|
||||
}
|
||||
if isOldest {
|
||||
// If this was the oldest job, advance the watermark.
|
||||
if t.tsHeap.Len() == 0 || resp.TsNs < t.tsHeap[0] {
|
||||
t.processedTsWatermark.Store(resp.TsNs)
|
||||
}
|
||||
t.activeJobsCond.Signal()
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *MetadataProcessor) conflictsWith(resp *filer_pb.SubscribeMetadataResponse) bool {
|
||||
for _, r := range t.activeJobs {
|
||||
if shouldWaitFor(resp, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// a is one possible job to schedule
|
||||
// b is one existing active job
|
||||
func shouldWaitFor(a *filer_pb.SubscribeMetadataResponse, b *filer_pb.SubscribeMetadataResponse) bool {
|
||||
aPath, aNewPath, aIsDirectory := extractPathsFromMetadata(a)
|
||||
bPath, bNewPath, bIsDirectory := extractPathsFromMetadata(b)
|
||||
|
||||
if pairShouldWaitFor(aPath, bPath, aIsDirectory, bIsDirectory) {
|
||||
return true
|
||||
}
|
||||
if aNewPath != "" {
|
||||
if pairShouldWaitFor(aNewPath, bPath, aIsDirectory, bIsDirectory) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if bNewPath != "" {
|
||||
if pairShouldWaitFor(aPath, bNewPath, aIsDirectory, bIsDirectory) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if aNewPath != "" && bNewPath != "" {
|
||||
if pairShouldWaitFor(aNewPath, bNewPath, aIsDirectory, bIsDirectory) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func pairShouldWaitFor(aPath, bPath util.FullPath, aIsDirectory, bIsDirectory bool) bool {
|
||||
if bIsDirectory {
|
||||
if aIsDirectory {
|
||||
return aPath.IsUnder(bPath) || bPath.IsUnder(aPath)
|
||||
} else {
|
||||
return aPath.IsUnder(bPath)
|
||||
}
|
||||
} else {
|
||||
if aIsDirectory {
|
||||
return bPath.IsUnder(aPath)
|
||||
} else {
|
||||
return aPath == bPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractPathsFromMetadata(resp *filer_pb.SubscribeMetadataResponse) (path, newPath util.FullPath, isDirectory bool) {
|
||||
func extractPathsFromMetadata(resp *filer_pb.SubscribeMetadataResponse) (p, newPath util.FullPath, isDirectory bool) {
|
||||
oldEntry := resp.EventNotification.OldEntry
|
||||
newEntry := resp.EventNotification.NewEntry
|
||||
// create
|
||||
if filer_pb.IsCreate(resp) {
|
||||
path = util.FullPath(resp.Directory).Child(newEntry.Name)
|
||||
p = util.FullPath(resp.Directory).Child(newEntry.Name)
|
||||
isDirectory = newEntry.IsDirectory
|
||||
return
|
||||
}
|
||||
if filer_pb.IsDelete(resp) {
|
||||
path = util.FullPath(resp.Directory).Child(oldEntry.Name)
|
||||
p = util.FullPath(resp.Directory).Child(oldEntry.Name)
|
||||
isDirectory = oldEntry.IsDirectory
|
||||
return
|
||||
}
|
||||
if filer_pb.IsUpdate(resp) {
|
||||
path = util.FullPath(resp.Directory).Child(newEntry.Name)
|
||||
p = util.FullPath(resp.Directory).Child(newEntry.Name)
|
||||
isDirectory = newEntry.IsDirectory
|
||||
return
|
||||
}
|
||||
// renaming
|
||||
path = util.FullPath(resp.Directory).Child(oldEntry.Name)
|
||||
p = util.FullPath(resp.Directory).Child(oldEntry.Name)
|
||||
isDirectory = oldEntry.IsDirectory
|
||||
newPath = util.FullPath(resp.EventNotification.NewParentPath).Child(newEntry.Name)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
func makeResp(dir, name string, isDir bool, tsNs int64, isNew bool) *filer_pb.SubscribeMetadataResponse {
|
||||
resp := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: dir,
|
||||
TsNs: tsNs,
|
||||
EventNotification: &filer_pb.EventNotification{},
|
||||
}
|
||||
entry := &filer_pb.Entry{
|
||||
Name: name,
|
||||
IsDirectory: isDir,
|
||||
}
|
||||
if isNew {
|
||||
resp.EventNotification.NewEntry = entry
|
||||
} else {
|
||||
resp.EventNotification.OldEntry = entry
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func makeRenameResp(oldDir, oldName, newDir, newName string, isDir bool, tsNs int64) *filer_pb.SubscribeMetadataResponse {
|
||||
return &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: oldDir,
|
||||
TsNs: tsNs,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{
|
||||
Name: oldName,
|
||||
IsDirectory: isDir,
|
||||
},
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: newName,
|
||||
IsDirectory: isDir,
|
||||
},
|
||||
NewParentPath: newDir,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathAncestors(t *testing.T) {
|
||||
tests := []struct {
|
||||
path util.FullPath
|
||||
expected []util.FullPath
|
||||
}{
|
||||
{"/a/b/c/file.txt", []util.FullPath{"/a/b/c", "/a/b", "/a", "/"}},
|
||||
{"/a/b", []util.FullPath{"/a", "/"}},
|
||||
{"/a", []util.FullPath{"/"}},
|
||||
{"/", nil},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := pathAncestors(tt.path)
|
||||
if len(got) != len(tt.expected) {
|
||||
t.Errorf("pathAncestors(%q) = %v, want %v", tt.path, got, tt.expected)
|
||||
continue
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.expected[i] {
|
||||
t.Errorf("pathAncestors(%q)[%d] = %q, want %q", tt.path, i, got[i], tt.expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileVsFileConflict verifies that two file operations on the same path conflict,
|
||||
// and on different paths do not.
|
||||
func TestFileVsFileConflict(t *testing.T) {
|
||||
noop := func(resp *filer_pb.SubscribeMetadataResponse) error { return nil }
|
||||
p := NewMetadataProcessor(noop, 100, 0)
|
||||
|
||||
// Add a file job
|
||||
active := makeResp("/dir1", "file.txt", false, 1, true)
|
||||
path, newPath, isDir := extractPathsFromMetadata(active)
|
||||
p.activeJobs[active.TsNs] = &syncJobPaths{path: path, newPath: newPath, isDirectory: isDir}
|
||||
p.addPathToIndex(path, isDir)
|
||||
|
||||
// Same file should conflict
|
||||
same := makeResp("/dir1", "file.txt", false, 2, true)
|
||||
if !p.conflictsWith(same) {
|
||||
t.Error("expected conflict for same file path")
|
||||
}
|
||||
|
||||
// Different file should not conflict
|
||||
diff := makeResp("/dir1", "other.txt", false, 3, true)
|
||||
if p.conflictsWith(diff) {
|
||||
t.Error("unexpected conflict for different file path")
|
||||
}
|
||||
|
||||
// File in different directory should not conflict
|
||||
diffDir := makeResp("/dir2", "file.txt", false, 4, true)
|
||||
if p.conflictsWith(diffDir) {
|
||||
t.Error("unexpected conflict for file in different directory")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileUnderActiveDirConflict verifies that a file under an active directory operation
|
||||
// conflicts, but a file outside does not.
|
||||
func TestFileUnderActiveDirConflict(t *testing.T) {
|
||||
noop := func(resp *filer_pb.SubscribeMetadataResponse) error { return nil }
|
||||
p := NewMetadataProcessor(noop, 100, 0)
|
||||
|
||||
// Add a directory job at /dir1
|
||||
active := makeResp("/", "dir1", true, 1, true)
|
||||
path, newPath, isDir := extractPathsFromMetadata(active)
|
||||
p.activeJobs[active.TsNs] = &syncJobPaths{path: path, newPath: newPath, isDirectory: isDir}
|
||||
p.addPathToIndex(path, isDir)
|
||||
|
||||
// File under /dir1 should conflict
|
||||
under := makeResp("/dir1", "file.txt", false, 2, true)
|
||||
if !p.conflictsWith(under) {
|
||||
t.Error("expected conflict for file under active directory")
|
||||
}
|
||||
|
||||
// File deeply nested under /dir1 should conflict
|
||||
deep := makeResp("/dir1/sub/deep", "file.txt", false, 3, true)
|
||||
if !p.conflictsWith(deep) {
|
||||
t.Error("expected conflict for deeply nested file under active directory")
|
||||
}
|
||||
|
||||
// File in /dir2 should not conflict
|
||||
outside := makeResp("/dir2", "file.txt", false, 4, true)
|
||||
if p.conflictsWith(outside) {
|
||||
t.Error("unexpected conflict for file outside active directory")
|
||||
}
|
||||
|
||||
// File at /dir1 itself (not under, at) should not conflict
|
||||
// because IsUnder is strict: "/dir1".IsUnder("/dir1") == false
|
||||
atSame := makeResp("/", "dir1", false, 5, true)
|
||||
if p.conflictsWith(atSame) {
|
||||
t.Error("unexpected conflict for file at same path as directory (IsUnder is strict)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDirWithActiveFileUnder verifies that a directory operation conflicts when
|
||||
// there are active file jobs under it.
|
||||
func TestDirWithActiveFileUnder(t *testing.T) {
|
||||
noop := func(resp *filer_pb.SubscribeMetadataResponse) error { return nil }
|
||||
p := NewMetadataProcessor(noop, 100, 0)
|
||||
|
||||
// Add file jobs under /dir1
|
||||
f1 := makeResp("/dir1/sub", "file.txt", false, 1, true)
|
||||
path, newPath, isDir := extractPathsFromMetadata(f1)
|
||||
p.activeJobs[f1.TsNs] = &syncJobPaths{path: path, newPath: newPath, isDirectory: isDir}
|
||||
p.addPathToIndex(path, isDir)
|
||||
|
||||
// Directory /dir1 should conflict (has active file under it)
|
||||
dirOp := makeResp("/", "dir1", true, 2, true)
|
||||
if !p.conflictsWith(dirOp) {
|
||||
t.Error("expected conflict for directory with active file under it")
|
||||
}
|
||||
|
||||
// Directory /dir2 should not conflict
|
||||
dirOp2 := makeResp("/", "dir2", true, 3, true)
|
||||
if p.conflictsWith(dirOp2) {
|
||||
t.Error("unexpected conflict for directory with no active jobs under it")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDirVsDirConflict verifies ancestor/descendant directory conflict detection.
|
||||
func TestDirVsDirConflict(t *testing.T) {
|
||||
noop := func(resp *filer_pb.SubscribeMetadataResponse) error { return nil }
|
||||
p := NewMetadataProcessor(noop, 100, 0)
|
||||
|
||||
// Add directory job at /a/b
|
||||
active := makeResp("/a", "b", true, 1, true)
|
||||
path, newPath, isDir := extractPathsFromMetadata(active)
|
||||
p.activeJobs[active.TsNs] = &syncJobPaths{path: path, newPath: newPath, isDirectory: isDir}
|
||||
p.addPathToIndex(path, isDir)
|
||||
|
||||
// /a/b/c (descendant) should conflict
|
||||
desc := makeResp("/a/b", "c", true, 2, true)
|
||||
if !p.conflictsWith(desc) {
|
||||
t.Error("expected conflict for descendant directory")
|
||||
}
|
||||
|
||||
// /a (ancestor) should conflict
|
||||
anc := makeResp("/", "a", true, 3, true)
|
||||
if !p.conflictsWith(anc) {
|
||||
t.Error("expected conflict for ancestor directory")
|
||||
}
|
||||
|
||||
// Same directory should NOT conflict (IsUnder is strict, not equal)
|
||||
same := makeResp("/a", "b", true, 4, true)
|
||||
if p.conflictsWith(same) {
|
||||
t.Error("unexpected conflict for same directory (IsUnder is strict)")
|
||||
}
|
||||
|
||||
// Sibling directory should not conflict
|
||||
sibling := makeResp("/a", "c", true, 5, true)
|
||||
if p.conflictsWith(sibling) {
|
||||
t.Error("unexpected conflict for sibling directory")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRenameConflict verifies that rename events with two paths check both paths.
|
||||
func TestRenameConflict(t *testing.T) {
|
||||
noop := func(resp *filer_pb.SubscribeMetadataResponse) error { return nil }
|
||||
p := NewMetadataProcessor(noop, 100, 0)
|
||||
|
||||
// Add file job at /dir1/file.txt
|
||||
f1 := makeResp("/dir1", "file.txt", false, 1, true)
|
||||
path, newPath, isDir := extractPathsFromMetadata(f1)
|
||||
p.activeJobs[f1.TsNs] = &syncJobPaths{path: path, newPath: newPath, isDirectory: isDir}
|
||||
p.addPathToIndex(path, isDir)
|
||||
|
||||
// Rename from /dir2/a.txt to /dir1/file.txt should conflict (newPath matches)
|
||||
rename := makeRenameResp("/dir2", "a.txt", "/dir1", "file.txt", false, 2)
|
||||
if !p.conflictsWith(rename) {
|
||||
t.Error("expected conflict for rename whose destination matches active file")
|
||||
}
|
||||
|
||||
// Rename from /dir1/file.txt to /dir2/b.txt should conflict (oldPath matches)
|
||||
rename2 := makeRenameResp("/dir1", "file.txt", "/dir2", "b.txt", false, 3)
|
||||
if !p.conflictsWith(rename2) {
|
||||
t.Error("expected conflict for rename whose source matches active file")
|
||||
}
|
||||
|
||||
// Rename between unrelated paths should not conflict
|
||||
rename3 := makeRenameResp("/dir3", "x.txt", "/dir4", "y.txt", false, 4)
|
||||
if p.conflictsWith(rename3) {
|
||||
t.Error("unexpected conflict for rename between unrelated paths")
|
||||
}
|
||||
}
|
||||
|
||||
// TestActiveRenameConflict verifies that an active rename job registers both paths.
|
||||
func TestActiveRenameConflict(t *testing.T) {
|
||||
noop := func(resp *filer_pb.SubscribeMetadataResponse) error { return nil }
|
||||
p := NewMetadataProcessor(noop, 100, 0)
|
||||
|
||||
// Add active rename job: /dir1/old.txt -> /dir2/new.txt
|
||||
rename := makeRenameResp("/dir1", "old.txt", "/dir2", "new.txt", false, 1)
|
||||
path, newPath, isDir := extractPathsFromMetadata(rename)
|
||||
p.activeJobs[rename.TsNs] = &syncJobPaths{path: path, newPath: newPath, isDirectory: isDir}
|
||||
p.addPathToIndex(path, isDir)
|
||||
if newPath != "" {
|
||||
p.addPathToIndex(newPath, isDir)
|
||||
}
|
||||
|
||||
// File at /dir1/old.txt should conflict
|
||||
f1 := makeResp("/dir1", "old.txt", false, 2, true)
|
||||
if !p.conflictsWith(f1) {
|
||||
t.Error("expected conflict at rename source path")
|
||||
}
|
||||
|
||||
// File at /dir2/new.txt should conflict
|
||||
f2 := makeResp("/dir2", "new.txt", false, 3, true)
|
||||
if !p.conflictsWith(f2) {
|
||||
t.Error("expected conflict at rename destination path")
|
||||
}
|
||||
|
||||
// File at unrelated path should not conflict
|
||||
f3 := makeResp("/dir3", "other.txt", false, 4, true)
|
||||
if p.conflictsWith(f3) {
|
||||
t.Error("unexpected conflict at unrelated path")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRootDirConflict verifies that an active job at / conflicts with everything.
|
||||
func TestRootDirConflict(t *testing.T) {
|
||||
noop := func(resp *filer_pb.SubscribeMetadataResponse) error { return nil }
|
||||
p := NewMetadataProcessor(noop, 100, 0)
|
||||
|
||||
// Add directory job at /
|
||||
// Note: a dir entry at "/" would be created as FullPath("/").Child("somedir")
|
||||
// But let's test what happens with an active dir at /some/path and check root
|
||||
active := makeResp("/some", "dir", true, 1, true)
|
||||
path, newPath, isDir := extractPathsFromMetadata(active)
|
||||
p.activeJobs[active.TsNs] = &syncJobPaths{path: path, newPath: newPath, isDirectory: isDir}
|
||||
p.addPathToIndex(path, isDir)
|
||||
|
||||
// Root dir should conflict because active dir /some/dir is under /
|
||||
// A new directory at "/" should see descendantCount["/"] > 0
|
||||
if p.descendantCount["/"] <= 0 {
|
||||
t.Error("expected descendantCount['/'] > 0 for active job under root")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIndexCleanup verifies that removing a job properly cleans up all indexes.
|
||||
func TestIndexCleanup(t *testing.T) {
|
||||
noop := func(resp *filer_pb.SubscribeMetadataResponse) error { return nil }
|
||||
p := NewMetadataProcessor(noop, 100, 0)
|
||||
|
||||
// Add then remove a file job
|
||||
path := util.FullPath("/a/b/c/file.txt")
|
||||
p.addPathToIndex(path, false)
|
||||
|
||||
if p.activeFilePaths[path] != 1 {
|
||||
t.Errorf("expected activeFilePaths count 1, got %d", p.activeFilePaths[path])
|
||||
}
|
||||
if p.descendantCount["/a/b/c"] != 1 {
|
||||
t.Errorf("expected descendantCount['/a/b/c'] = 1, got %d", p.descendantCount["/a/b/c"])
|
||||
}
|
||||
|
||||
p.removePathFromIndex(path, false)
|
||||
|
||||
if len(p.activeFilePaths) != 0 {
|
||||
t.Errorf("expected empty activeFilePaths after removal, got %v", p.activeFilePaths)
|
||||
}
|
||||
if len(p.descendantCount) != 0 {
|
||||
t.Errorf("expected empty descendantCount after removal, got %v", p.descendantCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWatermarkWithHeap verifies watermark advancement using the min-heap.
|
||||
func TestWatermarkWithHeap(t *testing.T) {
|
||||
noop := func(resp *filer_pb.SubscribeMetadataResponse) error { return nil }
|
||||
p := NewMetadataProcessor(noop, 100, 0)
|
||||
|
||||
// Simulate adding jobs in order
|
||||
for _, ts := range []int64{10, 20, 30} {
|
||||
jobPath := util.FullPath("/file" + string(rune('0'+ts/10)))
|
||||
p.activeJobs[ts] = &syncJobPaths{path: jobPath, isDirectory: false}
|
||||
p.addPathToIndex(jobPath, false)
|
||||
heap.Push(&p.tsHeap, ts)
|
||||
}
|
||||
|
||||
if p.tsHeap[0] != 10 {
|
||||
t.Errorf("expected heap min=10, got %d", p.tsHeap[0])
|
||||
}
|
||||
|
||||
// Remove non-oldest (ts=20) — heap top should stay 10
|
||||
delete(p.activeJobs, 20)
|
||||
p.removePathFromIndex("/file2", false)
|
||||
// Lazy clean: top is 10 which is still active, so no pop
|
||||
for p.tsHeap.Len() > 0 {
|
||||
if _, active := p.activeJobs[p.tsHeap[0]]; active {
|
||||
break
|
||||
}
|
||||
heap.Pop(&p.tsHeap)
|
||||
}
|
||||
if p.tsHeap[0] != 10 {
|
||||
t.Errorf("expected heap min=10 after removing 20, got %d", p.tsHeap[0])
|
||||
}
|
||||
|
||||
// Remove oldest (ts=10) — lazy clean should find 30
|
||||
delete(p.activeJobs, 10)
|
||||
p.removePathFromIndex("/file1", false)
|
||||
for p.tsHeap.Len() > 0 {
|
||||
if _, active := p.activeJobs[p.tsHeap[0]]; active {
|
||||
break
|
||||
}
|
||||
heap.Pop(&p.tsHeap)
|
||||
}
|
||||
if p.tsHeap.Len() != 1 || p.tsHeap[0] != 30 {
|
||||
t.Errorf("expected heap min=30 after removing 10 and 20, got len=%d", p.tsHeap.Len())
|
||||
}
|
||||
}
|
||||
|
||||
// benchResult prevents the compiler from optimizing away the conflict check.
|
||||
var benchResult bool
|
||||
|
||||
// BenchmarkConflictCheck measures conflict check cost with varying active job counts.
|
||||
// With the index-based approach, cost should be O(depth) regardless of job count.
|
||||
func BenchmarkConflictCheck(b *testing.B) {
|
||||
for _, numJobs := range []int{32, 256, 1024} {
|
||||
b.Run(fmt.Sprintf("jobs=%d", numJobs), func(b *testing.B) {
|
||||
noop := func(resp *filer_pb.SubscribeMetadataResponse) error { return nil }
|
||||
p := NewMetadataProcessor(noop, numJobs+1, 0)
|
||||
|
||||
// Fill with active jobs in different directories
|
||||
for i := range numJobs {
|
||||
dir := fmt.Sprintf("/dir%d/sub%d", i/100, i%100)
|
||||
name := fmt.Sprintf("file%d.txt", i)
|
||||
resp := makeResp(dir, name, false, int64(i+1), true)
|
||||
path, newPath, isDir := extractPathsFromMetadata(resp)
|
||||
p.activeJobs[resp.TsNs] = &syncJobPaths{path: path, newPath: newPath, isDirectory: isDir}
|
||||
p.addPathToIndex(path, isDir)
|
||||
}
|
||||
|
||||
// Benchmark conflict check for a non-conflicting event
|
||||
probe := makeResp("/other/path", "test.txt", false, int64(numJobs+1), true)
|
||||
var r bool
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
r = p.conflictsWith(probe)
|
||||
}
|
||||
benchResult = r
|
||||
})
|
||||
}
|
||||
}
|
||||
+32
-30
@@ -252,7 +252,7 @@ func RunMount(option *MountOptions, umask os.FileMode) bool {
|
||||
SingleThreaded: false,
|
||||
DisableXAttrs: *option.disableXAttr,
|
||||
Debug: *option.debug,
|
||||
EnableLocks: false,
|
||||
EnableLocks: true,
|
||||
ExplicitDataCacheControl: false,
|
||||
DirectMount: true,
|
||||
DirectMountFlags: 0,
|
||||
@@ -312,35 +312,37 @@ func RunMount(option *MountOptions, umask os.FileMode) bool {
|
||||
}
|
||||
|
||||
seaweedFileSystem := mount.NewSeaweedFileSystem(&mount.Option{
|
||||
MountDirectory: dir,
|
||||
FilerAddresses: filerAddresses,
|
||||
GrpcDialOption: grpcDialOption,
|
||||
FilerMountRootPath: mountRoot,
|
||||
Collection: *option.collection,
|
||||
Replication: *option.replication,
|
||||
TtlSec: int32(*option.ttlSec),
|
||||
DiskType: types.ToDiskType(*option.diskType),
|
||||
ChunkSizeLimit: int64(chunkSizeLimitMB) * 1024 * 1024,
|
||||
ConcurrentWriters: *option.concurrentWriters,
|
||||
ConcurrentReaders: *option.concurrentReaders,
|
||||
CacheDirForRead: *option.cacheDirForRead,
|
||||
CacheSizeMBForRead: *option.cacheSizeMBForRead,
|
||||
CacheDirForWrite: cacheDirForWrite,
|
||||
CacheMetaTTlSec: *option.cacheMetaTtlSec,
|
||||
DataCenter: *option.dataCenter,
|
||||
Quota: int64(*option.collectionQuota) * 1024 * 1024,
|
||||
MountUid: uid,
|
||||
MountGid: gid,
|
||||
MountMode: mountMode,
|
||||
MountCtime: fileInfo.ModTime(),
|
||||
MountMtime: time.Now(),
|
||||
Umask: umask,
|
||||
VolumeServerAccess: *mountOptions.volumeServerAccess,
|
||||
Cipher: cipher,
|
||||
UidGidMapper: uidGidMapper,
|
||||
DisableXAttr: *option.disableXAttr,
|
||||
IsMacOs: runtime.GOOS == "darwin",
|
||||
MetadataFlushSeconds: *option.metadataFlushSeconds,
|
||||
MountDirectory: dir,
|
||||
FilerAddresses: filerAddresses,
|
||||
GrpcDialOption: grpcDialOption,
|
||||
FilerSigningKey: security.SigningKey(util.GetViper().GetString("jwt.filer_signing.key")),
|
||||
FilerSigningExpiresAfterSec: util.GetViper().GetInt("jwt.filer_signing.expires_after_seconds"),
|
||||
FilerMountRootPath: mountRoot,
|
||||
Collection: *option.collection,
|
||||
Replication: *option.replication,
|
||||
TtlSec: int32(*option.ttlSec),
|
||||
DiskType: types.ToDiskType(*option.diskType),
|
||||
ChunkSizeLimit: int64(chunkSizeLimitMB) * 1024 * 1024,
|
||||
ConcurrentWriters: *option.concurrentWriters,
|
||||
ConcurrentReaders: *option.concurrentReaders,
|
||||
CacheDirForRead: *option.cacheDirForRead,
|
||||
CacheSizeMBForRead: *option.cacheSizeMBForRead,
|
||||
CacheDirForWrite: cacheDirForWrite,
|
||||
CacheMetaTTlSec: *option.cacheMetaTtlSec,
|
||||
DataCenter: *option.dataCenter,
|
||||
Quota: int64(*option.collectionQuota) * 1024 * 1024,
|
||||
MountUid: uid,
|
||||
MountGid: gid,
|
||||
MountMode: mountMode,
|
||||
MountCtime: fileInfo.ModTime(),
|
||||
MountMtime: time.Now(),
|
||||
Umask: umask,
|
||||
VolumeServerAccess: *mountOptions.volumeServerAccess,
|
||||
Cipher: cipher,
|
||||
UidGidMapper: uidGidMapper,
|
||||
DisableXAttr: *option.disableXAttr,
|
||||
IsMacOs: runtime.GOOS == "darwin",
|
||||
MetadataFlushSeconds: *option.metadataFlushSeconds,
|
||||
// RDMA acceleration options
|
||||
RdmaEnabled: *option.rdmaEnabled,
|
||||
RdmaSidecarAddr: *option.rdmaSidecarAddr,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package filer
|
||||
|
||||
const (
|
||||
CopyQueryParamFrom = "cp.from"
|
||||
CopyQueryParamOverwrite = "overwrite"
|
||||
CopyQueryParamDataOnly = "dataOnly"
|
||||
CopyQueryParamRequestID = "copy.requestId"
|
||||
CopyQueryParamSourceInode = "copy.srcInode"
|
||||
CopyQueryParamSourceMtime = "copy.srcMtime"
|
||||
CopyQueryParamSourceSize = "copy.srcSize"
|
||||
CopyQueryParamDestinationInode = "copy.dstInode"
|
||||
CopyQueryParamDestinationMtime = "copy.dstMtime"
|
||||
CopyQueryParamDestinationSize = "copy.dstSize"
|
||||
|
||||
CopyResponseHeaderCommitted = "X-SeaweedFS-Copy-Committed"
|
||||
CopyResponseHeaderRequestID = "X-SeaweedFS-Copy-Request-ID"
|
||||
)
|
||||
@@ -308,7 +308,7 @@ func (efc *EmptyFolderCleaner) executeCleanup(folder string, triggeredBy string)
|
||||
efc.mu.Unlock()
|
||||
|
||||
if count > 0 {
|
||||
glog.Infof("EmptyFolderCleaner: folder %s (triggered by %s) has %d items, not empty", folder, triggeredBy, count)
|
||||
glog.V(4).Infof("EmptyFolderCleaner: folder %s (triggered by %s) has %d items, not empty", folder, triggeredBy, count)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+15
-7
@@ -81,7 +81,7 @@ func NewFiler(masters pb.ServerDiscovery, grpcDialOption grpc.DialOption, filerH
|
||||
f.UniqueFilerId = -f.UniqueFilerId
|
||||
}
|
||||
|
||||
f.LocalMetaLogBuffer = log_buffer.NewLogBuffer("local", LogFlushInterval, f.logFlushFunc, nil, notifyFn)
|
||||
f.LocalMetaLogBuffer = log_buffer.NewLogBuffer("local", LogFlushInterval, f.logFlushFunc, f.readPersistedLogBufferPosition, notifyFn)
|
||||
f.metaLogCollection = collection
|
||||
f.metaLogReplication = replication
|
||||
|
||||
@@ -203,7 +203,7 @@ func (f *Filer) CreateEntry(ctx context.Context, entry *Entry, o_excl bool, isFr
|
||||
}
|
||||
|
||||
if entry.FullPath.IsLongerFileName(maxFilenameLength) {
|
||||
return fmt.Errorf("entry name too long")
|
||||
return filer_pb.ErrEntryNameTooLong
|
||||
}
|
||||
|
||||
if entry.IsDirectory() {
|
||||
@@ -237,7 +237,7 @@ func (f *Filer) CreateEntry(ctx context.Context, entry *Entry, o_excl bool, isFr
|
||||
} else {
|
||||
if o_excl {
|
||||
glog.V(3).InfofCtx(ctx, "EEXIST: entry %s already exists", entry.FullPath)
|
||||
return fmt.Errorf("EEXIST: entry %s already exists", entry.FullPath)
|
||||
return fmt.Errorf("%s: %w", entry.FullPath, filer_pb.ErrEntryAlreadyExists)
|
||||
}
|
||||
glog.V(4).InfofCtx(ctx, "UpdateEntry %s: old entry: %v", entry.FullPath, oldEntry.Name())
|
||||
if err := f.UpdateEntry(ctx, oldEntry, entry); err != nil {
|
||||
@@ -323,8 +323,16 @@ func (f *Filer) ensureParentDirectoryEntry(ctx context.Context, entry *Entry, di
|
||||
}
|
||||
|
||||
} else if !dirEntry.IsDirectory() {
|
||||
glog.ErrorfCtx(ctx, "CreateEntry %s: %s should be a directory", entry.FullPath, dirPath)
|
||||
return fmt.Errorf("%s is a file", dirPath)
|
||||
// S3 allows both "foo/bar" (object) and "foo/bar/xyzzy" (another
|
||||
// object) to coexist because S3 has a flat key space. Promote the
|
||||
// existing file to a directory, preserving its content/chunks so
|
||||
// the original object data remains accessible.
|
||||
glog.V(2).InfofCtx(ctx, "promoting %s from file to directory for %s", dirPath, entry.FullPath)
|
||||
dirEntry.Attr.Mode |= os.ModeDir | 0111
|
||||
if updateErr := f.Store.UpdateEntry(ctx, dirEntry); updateErr != nil {
|
||||
return fmt.Errorf("promote %s to directory: %v", dirPath, updateErr)
|
||||
}
|
||||
f.NotifyUpdateEvent(ctx, nil, dirEntry, false, isFromOtherCluster, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -335,11 +343,11 @@ func (f *Filer) UpdateEntry(ctx context.Context, oldEntry, entry *Entry) (err er
|
||||
entry.Attr.Crtime = oldEntry.Attr.Crtime
|
||||
if oldEntry.IsDirectory() && !entry.IsDirectory() {
|
||||
glog.ErrorfCtx(ctx, "existing %s is a directory", oldEntry.FullPath)
|
||||
return fmt.Errorf("existing %s is a directory", oldEntry.FullPath)
|
||||
return fmt.Errorf("%s: %w", oldEntry.FullPath, filer_pb.ErrExistingIsDirectory)
|
||||
}
|
||||
if !oldEntry.IsDirectory() && entry.IsDirectory() {
|
||||
glog.ErrorfCtx(ctx, "existing %s is a file", oldEntry.FullPath)
|
||||
return fmt.Errorf("existing %s is a file", oldEntry.FullPath)
|
||||
return fmt.Errorf("%s: %w", oldEntry.FullPath, filer_pb.ErrExistingIsFile)
|
||||
}
|
||||
}
|
||||
return f.Store.UpdateEntry(ctx, entry)
|
||||
|
||||
@@ -2,8 +2,10 @@ package filer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
nethttp "net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/notification"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
||||
)
|
||||
|
||||
func (f *Filer) NotifyUpdateEvent(ctx context.Context, oldEntry, newEntry *Entry, deleteChunks, isFromOtherCluster bool, signatures []int32) {
|
||||
@@ -174,6 +177,7 @@ func (f *Filer) logFlushFunc(logBuffer *log_buffer.LogBuffer, startTime, stopTim
|
||||
var (
|
||||
volumeNotFoundPattern = regexp.MustCompile(`volume \d+? not found`)
|
||||
chunkNotFoundPattern = regexp.MustCompile(`(urls not found|File Not Found)`)
|
||||
httpNotFoundPattern = regexp.MustCompile(`404 Not Found: not found`)
|
||||
)
|
||||
|
||||
// isChunkNotFoundError checks if the error indicates that a volume or chunk
|
||||
@@ -183,8 +187,13 @@ func isChunkNotFoundError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, util_http.ErrNotFound) || errors.Is(err, nethttp.ErrMissingFile) {
|
||||
return true
|
||||
}
|
||||
errMsg := err.Error()
|
||||
return volumeNotFoundPattern.MatchString(errMsg) || chunkNotFoundPattern.MatchString(errMsg)
|
||||
return volumeNotFoundPattern.MatchString(errMsg) ||
|
||||
chunkNotFoundPattern.MatchString(errMsg) ||
|
||||
httpNotFoundPattern.MatchString(errMsg)
|
||||
}
|
||||
|
||||
func (f *Filer) ReadPersistedLogBuffer(startPosition log_buffer.MessagePosition, stopTsNs int64, eachLogEntryFn log_buffer.EachLogEntryFuncType) (lastTsNs int64, isDone bool, err error) {
|
||||
@@ -220,3 +229,17 @@ func (f *Filer) ReadPersistedLogBuffer(startPosition log_buffer.MessagePosition,
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (f *Filer) readPersistedLogBufferPosition(startPosition log_buffer.MessagePosition, stopTsNs int64, eachLogEntryFn log_buffer.EachLogEntryFuncType) (lastReadPosition log_buffer.MessagePosition, isDone bool, err error) {
|
||||
lastReadPosition = startPosition
|
||||
|
||||
lastTsNs, isDone, err := f.ReadPersistedLogBuffer(startPosition, stopTsNs, eachLogEntryFn)
|
||||
if err != nil {
|
||||
return lastReadPosition, isDone, err
|
||||
}
|
||||
if lastTsNs != 0 {
|
||||
lastReadPosition = log_buffer.NewMessagePosition(lastTsNs, 1)
|
||||
}
|
||||
|
||||
return lastReadPosition, isDone, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package filer
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
@@ -17,6 +18,8 @@ func (fsw *FilerStoreWrapper) handleUpdateToHardLinks(ctx context.Context, entry
|
||||
|
||||
if len(entry.HardLinkId) > 0 {
|
||||
// handle hard links
|
||||
glog.V(4).InfofCtx(ctx, "handleUpdateToHardLinks %s HardLinkId %x counter=%d chunks=%d",
|
||||
entry.FullPath, entry.HardLinkId, entry.HardLinkCounter, len(entry.GetChunks()))
|
||||
if err := fsw.setHardLink(ctx, entry); err != nil {
|
||||
return fmt.Errorf("setHardLink %d: %v", entry.HardLinkId, err)
|
||||
}
|
||||
@@ -51,7 +54,8 @@ func (fsw *FilerStoreWrapper) setHardLink(ctx context.Context, entry *Entry) err
|
||||
return encodeErr
|
||||
}
|
||||
|
||||
glog.V(4).InfofCtx(ctx, "setHardLink %v nlink:%d", entry.FullPath, entry.HardLinkCounter)
|
||||
glog.V(4).InfofCtx(ctx, "setHardLink KvPut %s HardLinkId %x nlink:%d blobLen=%d",
|
||||
entry.FullPath, entry.HardLinkId, entry.HardLinkCounter, len(newBlob))
|
||||
|
||||
return fsw.KvPut(ctx, key, newBlob)
|
||||
}
|
||||
@@ -62,26 +66,35 @@ func (fsw *FilerStoreWrapper) maybeReadHardLink(ctx context.Context, entry *Entr
|
||||
}
|
||||
key := entry.HardLinkId
|
||||
|
||||
glog.V(1).InfofCtx(ctx, "maybeReadHardLink %s HardLinkId %x", entry.FullPath, entry.HardLinkId)
|
||||
|
||||
value, err := fsw.KvGet(ctx, key)
|
||||
if err != nil {
|
||||
glog.ErrorfCtx(ctx, "read %s hardlink %d: %v", entry.FullPath, entry.HardLinkId, err)
|
||||
if errors.Is(err, ErrKvNotFound) {
|
||||
glog.V(4).InfofCtx(ctx, "maybeReadHardLink %s HardLinkId %x: not found", entry.FullPath, entry.HardLinkId)
|
||||
} else {
|
||||
glog.ErrorfCtx(ctx, "read %s HardLinkId %x: %v", entry.FullPath, entry.HardLinkId, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err = entry.DecodeAttributesAndChunks(value); err != nil {
|
||||
glog.ErrorfCtx(ctx, "decode %s hardlink %d: %v", entry.FullPath, entry.HardLinkId, err)
|
||||
glog.ErrorfCtx(ctx, "decode %s HardLinkId %x: %v", entry.FullPath, entry.HardLinkId, err)
|
||||
return err
|
||||
}
|
||||
|
||||
glog.V(4).InfofCtx(ctx, "maybeReadHardLink %v nlink:%d", entry.FullPath, entry.HardLinkCounter)
|
||||
glog.V(1).InfofCtx(ctx, "maybeReadHardLink %s HardLinkId %x nlink:%d chunks=%d",
|
||||
entry.FullPath, entry.HardLinkId, entry.HardLinkCounter, len(entry.GetChunks()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fsw *FilerStoreWrapper) DeleteHardLink(ctx context.Context, hardLinkId HardLinkId) error {
|
||||
key := hardLinkId
|
||||
glog.V(4).InfofCtx(ctx, "DeleteHardLink HardLinkId %x", key)
|
||||
value, err := fsw.KvGet(ctx, key)
|
||||
if err == ErrKvNotFound {
|
||||
glog.V(4).InfofCtx(ctx, "DeleteHardLink HardLinkId %x: already gone", key)
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
@@ -95,7 +108,8 @@ func (fsw *FilerStoreWrapper) DeleteHardLink(ctx context.Context, hardLinkId Har
|
||||
|
||||
entry.HardLinkCounter--
|
||||
if entry.HardLinkCounter <= 0 {
|
||||
glog.V(4).InfofCtx(ctx, "DeleteHardLink KvDelete %v", key)
|
||||
glog.V(4).InfofCtx(ctx, "DeleteHardLink KvDelete HardLinkId %x counter reached %d",
|
||||
key, entry.HardLinkCounter)
|
||||
return fsw.KvDelete(ctx, key)
|
||||
}
|
||||
|
||||
@@ -104,7 +118,8 @@ func (fsw *FilerStoreWrapper) DeleteHardLink(ctx context.Context, hardLinkId Har
|
||||
return encodeErr
|
||||
}
|
||||
|
||||
glog.V(4).InfofCtx(ctx, "DeleteHardLink KvPut %v", key)
|
||||
glog.V(4).InfofCtx(ctx, "DeleteHardLink KvPut HardLinkId %x counter decremented to %d",
|
||||
key, entry.HardLinkCounter)
|
||||
return fsw.KvPut(ctx, key, newBlob)
|
||||
|
||||
}
|
||||
|
||||
@@ -130,6 +130,11 @@ func (fsw *FilerStoreWrapper) InsertEntry(ctx context.Context, entry *Entry) err
|
||||
filer_pb.BeforeEntrySerialization(entry.GetChunks())
|
||||
normalizeEntryMimeForStore(entry)
|
||||
|
||||
if len(entry.HardLinkId) > 0 {
|
||||
glog.V(4).InfofCtx(ctx, "InsertEntry %s has HardLinkId %x counter=%d",
|
||||
entry.FullPath, entry.HardLinkId, entry.HardLinkCounter)
|
||||
}
|
||||
|
||||
if err := fsw.handleUpdateToHardLinks(ctx, entry); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -150,6 +155,11 @@ func (fsw *FilerStoreWrapper) UpdateEntry(ctx context.Context, entry *Entry) err
|
||||
filer_pb.BeforeEntrySerialization(entry.GetChunks())
|
||||
normalizeEntryMimeForStore(entry)
|
||||
|
||||
if len(entry.HardLinkId) > 0 {
|
||||
glog.V(4).InfofCtx(ctx, "UpdateEntry %s has HardLinkId %x counter=%d",
|
||||
entry.FullPath, entry.HardLinkId, entry.HardLinkCounter)
|
||||
}
|
||||
|
||||
if err := fsw.handleUpdateToHardLinks(ctx, entry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -74,6 +74,18 @@ func (ma *MetaAggregator) OnPeerUpdate(update *master_pb.ClusterNodeUpdate, star
|
||||
}
|
||||
}
|
||||
|
||||
func (ma *MetaAggregator) HasRemotePeers() bool {
|
||||
ma.peerChansLock.Lock()
|
||||
defer ma.peerChansLock.Unlock()
|
||||
|
||||
for address := range ma.peerChans {
|
||||
if address != ma.self {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ma *MetaAggregator) loopSubscribeToOneFiler(f *Filer, self pb.ServerAddress, peer pb.ServerAddress, startFrom time.Time, stopChan chan struct{}) {
|
||||
lastTsNs := startFrom.UnixNano()
|
||||
for {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
@@ -31,10 +32,11 @@ type FileHandle struct {
|
||||
asyncFlushPending bool // set in writebackCache mode to defer flush to Release
|
||||
asyncFlushUid uint32 // saved uid for deferred metadata flush
|
||||
asyncFlushGid uint32 // saved gid for deferred metadata flush
|
||||
asyncFlushDir string // saved directory at defer time (fallback if inode forgotten)
|
||||
asyncFlushName string // saved file name at defer time (fallback if inode forgotten)
|
||||
savedDir string // last known parent path if inode-to-path state is forgotten
|
||||
savedName string // last known file name if inode-to-path state is forgotten
|
||||
|
||||
isDeleted bool
|
||||
isRenamed bool // set by Rename before waiting for async flush; skips old-path metadata flush
|
||||
|
||||
// RDMA chunk offset cache for performance optimization
|
||||
chunkOffsetCache []int64
|
||||
@@ -73,8 +75,20 @@ func newFileHandle(wfs *WFS, handleId FileHandleId, inode uint64, entry *filer_p
|
||||
}
|
||||
|
||||
func (fh *FileHandle) FullPath() util.FullPath {
|
||||
fp, _ := fh.wfs.inodeToPath.GetPath(fh.inode)
|
||||
return fp
|
||||
if fp, status := fh.wfs.inodeToPath.GetPath(fh.inode); status == fuse.OK {
|
||||
return fp
|
||||
}
|
||||
if fh.savedName != "" {
|
||||
return util.FullPath(fh.savedDir).Child(fh.savedName)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (fh *FileHandle) RememberPath(fullPath util.FullPath) {
|
||||
if fullPath == "" {
|
||||
return
|
||||
}
|
||||
fh.savedDir, fh.savedName = fullPath.DirAndName()
|
||||
}
|
||||
|
||||
func (fh *FileHandle) GetEntry() *LockedEntry {
|
||||
@@ -99,6 +113,13 @@ func (fh *FileHandle) SetEntry(entry *filer_pb.Entry) {
|
||||
fh.invalidateChunkCache()
|
||||
}
|
||||
|
||||
func (fh *FileHandle) ResetDirtyPages() {
|
||||
fh.dirtyPages.Destroy()
|
||||
fh.dirtyPages = newPageWriter(fh, fh.wfs.option.ChunkSizeLimit)
|
||||
fh.dirtyMetadata = false
|
||||
fh.contentType = ""
|
||||
}
|
||||
|
||||
func (fh *FileHandle) UpdateEntry(fn func(entry *filer_pb.Entry)) *filer_pb.Entry {
|
||||
result := fh.entry.UpdateEntry(fn)
|
||||
|
||||
|
||||
@@ -38,6 +38,17 @@ func (i *FileHandleToInode) FindFileHandle(inode uint64) (fh *FileHandle, found
|
||||
return
|
||||
}
|
||||
|
||||
// MarkInodeRenamed sets isRenamed on any file handle associated with the
|
||||
// given inode. This prevents the async flush from recreating a renamed
|
||||
// file's metadata under its old path.
|
||||
func (i *FileHandleToInode) MarkInodeRenamed(inode uint64) {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
if fh, ok := i.inode2fh[inode]; ok {
|
||||
fh.isRenamed = true
|
||||
}
|
||||
}
|
||||
|
||||
func (i *FileHandleToInode) AcquireFileHandle(wfs *WFS, inode uint64, entry *filer_pb.Entry) *FileHandle {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
func TestFileHandleFullPathFallsBackAfterForget(t *testing.T) {
|
||||
wfs := &WFS{
|
||||
inodeToPath: NewInodeToPath(util.FullPath("/"), 0),
|
||||
}
|
||||
|
||||
fullPath := util.FullPath("/worker_0/subdir_0/test.txt")
|
||||
inode := wfs.inodeToPath.Lookup(fullPath, 1, false, false, 0, true)
|
||||
|
||||
fh := &FileHandle{
|
||||
inode: inode,
|
||||
wfs: wfs,
|
||||
}
|
||||
fh.RememberPath(fullPath)
|
||||
|
||||
wfs.inodeToPath.Forget(inode, 1, nil)
|
||||
|
||||
if got := fh.FullPath(); got != fullPath {
|
||||
t.Fatalf("FullPath() after forget = %q, want %q", got, fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileHandleFullPathUsesSavedRenamePathAfterForget(t *testing.T) {
|
||||
wfs := &WFS{
|
||||
inodeToPath: NewInodeToPath(util.FullPath("/"), 0),
|
||||
}
|
||||
|
||||
oldPath := util.FullPath("/worker_0/subdir_0/test.txt")
|
||||
newPath := util.FullPath("/worker_0/subdir_1/test.txt")
|
||||
inode := wfs.inodeToPath.Lookup(oldPath, 1, false, false, 0, true)
|
||||
|
||||
fh := &FileHandle{
|
||||
inode: inode,
|
||||
wfs: wfs,
|
||||
}
|
||||
fh.RememberPath(oldPath)
|
||||
|
||||
wfs.inodeToPath.MovePath(oldPath, newPath)
|
||||
fh.RememberPath(newPath)
|
||||
wfs.inodeToPath.Forget(inode, 1, nil)
|
||||
|
||||
if got := fh.FullPath(); got != newPath {
|
||||
t.Fatalf("FullPath() after rename+forget = %q, want %q", got, newPath)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package mount
|
||||
|
||||
import "time"
|
||||
|
||||
const metadataFlushRetries = 3
|
||||
|
||||
var metadataFlushSleep = time.Sleep
|
||||
|
||||
func retryMetadataFlush(flush func() error, onRetry func(nextAttempt, totalAttempts int, backoff time.Duration, err error)) error {
|
||||
totalAttempts := metadataFlushRetries + 1
|
||||
var err error
|
||||
for attempt := 1; attempt <= totalAttempts; attempt++ {
|
||||
err = flush()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if attempt == totalAttempts {
|
||||
break
|
||||
}
|
||||
|
||||
backoff := time.Duration(1<<uint(attempt-1)) * time.Second
|
||||
if onRetry != nil {
|
||||
onRetry(attempt+1, totalAttempts, backoff, err)
|
||||
}
|
||||
metadataFlushSleep(backoff)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRetryMetadataFlushEventuallySucceeds(t *testing.T) {
|
||||
originalSleep := metadataFlushSleep
|
||||
t.Cleanup(func() {
|
||||
metadataFlushSleep = originalSleep
|
||||
})
|
||||
|
||||
var sleeps []time.Duration
|
||||
metadataFlushSleep = func(d time.Duration) {
|
||||
sleeps = append(sleeps, d)
|
||||
}
|
||||
|
||||
attempts := 0
|
||||
err := retryMetadataFlush(func() error {
|
||||
attempts++
|
||||
if attempts < 3 {
|
||||
return errors.New("temporary failure")
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("retryMetadataFlush returned error: %v", err)
|
||||
}
|
||||
|
||||
if attempts != 3 {
|
||||
t.Fatalf("attempts = %d, want 3", attempts)
|
||||
}
|
||||
|
||||
wantSleeps := []time.Duration{time.Second, 2 * time.Second}
|
||||
if len(sleeps) != len(wantSleeps) {
|
||||
t.Fatalf("sleep count = %d, want %d", len(sleeps), len(wantSleeps))
|
||||
}
|
||||
for i, want := range wantSleeps {
|
||||
if sleeps[i] != want {
|
||||
t.Fatalf("sleep[%d] = %v, want %v", i, sleeps[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryMetadataFlushReturnsLastError(t *testing.T) {
|
||||
originalSleep := metadataFlushSleep
|
||||
t.Cleanup(func() {
|
||||
metadataFlushSleep = originalSleep
|
||||
})
|
||||
|
||||
var sleeps []time.Duration
|
||||
metadataFlushSleep = func(d time.Duration) {
|
||||
sleeps = append(sleeps, d)
|
||||
}
|
||||
|
||||
expectedErr := errors.New("permanent failure")
|
||||
attempts := 0
|
||||
err := retryMetadataFlush(func() error {
|
||||
attempts++
|
||||
return expectedErr
|
||||
}, nil)
|
||||
if !errors.Is(err, expectedErr) {
|
||||
t.Fatalf("retryMetadataFlush error = %v, want %v", err, expectedErr)
|
||||
}
|
||||
|
||||
if attempts != metadataFlushRetries+1 {
|
||||
t.Fatalf("attempts = %d, want %d", attempts, metadataFlushRetries+1)
|
||||
}
|
||||
|
||||
wantSleeps := []time.Duration{time.Second, 2 * time.Second, 4 * time.Second}
|
||||
if len(sleeps) != len(wantSleeps) {
|
||||
t.Fatalf("sleep count = %d, want %d", len(sleeps), len(wantSleeps))
|
||||
}
|
||||
for i, want := range wantSleeps {
|
||||
if sleeps[i] != want {
|
||||
t.Fatalf("sleep[%d] = %v, want %v", i, sleeps[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
)
|
||||
|
||||
// lockRange represents a single held POSIX byte-range lock.
|
||||
type lockRange struct {
|
||||
Start uint64 // inclusive byte offset
|
||||
End uint64 // inclusive; math.MaxUint64 means "to EOF"
|
||||
Typ uint32 // syscall.F_RDLCK or syscall.F_WRLCK
|
||||
Owner uint64 // FUSE lock owner (from LkIn.Owner)
|
||||
Pid uint32 // PID of lock holder (for GetLk reporting)
|
||||
// flock and fcntl locks have different ownership and close semantics.
|
||||
// Keep them in separate namespaces inside the table.
|
||||
IsFlock bool
|
||||
}
|
||||
|
||||
// inodeLocks holds all locks for one inode plus a waiter queue for SetLkw.
|
||||
type inodeLocks struct {
|
||||
mu sync.Mutex
|
||||
locks []lockRange // currently held locks, sorted by Start
|
||||
waiters []*lockWaiter // blocked SetLkw callers
|
||||
wakeRefs int // woken waiters still retrying on this inodeLocks
|
||||
}
|
||||
|
||||
// lockWaiter represents a blocked SetLkw caller.
|
||||
type lockWaiter struct {
|
||||
requested lockRange // the lock this waiter is trying to acquire
|
||||
ch chan struct{} // closed when the waiter should re-check
|
||||
wakeRefHeld bool
|
||||
}
|
||||
|
||||
// PosixLockTable is the per-mount POSIX lock manager.
|
||||
type PosixLockTable struct {
|
||||
mu sync.Mutex
|
||||
inodes map[uint64]*inodeLocks
|
||||
}
|
||||
|
||||
func NewPosixLockTable() *PosixLockTable {
|
||||
return &PosixLockTable{
|
||||
inodes: make(map[uint64]*inodeLocks),
|
||||
}
|
||||
}
|
||||
|
||||
// getOrCreateInodeLocks returns the lock state for an inode, creating it if needed.
|
||||
func (plt *PosixLockTable) getOrCreateInodeLocks(inode uint64) *inodeLocks {
|
||||
plt.mu.Lock()
|
||||
defer plt.mu.Unlock()
|
||||
il, ok := plt.inodes[inode]
|
||||
if !ok {
|
||||
il = &inodeLocks{}
|
||||
plt.inodes[inode] = il
|
||||
}
|
||||
return il
|
||||
}
|
||||
|
||||
// getInodeLocks returns the lock state for an inode, or nil if none exists.
|
||||
func (plt *PosixLockTable) getInodeLocks(inode uint64) *inodeLocks {
|
||||
plt.mu.Lock()
|
||||
defer plt.mu.Unlock()
|
||||
return plt.inodes[inode]
|
||||
}
|
||||
|
||||
// maybeCleanupInode removes the inodeLocks entry if it has no locks, no waiters,
|
||||
// and no woken waiters still retrying against this inodeLocks.
|
||||
func (plt *PosixLockTable) maybeCleanupInode(inode uint64, il *inodeLocks) {
|
||||
// Caller must NOT hold il.mu. We acquire both locks in the correct order.
|
||||
plt.mu.Lock()
|
||||
defer plt.mu.Unlock()
|
||||
il.mu.Lock()
|
||||
defer il.mu.Unlock()
|
||||
if len(il.locks) == 0 && len(il.waiters) == 0 && il.wakeRefs == 0 {
|
||||
delete(plt.inodes, inode)
|
||||
}
|
||||
}
|
||||
|
||||
// rangesOverlap returns true if two inclusive ranges overlap.
|
||||
func rangesOverlap(aStart, aEnd, bStart, bEnd uint64) bool {
|
||||
return aStart <= bEnd && bStart <= aEnd
|
||||
}
|
||||
|
||||
// findConflict returns the first lock that conflicts with the proposed lock.
|
||||
// A conflict exists when ranges overlap, at least one is a write lock, and the owners differ.
|
||||
func findConflict(locks []lockRange, proposed lockRange) (lockRange, bool) {
|
||||
for _, h := range locks {
|
||||
if h.IsFlock != proposed.IsFlock {
|
||||
continue
|
||||
}
|
||||
if h.Owner == proposed.Owner {
|
||||
continue
|
||||
}
|
||||
if !rangesOverlap(h.Start, h.End, proposed.Start, proposed.End) {
|
||||
continue
|
||||
}
|
||||
if h.Typ == syscall.F_RDLCK && proposed.Typ == syscall.F_RDLCK {
|
||||
continue
|
||||
}
|
||||
return h, true
|
||||
}
|
||||
return lockRange{}, false
|
||||
}
|
||||
|
||||
// insertAndCoalesce inserts a lock for the given owner, replacing/splitting any
|
||||
// existing same-owner locks that overlap. Adjacent same-type locks are merged.
|
||||
// Caller must hold il.mu.
|
||||
func insertAndCoalesce(il *inodeLocks, lk lockRange) {
|
||||
owner := lk.Owner
|
||||
|
||||
var kept []lockRange
|
||||
for _, h := range il.locks {
|
||||
if h.Owner != owner || h.IsFlock != lk.IsFlock {
|
||||
kept = append(kept, h)
|
||||
continue
|
||||
}
|
||||
if !rangesOverlap(h.Start, h.End, lk.Start, lk.End) {
|
||||
// Check for adjacency with same type for merging.
|
||||
if h.Typ == lk.Typ && ((h.End < ^uint64(0) && h.End+1 == lk.Start) || (lk.End < ^uint64(0) && lk.End+1 == h.Start)) {
|
||||
// Merge adjacent same-type lock into lk.
|
||||
if h.Start < lk.Start {
|
||||
lk.Start = h.Start
|
||||
}
|
||||
if h.End > lk.End {
|
||||
lk.End = h.End
|
||||
}
|
||||
continue
|
||||
}
|
||||
kept = append(kept, h)
|
||||
continue
|
||||
}
|
||||
// Overlapping same-owner lock.
|
||||
if h.Typ == lk.Typ {
|
||||
// Same type: absorb into lk (expand range).
|
||||
if h.Start < lk.Start {
|
||||
lk.Start = h.Start
|
||||
}
|
||||
if h.End > lk.End {
|
||||
lk.End = h.End
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Different type: truncate or split the existing lock.
|
||||
if h.Start < lk.Start {
|
||||
// Left portion survives.
|
||||
left := h
|
||||
left.End = lk.Start - 1
|
||||
kept = append(kept, left)
|
||||
}
|
||||
if h.End > lk.End {
|
||||
// Right portion survives.
|
||||
right := h
|
||||
right.Start = lk.End + 1
|
||||
kept = append(kept, right)
|
||||
}
|
||||
}
|
||||
|
||||
kept = append(kept, lk)
|
||||
sort.Slice(kept, func(i, j int) bool {
|
||||
return kept[i].Start < kept[j].Start
|
||||
})
|
||||
il.locks = kept
|
||||
}
|
||||
|
||||
// removeLocks removes or splits matching locks in the given range.
|
||||
// Caller must hold il.mu.
|
||||
func removeLocks(il *inodeLocks, matches func(lockRange) bool, start, end uint64) {
|
||||
var kept []lockRange
|
||||
for _, h := range il.locks {
|
||||
if !matches(h) || !rangesOverlap(h.Start, h.End, start, end) {
|
||||
kept = append(kept, h)
|
||||
continue
|
||||
}
|
||||
// h overlaps the unlock range.
|
||||
if h.Start < start {
|
||||
// Left portion survives.
|
||||
left := h
|
||||
left.End = start - 1
|
||||
kept = append(kept, left)
|
||||
}
|
||||
if h.End > end {
|
||||
// Right portion survives.
|
||||
right := h
|
||||
right.Start = end + 1
|
||||
kept = append(kept, right)
|
||||
}
|
||||
// If fully contained, it's simply dropped.
|
||||
}
|
||||
il.locks = kept
|
||||
}
|
||||
|
||||
func (plt *PosixLockTable) releaseMatching(inode uint64, matches func(lockRange) bool) {
|
||||
il := plt.getInodeLocks(inode)
|
||||
if il == nil {
|
||||
return
|
||||
}
|
||||
il.mu.Lock()
|
||||
removeLocks(il, matches, 0, math.MaxUint64)
|
||||
wakeEligibleWaiters(il)
|
||||
il.mu.Unlock()
|
||||
plt.maybeCleanupInode(inode, il)
|
||||
}
|
||||
|
||||
// releaseWakeRef drops the temporary reference that keeps inodeLocks live while
|
||||
// a woken waiter retries its SetLkw acquisition.
|
||||
func releaseWakeRef(il *inodeLocks, waiter *lockWaiter) {
|
||||
if waiter == nil || !waiter.wakeRefHeld {
|
||||
return
|
||||
}
|
||||
waiter.wakeRefHeld = false
|
||||
il.wakeRefs--
|
||||
}
|
||||
|
||||
// wakeEligibleWaiters selectively wakes blocked SetLkw callers that can now
|
||||
// succeed given the current lock state. Waiters whose requests still conflict
|
||||
// with held locks remain in the queue, avoiding a thundering herd.
|
||||
// Caller must hold il.mu.
|
||||
func wakeEligibleWaiters(il *inodeLocks) {
|
||||
remaining := il.waiters[:0]
|
||||
for _, w := range il.waiters {
|
||||
if _, conflicted := findConflict(il.locks, w.requested); !conflicted {
|
||||
w.wakeRefHeld = true
|
||||
il.wakeRefs++
|
||||
close(w.ch)
|
||||
} else {
|
||||
remaining = append(remaining, w)
|
||||
}
|
||||
}
|
||||
il.waiters = remaining
|
||||
}
|
||||
|
||||
// removeWaiter removes a specific waiter from the list.
|
||||
// Caller must hold il.mu.
|
||||
func removeWaiter(il *inodeLocks, w *lockWaiter) {
|
||||
for i, existing := range il.waiters {
|
||||
if existing == w {
|
||||
il.waiters = append(il.waiters[:i], il.waiters[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetLk checks for a conflicting lock. If found, it populates out with the
|
||||
// conflict details. If no conflict, out.Typ is set to F_UNLCK.
|
||||
func (plt *PosixLockTable) GetLk(inode uint64, proposed lockRange, out *fuse.LkOut) {
|
||||
il := plt.getInodeLocks(inode)
|
||||
if il == nil {
|
||||
out.Lk.Typ = syscall.F_UNLCK
|
||||
return
|
||||
}
|
||||
il.mu.Lock()
|
||||
conflict, found := findConflict(il.locks, proposed)
|
||||
il.mu.Unlock()
|
||||
|
||||
if found {
|
||||
out.Lk.Start = conflict.Start
|
||||
out.Lk.End = conflict.End
|
||||
out.Lk.Typ = conflict.Typ
|
||||
out.Lk.Pid = conflict.Pid
|
||||
} else {
|
||||
out.Lk.Typ = syscall.F_UNLCK
|
||||
}
|
||||
}
|
||||
|
||||
// SetLk attempts a non-blocking lock or unlock.
|
||||
// For unlock (F_UNLCK): removes locks in the given range for the owner.
|
||||
// For lock: returns fuse.EAGAIN if a conflict exists, fuse.OK on success.
|
||||
func (plt *PosixLockTable) SetLk(inode uint64, lk lockRange) fuse.Status {
|
||||
if lk.Typ == syscall.F_UNLCK {
|
||||
il := plt.getInodeLocks(inode)
|
||||
if il == nil {
|
||||
return fuse.OK
|
||||
}
|
||||
il.mu.Lock()
|
||||
removeLocks(il, func(existing lockRange) bool {
|
||||
return existing.Owner == lk.Owner && existing.IsFlock == lk.IsFlock
|
||||
}, lk.Start, lk.End)
|
||||
wakeEligibleWaiters(il)
|
||||
il.mu.Unlock()
|
||||
plt.maybeCleanupInode(inode, il)
|
||||
return fuse.OK
|
||||
}
|
||||
|
||||
il := plt.getOrCreateInodeLocks(inode)
|
||||
il.mu.Lock()
|
||||
if _, found := findConflict(il.locks, lk); found {
|
||||
il.mu.Unlock()
|
||||
return fuse.EAGAIN
|
||||
}
|
||||
insertAndCoalesce(il, lk)
|
||||
il.mu.Unlock()
|
||||
return fuse.OK
|
||||
}
|
||||
|
||||
// SetLkw attempts a blocking lock. It waits until the lock can be acquired
|
||||
// or the cancel channel is closed.
|
||||
func (plt *PosixLockTable) SetLkw(inode uint64, lk lockRange, cancel <-chan struct{}) fuse.Status {
|
||||
if lk.Typ == syscall.F_UNLCK {
|
||||
return plt.SetLk(inode, lk)
|
||||
}
|
||||
|
||||
il := plt.getOrCreateInodeLocks(inode)
|
||||
var waiter *lockWaiter
|
||||
for {
|
||||
il.mu.Lock()
|
||||
releaseWakeRef(il, waiter)
|
||||
if _, found := findConflict(il.locks, lk); !found {
|
||||
insertAndCoalesce(il, lk)
|
||||
il.mu.Unlock()
|
||||
return fuse.OK
|
||||
}
|
||||
// Register waiter with the requested lock details for selective waking.
|
||||
waiter = &lockWaiter{requested: lk, ch: make(chan struct{})}
|
||||
il.waiters = append(il.waiters, waiter)
|
||||
il.mu.Unlock()
|
||||
|
||||
// Block until woken or cancelled.
|
||||
select {
|
||||
case <-waiter.ch:
|
||||
// Woken — retry.
|
||||
continue
|
||||
case <-cancel:
|
||||
// Request cancelled.
|
||||
il.mu.Lock()
|
||||
releaseWakeRef(il, waiter)
|
||||
removeWaiter(il, waiter)
|
||||
il.mu.Unlock()
|
||||
plt.maybeCleanupInode(inode, il)
|
||||
return fuse.EINTR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReleaseOwner removes all locks held by the given owner on the given inode.
|
||||
// Used for same-owner cleanup in tests and lock-table operations.
|
||||
func (plt *PosixLockTable) ReleaseOwner(inode uint64, owner uint64) {
|
||||
plt.releaseMatching(inode, func(lk lockRange) bool {
|
||||
return lk.Owner == owner
|
||||
})
|
||||
}
|
||||
|
||||
// ReleaseFlockOwner removes flock locks for a released file description.
|
||||
// FUSE only provides LockOwner on RELEASE when FUSE_RELEASE_FLOCK_UNLOCK is set.
|
||||
func (plt *PosixLockTable) ReleaseFlockOwner(inode uint64, owner uint64) {
|
||||
plt.releaseMatching(inode, func(lk lockRange) bool {
|
||||
return lk.IsFlock && lk.Owner == owner
|
||||
})
|
||||
}
|
||||
|
||||
// ReleasePosixOwner removes POSIX fcntl locks for a closing lock owner.
|
||||
// FUSE passes the closing fi->owner on FLUSH, which is the correct close-time
|
||||
// identity for POSIX byte-range lock cleanup.
|
||||
func (plt *PosixLockTable) ReleasePosixOwner(inode uint64, owner uint64) {
|
||||
plt.releaseMatching(inode, func(lk lockRange) bool {
|
||||
return !lk.IsFlock && lk.Owner == owner
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,617 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"math"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
)
|
||||
|
||||
func TestNonOverlappingLocksFromDifferentOwners(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
s1 := plt.SetLk(inode, lockRange{Start: 0, End: 49, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
if s1 != fuse.OK {
|
||||
t.Fatalf("expected OK, got %v", s1)
|
||||
}
|
||||
s2 := plt.SetLk(inode, lockRange{Start: 50, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20})
|
||||
if s2 != fuse.OK {
|
||||
t.Fatalf("expected OK, got %v", s2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlappingReadLocksFromDifferentOwners(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
s1 := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10})
|
||||
if s1 != fuse.OK {
|
||||
t.Fatalf("expected OK, got %v", s1)
|
||||
}
|
||||
s2 := plt.SetLk(inode, lockRange{Start: 50, End: 149, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20})
|
||||
if s2 != fuse.OK {
|
||||
t.Fatalf("expected OK, got %v", s2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlappingWriteReadConflict(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
s := plt.SetLk(inode, lockRange{Start: 50, End: 149, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20})
|
||||
if s != fuse.EAGAIN {
|
||||
t.Fatalf("expected EAGAIN, got %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlappingWriteWriteConflict(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
s := plt.SetLk(inode, lockRange{Start: 50, End: 149, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20})
|
||||
if s != fuse.EAGAIN {
|
||||
t.Fatalf("expected EAGAIN, got %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameOwnerUpgradeReadToWrite(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10})
|
||||
s := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
if s != fuse.OK {
|
||||
t.Fatalf("expected OK for same-owner upgrade, got %v", s)
|
||||
}
|
||||
|
||||
// Verify the lock is now a write lock.
|
||||
var out fuse.LkOut
|
||||
plt.GetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, &out)
|
||||
if out.Lk.Typ != syscall.F_WRLCK {
|
||||
t.Fatalf("expected conflicting write lock, got type %d", out.Lk.Typ)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameOwnerDowngradeWriteToRead(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
s := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10})
|
||||
if s != fuse.OK {
|
||||
t.Fatalf("expected OK for same-owner downgrade, got %v", s)
|
||||
}
|
||||
|
||||
// Another owner should now be able to get a read lock.
|
||||
s2 := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20})
|
||||
if s2 != fuse.OK {
|
||||
t.Fatalf("expected OK for shared read lock, got %v", s2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockCoalescing(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 9, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
plt.SetLk(inode, lockRange{Start: 10, End: 19, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
|
||||
il := plt.getInodeLocks(inode)
|
||||
il.mu.Lock()
|
||||
ownerLocks := 0
|
||||
for _, lk := range il.locks {
|
||||
if lk.Owner == 1 {
|
||||
ownerLocks++
|
||||
if lk.Start != 0 || lk.End != 19 {
|
||||
t.Errorf("expected coalesced lock [0,19], got [%d,%d]", lk.Start, lk.End)
|
||||
}
|
||||
}
|
||||
}
|
||||
il.mu.Unlock()
|
||||
if ownerLocks != 1 {
|
||||
t.Fatalf("expected 1 coalesced lock, got %d", ownerLocks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockSplitting(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
// Unlock the middle portion.
|
||||
plt.SetLk(inode, lockRange{Start: 40, End: 59, Typ: syscall.F_UNLCK, Owner: 1, Pid: 10})
|
||||
|
||||
il := plt.getInodeLocks(inode)
|
||||
il.mu.Lock()
|
||||
ownerLocks := 0
|
||||
for _, lk := range il.locks {
|
||||
if lk.Owner == 1 {
|
||||
ownerLocks++
|
||||
}
|
||||
}
|
||||
if ownerLocks != 2 {
|
||||
il.mu.Unlock()
|
||||
t.Fatalf("expected 2 locks after split, got %d", ownerLocks)
|
||||
}
|
||||
// Check the ranges.
|
||||
if il.locks[0].Start != 0 || il.locks[0].End != 39 {
|
||||
t.Errorf("expected left lock [0,39], got [%d,%d]", il.locks[0].Start, il.locks[0].End)
|
||||
}
|
||||
if il.locks[1].Start != 60 || il.locks[1].End != 99 {
|
||||
t.Errorf("expected right lock [60,99], got [%d,%d]", il.locks[1].Start, il.locks[1].End)
|
||||
}
|
||||
il.mu.Unlock()
|
||||
}
|
||||
|
||||
func TestGetLkConflict(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 10, End: 50, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
|
||||
var out fuse.LkOut
|
||||
plt.GetLk(inode, lockRange{Start: 30, End: 70, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20}, &out)
|
||||
if out.Lk.Typ != syscall.F_WRLCK {
|
||||
t.Fatalf("expected conflicting write lock, got type %d", out.Lk.Typ)
|
||||
}
|
||||
if out.Lk.Pid != 10 {
|
||||
t.Fatalf("expected holder PID 10, got %d", out.Lk.Pid)
|
||||
}
|
||||
if out.Lk.Start != 10 || out.Lk.End != 50 {
|
||||
t.Fatalf("expected conflict [10,50], got [%d,%d]", out.Lk.Start, out.Lk.End)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLkNoConflict(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 10, End: 50, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10})
|
||||
|
||||
var out fuse.LkOut
|
||||
plt.GetLk(inode, lockRange{Start: 30, End: 70, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20}, &out)
|
||||
if out.Lk.Typ != syscall.F_UNLCK {
|
||||
t.Fatalf("expected F_UNLCK (no conflict), got type %d", out.Lk.Typ)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLkSameOwnerNoConflict(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
|
||||
var out fuse.LkOut
|
||||
plt.GetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}, &out)
|
||||
if out.Lk.Typ != syscall.F_UNLCK {
|
||||
t.Fatalf("same owner should not conflict with itself, got type %d", out.Lk.Typ)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseOwner(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 49, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
plt.SetLk(inode, lockRange{Start: 50, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
plt.SetLk(inode, lockRange{Start: 200, End: 299, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20})
|
||||
|
||||
plt.ReleaseOwner(inode, 1)
|
||||
|
||||
// Owner 1's locks should be gone.
|
||||
var out fuse.LkOut
|
||||
plt.GetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 3, Pid: 30}, &out)
|
||||
if out.Lk.Typ != syscall.F_UNLCK {
|
||||
t.Fatalf("expected no conflict after ReleaseOwner, got type %d", out.Lk.Typ)
|
||||
}
|
||||
|
||||
// Owner 2's lock should still exist.
|
||||
plt.GetLk(inode, lockRange{Start: 200, End: 299, Typ: syscall.F_WRLCK, Owner: 3, Pid: 30}, &out)
|
||||
if out.Lk.Typ != syscall.F_RDLCK {
|
||||
t.Fatalf("expected owner 2's read lock to remain, got type %d", out.Lk.Typ)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDifferentLockKindsDoNotConflict(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
s1 := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
if s1 != fuse.OK {
|
||||
t.Fatalf("expected POSIX lock OK, got %v", s1)
|
||||
}
|
||||
|
||||
s2 := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20, IsFlock: true})
|
||||
if s2 != fuse.OK {
|
||||
t.Fatalf("expected flock lock OK in separate namespace, got %v", s2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleasePosixOwnerReleasesPosixLocksAndWakesWaiters(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
|
||||
done := make(chan fuse.Status, 1)
|
||||
go func() {
|
||||
cancel := make(chan struct{})
|
||||
done <- plt.SetLkw(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, cancel)
|
||||
}()
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
plt.ReleasePosixOwner(inode, 1)
|
||||
|
||||
select {
|
||||
case s := <-done:
|
||||
if s != fuse.OK {
|
||||
t.Fatalf("expected OK after ReleasePosixOwner, got %v", s)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("SetLkw did not unblock after ReleasePosixOwner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleasePosixOwnerDoesNotReleaseFlockLocks(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10, IsFlock: true})
|
||||
plt.ReleasePosixOwner(inode, 1)
|
||||
|
||||
var out fuse.LkOut
|
||||
plt.GetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20, IsFlock: true}, &out)
|
||||
if out.Lk.Typ != syscall.F_WRLCK {
|
||||
t.Fatalf("expected flock lock to remain after ReleasePosixOwner, got type %d", out.Lk.Typ)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWakeEligibleWaitersKeepsInodeUntilWakeRefReleased(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
il := plt.getOrCreateInodeLocks(inode)
|
||||
waiter := &lockWaiter{
|
||||
requested: lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20},
|
||||
ch: make(chan struct{}),
|
||||
}
|
||||
|
||||
il.mu.Lock()
|
||||
il.waiters = append(il.waiters, waiter)
|
||||
il.mu.Unlock()
|
||||
|
||||
plt.releaseMatching(inode, func(lockRange) bool { return false })
|
||||
|
||||
select {
|
||||
case <-waiter.ch:
|
||||
// Expected.
|
||||
default:
|
||||
t.Fatal("expected waiter to be woken")
|
||||
}
|
||||
|
||||
plt.mu.Lock()
|
||||
_, exists := plt.inodes[inode]
|
||||
plt.mu.Unlock()
|
||||
if !exists {
|
||||
t.Fatal("inodeLocks should remain while a woken waiter still holds a wake ref")
|
||||
}
|
||||
|
||||
il.mu.Lock()
|
||||
releaseWakeRef(il, waiter)
|
||||
il.mu.Unlock()
|
||||
plt.maybeCleanupInode(inode, il)
|
||||
|
||||
plt.mu.Lock()
|
||||
_, exists = plt.inodes[inode]
|
||||
plt.mu.Unlock()
|
||||
if exists {
|
||||
t.Fatal("inodeLocks should be cleaned up after the final wake ref is released")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseFlockOwnerDoesNotReleasePosixLocks(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, Pid: 10, IsFlock: true})
|
||||
|
||||
plt.ReleaseFlockOwner(inode, 2)
|
||||
|
||||
var out fuse.LkOut
|
||||
plt.GetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 3, Pid: 30}, &out)
|
||||
if out.Lk.Typ != syscall.F_WRLCK {
|
||||
t.Fatalf("expected POSIX lock to remain after ReleaseFlockOwner, got type %d", out.Lk.Typ)
|
||||
}
|
||||
|
||||
plt.GetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 4, Pid: 40, IsFlock: true}, &out)
|
||||
if out.Lk.Typ != syscall.F_UNLCK {
|
||||
t.Fatalf("expected flock lock to be removed after ReleaseFlockOwner, got type %d", out.Lk.Typ)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseOwnerWakesWaiters(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
|
||||
done := make(chan fuse.Status, 1)
|
||||
go func() {
|
||||
cancel := make(chan struct{})
|
||||
s := plt.SetLkw(inode, lockRange{Start: 50, End: 60, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, cancel)
|
||||
done <- s
|
||||
}()
|
||||
|
||||
// Give the goroutine time to block.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
plt.ReleaseOwner(inode, 1)
|
||||
|
||||
select {
|
||||
case s := <-done:
|
||||
if s != fuse.OK {
|
||||
t.Fatalf("expected OK after ReleaseOwner woke waiter, got %v", s)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("SetLkw did not unblock after ReleaseOwner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetLkwBlocksAndSucceeds(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
|
||||
done := make(chan fuse.Status, 1)
|
||||
go func() {
|
||||
cancel := make(chan struct{})
|
||||
s := plt.SetLkw(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, cancel)
|
||||
done <- s
|
||||
}()
|
||||
|
||||
// Give the goroutine time to block.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Release the conflicting lock.
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_UNLCK, Owner: 1, Pid: 10})
|
||||
|
||||
select {
|
||||
case s := <-done:
|
||||
if s != fuse.OK {
|
||||
t.Fatalf("expected OK, got %v", s)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("SetLkw did not unblock after conflicting lock was released")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetLkwCancellation(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
|
||||
cancel := make(chan struct{})
|
||||
done := make(chan fuse.Status, 1)
|
||||
go func() {
|
||||
s := plt.SetLkw(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, cancel)
|
||||
done <- s
|
||||
}()
|
||||
|
||||
// Give the goroutine time to block.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
close(cancel)
|
||||
|
||||
select {
|
||||
case s := <-done:
|
||||
if s != fuse.EINTR {
|
||||
t.Fatalf("expected EINTR on cancel, got %v", s)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("SetLkw did not unblock after cancel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWholeFileLock(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
// Simulate flock() — whole-file exclusive lock.
|
||||
s1 := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
if s1 != fuse.OK {
|
||||
t.Fatalf("expected OK, got %v", s1)
|
||||
}
|
||||
|
||||
// Second owner should be blocked.
|
||||
s2 := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20})
|
||||
if s2 != fuse.EAGAIN {
|
||||
t.Fatalf("expected EAGAIN, got %v", s2)
|
||||
}
|
||||
|
||||
// Even a partial overlap should fail.
|
||||
s3 := plt.SetLk(inode, lockRange{Start: 100, End: 200, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20})
|
||||
if s3 != fuse.EAGAIN {
|
||||
t.Fatalf("expected EAGAIN for partial overlap with whole-file lock, got %v", s3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnlockNoExistingLocks(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
// Unlock on an inode with no locks should succeed silently.
|
||||
s := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_UNLCK, Owner: 1, Pid: 10})
|
||||
if s != fuse.OK {
|
||||
t.Fatalf("expected OK for unlock with no existing locks, got %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleInodesIndependent(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
|
||||
// Write lock on inode 1 should not affect inode 2.
|
||||
plt.SetLk(1, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
s := plt.SetLk(2, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20})
|
||||
if s != fuse.OK {
|
||||
t.Fatalf("locks on different inodes should be independent, got %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryCleanup(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
plt.ReleaseOwner(inode, 1)
|
||||
|
||||
plt.mu.Lock()
|
||||
_, exists := plt.inodes[inode]
|
||||
plt.mu.Unlock()
|
||||
if exists {
|
||||
t.Fatal("expected inode entry to be cleaned up after all locks released")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectiveWaking(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
// Owner 1 holds write lock on [0, 99], owner 2 holds write lock on [200, 299].
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
plt.SetLk(inode, lockRange{Start: 200, End: 299, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20})
|
||||
|
||||
// Owner 3 waits for [50, 60] (blocked by owner 1).
|
||||
done3 := make(chan fuse.Status, 1)
|
||||
go func() {
|
||||
cancel := make(chan struct{})
|
||||
s := plt.SetLkw(inode, lockRange{Start: 50, End: 60, Typ: syscall.F_WRLCK, Owner: 3, Pid: 30}, cancel)
|
||||
done3 <- s
|
||||
}()
|
||||
// Owner 4 waits for [250, 260] (blocked by owner 2).
|
||||
done4 := make(chan fuse.Status, 1)
|
||||
go func() {
|
||||
cancel := make(chan struct{})
|
||||
s := plt.SetLkw(inode, lockRange{Start: 250, End: 260, Typ: syscall.F_WRLCK, Owner: 4, Pid: 40}, cancel)
|
||||
done4 <- s
|
||||
}()
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Release owner 1's lock. Only owner 3 should be woken; owner 4 is still blocked.
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_UNLCK, Owner: 1, Pid: 10})
|
||||
|
||||
select {
|
||||
case s := <-done3:
|
||||
if s != fuse.OK {
|
||||
t.Fatalf("expected OK for owner 3, got %v", s)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("owner 3 was not woken after owner 1 released")
|
||||
}
|
||||
|
||||
// Owner 4 should still be blocked.
|
||||
select {
|
||||
case s := <-done4:
|
||||
t.Fatalf("owner 4 should still be blocked, but got %v", s)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
// Expected — still blocked.
|
||||
}
|
||||
|
||||
// Now release owner 2's lock. Owner 4 should wake.
|
||||
plt.SetLk(inode, lockRange{Start: 200, End: 299, Typ: syscall.F_UNLCK, Owner: 2, Pid: 20})
|
||||
|
||||
select {
|
||||
case s := <-done4:
|
||||
if s != fuse.OK {
|
||||
t.Fatalf("expected OK for owner 4, got %v", s)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("owner 4 was not woken after owner 2 released")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameOwnerReplaceDifferentType(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
// Lock [0, 99] as write.
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
// Replace middle portion [30, 60] with read lock.
|
||||
plt.SetLk(inode, lockRange{Start: 30, End: 60, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10})
|
||||
|
||||
il := plt.getInodeLocks(inode)
|
||||
il.mu.Lock()
|
||||
defer il.mu.Unlock()
|
||||
|
||||
// Should have 3 locks: write [0,29], read [30,60], write [61,99].
|
||||
if len(il.locks) != 3 {
|
||||
t.Fatalf("expected 3 locks after partial type change, got %d", len(il.locks))
|
||||
}
|
||||
if il.locks[0].Typ != syscall.F_WRLCK || il.locks[0].Start != 0 || il.locks[0].End != 29 {
|
||||
t.Errorf("expected write [0,29], got type=%d [%d,%d]", il.locks[0].Typ, il.locks[0].Start, il.locks[0].End)
|
||||
}
|
||||
if il.locks[1].Typ != syscall.F_RDLCK || il.locks[1].Start != 30 || il.locks[1].End != 60 {
|
||||
t.Errorf("expected read [30,60], got type=%d [%d,%d]", il.locks[1].Typ, il.locks[1].Start, il.locks[1].End)
|
||||
}
|
||||
if il.locks[2].Typ != syscall.F_WRLCK || il.locks[2].Start != 61 || il.locks[2].End != 99 {
|
||||
t.Errorf("expected write [61,99], got type=%d [%d,%d]", il.locks[2].Typ, il.locks[2].Start, il.locks[2].End)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonAdjacentRangesNotCoalesced(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
// Lock [5, MaxUint64] then [0, 2] — gap at [3,4] must prevent coalescing.
|
||||
plt.SetLk(inode, lockRange{Start: 5, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
s := plt.SetLk(inode, lockRange{Start: 0, End: 2, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
if s != fuse.OK {
|
||||
t.Fatalf("expected OK, got %v", s)
|
||||
}
|
||||
|
||||
il := plt.getInodeLocks(inode)
|
||||
il.mu.Lock()
|
||||
defer il.mu.Unlock()
|
||||
|
||||
if len(il.locks) != 2 {
|
||||
t.Fatalf("expected 2 separate locks (gap [3,4] prevents coalescing), got %d", len(il.locks))
|
||||
}
|
||||
if il.locks[0].Start != 0 || il.locks[0].End != 2 {
|
||||
t.Errorf("expected first lock [0,2], got [%d,%d]", il.locks[0].Start, il.locks[0].End)
|
||||
}
|
||||
if il.locks[1].Start != 5 || il.locks[1].End != math.MaxUint64 {
|
||||
t.Errorf("expected second lock [5,MaxUint64], got [%d,%d]", il.locks[1].Start, il.locks[1].End)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdjacencyNoOverflowAtMaxUint64(t *testing.T) {
|
||||
plt := NewPosixLockTable()
|
||||
inode := uint64(1)
|
||||
|
||||
// Lock to EOF (End = MaxUint64), then lock [0, 0] same type.
|
||||
// Without the overflow guard, MaxUint64+1 wraps to 0, falsely merging.
|
||||
plt.SetLk(inode, lockRange{Start: 100, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
plt.SetLk(inode, lockRange{Start: 0, End: 0, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
|
||||
|
||||
il := plt.getInodeLocks(inode)
|
||||
il.mu.Lock()
|
||||
defer il.mu.Unlock()
|
||||
|
||||
// Should remain 2 separate locks, not merged.
|
||||
ownerLocks := 0
|
||||
for _, lk := range il.locks {
|
||||
if lk.Owner == 1 {
|
||||
ownerLocks++
|
||||
}
|
||||
}
|
||||
if ownerLocks != 2 {
|
||||
t.Fatalf("expected 2 separate locks (no overflow merge), got %d", ownerLocks)
|
||||
}
|
||||
}
|
||||
+71
-27
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/mount_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/security"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util/chunk_cache"
|
||||
@@ -30,27 +31,29 @@ import (
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
filerIndex int32 // align memory for atomic read/write
|
||||
FilerAddresses []pb.ServerAddress
|
||||
MountDirectory string
|
||||
GrpcDialOption grpc.DialOption
|
||||
FilerMountRootPath string
|
||||
Collection string
|
||||
Replication string
|
||||
TtlSec int32
|
||||
DiskType types.DiskType
|
||||
ChunkSizeLimit int64
|
||||
ConcurrentWriters int
|
||||
ConcurrentReaders int
|
||||
CacheDirForRead string
|
||||
CacheSizeMBForRead int64
|
||||
CacheDirForWrite string
|
||||
CacheMetaTTlSec int
|
||||
DataCenter string
|
||||
Umask os.FileMode
|
||||
Quota int64
|
||||
DisableXAttr bool
|
||||
IsMacOs bool
|
||||
filerIndex int32 // align memory for atomic read/write
|
||||
FilerAddresses []pb.ServerAddress
|
||||
MountDirectory string
|
||||
GrpcDialOption grpc.DialOption
|
||||
FilerSigningKey security.SigningKey
|
||||
FilerSigningExpiresAfterSec int
|
||||
FilerMountRootPath string
|
||||
Collection string
|
||||
Replication string
|
||||
TtlSec int32
|
||||
DiskType types.DiskType
|
||||
ChunkSizeLimit int64
|
||||
ConcurrentWriters int
|
||||
ConcurrentReaders int
|
||||
CacheDirForRead string
|
||||
CacheSizeMBForRead int64
|
||||
CacheDirForWrite string
|
||||
CacheMetaTTlSec int
|
||||
DataCenter string
|
||||
Umask os.FileMode
|
||||
Quota int64
|
||||
DisableXAttr bool
|
||||
IsMacOs bool
|
||||
|
||||
MountUid uint32
|
||||
MountGid uint32
|
||||
@@ -106,6 +109,7 @@ type WFS struct {
|
||||
fuseServer *fuse.Server
|
||||
IsOverQuota bool
|
||||
fhLockTable *util.LockTable[FileHandleId]
|
||||
posixLocks *PosixLockTable
|
||||
rdmaClient *RDMAMountClient
|
||||
FilerConf *filer.FilerConf
|
||||
filerClient *wdclient.FilerClient // Cached volume location client
|
||||
@@ -115,15 +119,25 @@ type WFS struct {
|
||||
dirHotThreshold int
|
||||
dirIdleEvict time.Duration
|
||||
|
||||
// asyncFlushWg tracks pending background flush goroutines for writebackCache mode.
|
||||
// asyncFlushWg tracks pending background flush work items for writebackCache mode.
|
||||
// Must be waited on before unmount cleanup to prevent data loss.
|
||||
asyncFlushWg sync.WaitGroup
|
||||
|
||||
// asyncFlushCh is a bounded work queue for background flush operations.
|
||||
// A fixed pool of worker goroutines processes items from this channel,
|
||||
// preventing resource exhaustion from unbounded goroutine creation.
|
||||
asyncFlushCh chan *asyncFlushItem
|
||||
|
||||
// pendingAsyncFlush tracks in-flight async flush goroutines by inode.
|
||||
// AcquireHandle checks this to wait for a pending flush before reopening
|
||||
// the same inode, preventing stale metadata from overwriting the async flush.
|
||||
pendingAsyncFlushMu sync.Mutex
|
||||
pendingAsyncFlush map[uint64]chan struct{}
|
||||
|
||||
// streamMutate is the multiplexed streaming gRPC connection for all filer
|
||||
// mutations (create, update, delete, rename). All mutations go through one
|
||||
// ordered stream to prevent cross-operation reordering.
|
||||
streamMutate *streamMutateMux
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -173,11 +187,12 @@ func NewSeaweedFileSystem(option *Option) *WFS {
|
||||
dhMap: NewDirectoryHandleToInode(),
|
||||
filerClient: filerClient, // nil for proxy mode, initialized for direct access
|
||||
pendingAsyncFlush: make(map[uint64]chan struct{}),
|
||||
fhLockTable: util.NewLockTable[FileHandleId](),
|
||||
refreshingDirs: make(map[util.FullPath]struct{}),
|
||||
dirHotWindow: dirHotWindow,
|
||||
dirHotThreshold: dirHotThreshold,
|
||||
dirIdleEvict: dirIdleEvict,
|
||||
fhLockTable: util.NewLockTable[FileHandleId](),
|
||||
posixLocks: NewPosixLockTable(),
|
||||
refreshingDirs: make(map[util.FullPath]struct{}),
|
||||
dirHotWindow: dirHotWindow,
|
||||
dirHotThreshold: dirHotThreshold,
|
||||
dirIdleEvict: dirIdleEvict,
|
||||
}
|
||||
|
||||
wfs.option.filerIndex = int32(rand.IntN(len(option.FilerAddresses)))
|
||||
@@ -272,6 +287,14 @@ func NewSeaweedFileSystem(option *Option) *WFS {
|
||||
wfs.concurrentWriters = util.NewLimitedConcurrentExecutor(wfs.option.ConcurrentWriters)
|
||||
wfs.concurrentCopiersSem = make(chan struct{}, wfs.option.ConcurrentWriters)
|
||||
}
|
||||
if wfs.option.WritebackCache {
|
||||
numWorkers := wfs.option.ConcurrentWriters
|
||||
if numWorkers <= 0 {
|
||||
numWorkers = 128
|
||||
}
|
||||
wfs.startAsyncFlushWorkers(numWorkers)
|
||||
}
|
||||
wfs.streamMutate = newStreamMutateMux(wfs)
|
||||
wfs.copyBufferPool.New = func() any {
|
||||
return make([]byte, option.ChunkSizeLimit)
|
||||
}
|
||||
@@ -387,6 +410,27 @@ func (wfs *WFS) lookupEntry(fullpath util.FullPath) (*filer.Entry, fuse.Status)
|
||||
entry, err := filer_pb.GetEntry(context.Background(), wfs, fullpath)
|
||||
if err != nil {
|
||||
if err == filer_pb.ErrNotFound {
|
||||
// The entry may exist in the local store from a deferred create
|
||||
// (deferFilerCreate=true) that hasn't been flushed yet. Only trust
|
||||
// the local store when an open file handle or pending async flush
|
||||
// confirms the entry is genuinely local-only; otherwise a stale
|
||||
// cache hit could resurrect a deleted/renamed entry.
|
||||
if inode, inodeFound := wfs.inodeToPath.GetInode(fullpath); inodeFound {
|
||||
hasDirtyHandle := false
|
||||
if fh, fhFound := wfs.fhMap.FindFileHandle(inode); fhFound && fh.dirtyMetadata {
|
||||
hasDirtyHandle = true
|
||||
}
|
||||
wfs.pendingAsyncFlushMu.Lock()
|
||||
_, hasPendingFlush := wfs.pendingAsyncFlush[inode]
|
||||
wfs.pendingAsyncFlushMu.Unlock()
|
||||
|
||||
if hasDirtyHandle || hasPendingFlush {
|
||||
if localEntry, localErr := wfs.metaCache.FindEntry(context.Background(), fullpath); localErr == nil && localEntry != nil {
|
||||
glog.V(4).Infof("lookupEntry found deferred entry in local cache %s", fullpath)
|
||||
return localEntry, fuse.OK
|
||||
}
|
||||
}
|
||||
}
|
||||
glog.V(4).Infof("lookupEntry not found %s", fullpath)
|
||||
return nil, fuse.ENOENT
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"os/user"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
)
|
||||
|
||||
var lookupSupplementaryGroupIDs = func(callerUid uint32) ([]string, error) {
|
||||
u, err := user.LookupId(strconv.Itoa(int(callerUid)))
|
||||
if err != nil {
|
||||
glog.Warningf("hasAccess: user.LookupId for uid %d failed: %v", callerUid, err)
|
||||
return nil, err
|
||||
}
|
||||
groupIDs, err := u.GroupIds()
|
||||
if err != nil {
|
||||
glog.Warningf("hasAccess: u.GroupIds for uid %d failed: %v", callerUid, err)
|
||||
return nil, err
|
||||
}
|
||||
return groupIDs, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* Check file access permissions
|
||||
*
|
||||
* This will be called for the access() system call. If the
|
||||
* 'default_permissions' mount option is given, this method is not
|
||||
* called.
|
||||
*
|
||||
* This method is not called under Linux kernel versions 2.4.x
|
||||
*/
|
||||
func (wfs *WFS) Access(cancel <-chan struct{}, input *fuse.AccessIn) (code fuse.Status) {
|
||||
_, _, entry, code := wfs.maybeReadEntry(input.NodeId)
|
||||
if code != fuse.OK {
|
||||
return code
|
||||
}
|
||||
if entry == nil || entry.Attributes == nil {
|
||||
return fuse.EIO
|
||||
}
|
||||
// Map entry uid/gid from filer-space to local-space so the permission
|
||||
// check compares like with like (caller uid/gid from FUSE are local).
|
||||
fileUid, fileGid := entry.Attributes.Uid, entry.Attributes.Gid
|
||||
if wfs.option.UidGidMapper != nil {
|
||||
fileUid, fileGid = wfs.option.UidGidMapper.FilerToLocal(fileUid, fileGid)
|
||||
}
|
||||
if hasAccess(input.Uid, input.Gid, fileUid, fileGid, entry.Attributes.FileMode, input.Mask) {
|
||||
return fuse.OK
|
||||
}
|
||||
return fuse.EACCES
|
||||
}
|
||||
|
||||
func hasAccess(callerUid, callerGid, fileUid, fileGid uint32, perm uint32, mask uint32) bool {
|
||||
mask &= fuse.R_OK | fuse.W_OK | fuse.X_OK
|
||||
if mask == 0 {
|
||||
return true
|
||||
}
|
||||
if callerUid == 0 {
|
||||
return mask&fuse.X_OK == 0 || perm&0o111 != 0
|
||||
}
|
||||
|
||||
if callerUid == fileUid {
|
||||
return (perm>>6)&mask == mask
|
||||
}
|
||||
|
||||
isMember := callerGid == fileGid
|
||||
if !isMember {
|
||||
groupIDs, err := lookupSupplementaryGroupIDs(callerUid)
|
||||
if err != nil {
|
||||
// Cannot determine group membership; require both group and
|
||||
// other permission classes to satisfy the mask so we never
|
||||
// overgrant when the lookup fails.
|
||||
groupMatch := ((perm >> 3) & mask) == mask
|
||||
otherMatch := (perm & mask) == mask
|
||||
return groupMatch && otherMatch
|
||||
}
|
||||
fileGidStr := strconv.Itoa(int(fileGid))
|
||||
for _, gidStr := range groupIDs {
|
||||
if gidStr == fileGidStr {
|
||||
isMember = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isMember {
|
||||
return (perm>>3)&mask == mask
|
||||
}
|
||||
|
||||
return (perm & mask) == mask
|
||||
}
|
||||
|
||||
// openFlagsToAccessMask converts open(2) flags to an access permission mask.
|
||||
func openFlagsToAccessMask(flags uint32) uint32 {
|
||||
switch flags & uint32(syscall.O_ACCMODE) {
|
||||
case syscall.O_WRONLY:
|
||||
return fuse.W_OK
|
||||
case syscall.O_RDWR:
|
||||
return fuse.R_OK | fuse.W_OK
|
||||
default: // O_RDONLY
|
||||
return fuse.R_OK
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,52 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
const asyncFlushMetadataRetries = 3
|
||||
// asyncFlushItem holds the data needed for a background flush work item.
|
||||
type asyncFlushItem struct {
|
||||
fh *FileHandle
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// startAsyncFlushWorkers launches a fixed pool of goroutines that process
|
||||
// background flush work items from asyncFlushCh. This bounds the number of
|
||||
// concurrent flush operations to prevent resource exhaustion (connections,
|
||||
// goroutines) when many files are closed rapidly (e.g., cp -r with writebackCache).
|
||||
func (wfs *WFS) startAsyncFlushWorkers(numWorkers int) {
|
||||
wfs.asyncFlushCh = make(chan *asyncFlushItem, numWorkers*4)
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
go wfs.asyncFlushWorker()
|
||||
}
|
||||
}
|
||||
|
||||
func (wfs *WFS) asyncFlushWorker() {
|
||||
for item := range wfs.asyncFlushCh {
|
||||
wfs.processAsyncFlushItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
func (wfs *WFS) processAsyncFlushItem(item *asyncFlushItem) {
|
||||
defer wfs.asyncFlushWg.Done()
|
||||
defer func() {
|
||||
// Remove from fhMap first (so AcquireFileHandle creates a fresh handle).
|
||||
wfs.fhMap.RemoveFileHandle(item.fh.fh, item.fh.inode)
|
||||
// Then signal completion (unblocks waitForPendingAsyncFlush).
|
||||
close(item.done)
|
||||
wfs.pendingAsyncFlushMu.Lock()
|
||||
delete(wfs.pendingAsyncFlush, item.fh.inode)
|
||||
wfs.pendingAsyncFlushMu.Unlock()
|
||||
}()
|
||||
wfs.completeAsyncFlush(item.fh)
|
||||
}
|
||||
|
||||
// completeAsyncFlush 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) {
|
||||
glog.V(4).Infof("completeAsyncFlush inode %d fh %d saved=%s/%s dirtyMetadata=%v isDeleted=%v isRenamed=%v",
|
||||
fh.inode, fh.fh, fh.savedDir, fh.savedName, fh.dirtyMetadata, fh.isDeleted, fh.isRenamed)
|
||||
|
||||
// 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.
|
||||
@@ -33,20 +68,31 @@ func (wfs *WFS) completeAsyncFlush(fh *FileHandle) {
|
||||
// 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)
|
||||
if fh.isDeleted || fh.isRenamed {
|
||||
if fh.isDeleted {
|
||||
glog.V(3).Infof("completeAsyncFlush inode %d: file was unlinked, skipping metadata flush", fh.inode)
|
||||
} else {
|
||||
glog.V(3).Infof("completeAsyncFlush inode %d: file was renamed, skipping old-path metadata flush (Rename handles it)", fh.inode)
|
||||
}
|
||||
} else if savedInode, found := wfs.inodeToPath.GetInode(util.FullPath(fh.savedDir).Child(fh.savedName)); !found || savedInode != fh.inode {
|
||||
// The saved path no longer maps to this inode — the file was
|
||||
// renamed (or deleted and recreated). Flushing metadata under
|
||||
// the old path would re-insert a stale entry into the meta
|
||||
// cache, breaking git's lock file protocol.
|
||||
glog.V(3).Infof("completeAsyncFlush inode %d: saved path %s/%s no longer maps to this inode, skipping metadata flush",
|
||||
fh.inode, fh.savedDir, fh.savedName)
|
||||
} 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 dir/name saved at doFlush time. Rename also
|
||||
// updates the saved path, so the fallback is always current.
|
||||
// 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.asyncFlushDir, fh.asyncFlushName
|
||||
dir, name := fh.savedDir, fh.savedName
|
||||
fileFullPath := util.FullPath(dir).Child(name)
|
||||
|
||||
if resolvedPath, status := wfs.inodeToPath.GetPath(fh.inode); status == fuse.OK {
|
||||
@@ -68,29 +114,28 @@ func (wfs *WFS) completeAsyncFlush(fh *FileHandle) {
|
||||
// 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) {
|
||||
for attempt := 0; attempt <= asyncFlushMetadataRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
backoff := time.Duration(1<<uint(attempt-1)) * time.Second
|
||||
glog.Warningf("completeAsyncFlush %s: retrying metadata flush (attempt %d/%d) after %v",
|
||||
fileFullPath, attempt+1, asyncFlushMetadataRetries+1, backoff)
|
||||
time.Sleep(backoff)
|
||||
}
|
||||
|
||||
if err := wfs.flushMetadataToFiler(fh, dir, name, fh.asyncFlushUid, fh.asyncFlushGid); err != nil {
|
||||
if attempt == asyncFlushMetadataRetries {
|
||||
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, asyncFlushMetadataRetries+1, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
return // success
|
||||
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.
|
||||
// WaitForAsyncFlush waits for all pending background flush work items to complete.
|
||||
// Called before unmount cleanup to ensure no data is lost.
|
||||
func (wfs *WFS) WaitForAsyncFlush() {
|
||||
wfs.asyncFlushWg.Wait()
|
||||
if wfs.asyncFlushCh != nil {
|
||||
close(wfs.asyncFlushCh)
|
||||
}
|
||||
if wfs.streamMutate != nil {
|
||||
wfs.streamMutate.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,25 +49,24 @@ func (wfs *WFS) Mkdir(cancel <-chan struct{}, in *fuse.MkdirIn, name string, out
|
||||
|
||||
entryFullPath := dirFullPath.Child(name)
|
||||
|
||||
err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
wfs.mapPbIdFromLocalToFiler(newEntry)
|
||||
// Defer restoring to local uid/gid AFTER the entry is sent to the filer
|
||||
// but BEFORE outputPbEntry writes attributes to the kernel. We restore
|
||||
// explicitly below instead of using defer so the kernel gets local values.
|
||||
|
||||
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("mkdir: %v", request)
|
||||
resp, err := filer_pb.CreateEntryWithResponse(context.Background(), client, request)
|
||||
if err != nil {
|
||||
glog.V(0).Infof("mkdir %s: %v", entryFullPath, err)
|
||||
return err
|
||||
}
|
||||
request := &filer_pb.CreateEntryRequest{
|
||||
Directory: string(dirFullPath),
|
||||
Entry: newEntry,
|
||||
Signatures: []int32{wfs.signature},
|
||||
SkipCheckParentDirectory: true,
|
||||
}
|
||||
|
||||
glog.V(1).Infof("mkdir: %v", request)
|
||||
resp, err := wfs.streamCreateEntry(context.Background(), request)
|
||||
if err != nil {
|
||||
glog.V(0).Infof("mkdir %s: %v", entryFullPath, err)
|
||||
} else {
|
||||
event := resp.GetMetadataEvent()
|
||||
if event == nil {
|
||||
event = metadataCreateEvent(string(dirFullPath), newEntry)
|
||||
@@ -77,16 +76,20 @@ func (wfs *WFS) Mkdir(cancel <-chan struct{}, in *fuse.MkdirIn, name string, out
|
||||
wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
|
||||
}
|
||||
wfs.inodeToPath.TouchDirectory(dirFullPath)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
glog.V(3).Infof("mkdir %s: %v", entryFullPath, err)
|
||||
|
||||
if err != nil {
|
||||
wfs.mapPbIdFromFilerToLocal(newEntry)
|
||||
return fuse.EIO
|
||||
}
|
||||
|
||||
// Map uid/gid back to local-space before writing attributes to the
|
||||
// kernel. The kernel (especially macFUSE) caches these and uses them
|
||||
// for subsequent permission checks on children.
|
||||
wfs.mapPbIdFromFilerToLocal(newEntry)
|
||||
|
||||
inode := wfs.inodeToPath.Lookup(entryFullPath, newEntry.Attributes.Crtime, true, false, 0, true)
|
||||
|
||||
wfs.outputPbEntry(out, inode, newEntry)
|
||||
@@ -112,10 +115,16 @@ func (wfs *WFS) Rmdir(cancel <-chan struct{}, header *fuse.InHeader, name string
|
||||
entryFullPath := dirFullPath.Child(name)
|
||||
|
||||
glog.V(3).Infof("remove directory: %v", entryFullPath)
|
||||
ignoreRecursiveErr := true // ignore recursion error since the OS should manage it
|
||||
resp, err := filer_pb.RemoveWithResponse(context.Background(), wfs, string(dirFullPath), name, true, false, ignoreRecursiveErr, false, []int32{wfs.signature})
|
||||
deleteReq := &filer_pb.DeleteEntryRequest{
|
||||
Directory: string(dirFullPath),
|
||||
Name: name,
|
||||
IsDeleteData: true,
|
||||
IgnoreRecursiveError: true, // ignore recursion error since the OS should manage it
|
||||
Signatures: []int32{wfs.signature},
|
||||
}
|
||||
resp, err := wfs.streamDeleteEntry(context.Background(), deleteReq)
|
||||
if err != nil {
|
||||
glog.V(0).Infof("remove %s: %v", entryFullPath, err)
|
||||
glog.V(1).Infof("remove %s: %v", entryFullPath, err)
|
||||
if strings.Contains(err.Error(), filer.MsgFailDelNonEmptyFolder) {
|
||||
return fuse.Status(syscall.ENOTEMPTY)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,63 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"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/security"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
||||
request_id "github.com/seaweedfs/seaweedfs/weed/util/request_id"
|
||||
)
|
||||
|
||||
type serverSideWholeFileCopyOutcome uint8
|
||||
|
||||
const (
|
||||
serverSideWholeFileCopyNotCommitted serverSideWholeFileCopyOutcome = iota
|
||||
serverSideWholeFileCopyCommitted
|
||||
serverSideWholeFileCopyAmbiguous
|
||||
)
|
||||
|
||||
type wholeFileServerCopyRequest struct {
|
||||
srcPath util.FullPath
|
||||
dstPath util.FullPath
|
||||
sourceSize int64
|
||||
srcInode uint64
|
||||
srcMtime int64
|
||||
dstInode uint64
|
||||
dstMtime int64
|
||||
dstSize int64
|
||||
sourceMime string
|
||||
sourceMd5 []byte
|
||||
copyRequestID string
|
||||
}
|
||||
|
||||
// performServerSideWholeFileCopy is a package-level seam so tests can override
|
||||
// the filer call without standing up an HTTP endpoint.
|
||||
var performServerSideWholeFileCopy = func(cancel <-chan struct{}, wfs *WFS, copyRequest wholeFileServerCopyRequest) (*filer_pb.Entry, serverSideWholeFileCopyOutcome, error) {
|
||||
return wfs.copyEntryViaFiler(cancel, copyRequest)
|
||||
}
|
||||
|
||||
// filerCopyRequestTimeout bounds the mount->filer POST so a stalled copy does
|
||||
// not block copy_file_range workers indefinitely.
|
||||
const filerCopyRequestTimeout = 60 * time.Second
|
||||
|
||||
// filerCopyReadbackTimeout gives the follow-up metadata reload a fresh deadline
|
||||
// after the filer already accepted the copy request.
|
||||
const filerCopyReadbackTimeout = 15 * time.Second
|
||||
|
||||
// CopyFileRange copies data from one file to another from and to specified offsets.
|
||||
//
|
||||
// See https://man7.org/linux/man-pages/man2/copy_file_range.2.html
|
||||
@@ -70,6 +118,10 @@ func (wfs *WFS) CopyFileRange(cancel <-chan struct{}, in *fuse.CopyFileRangeIn)
|
||||
in.OffOut, in.OffOut+in.Len,
|
||||
)
|
||||
|
||||
if written, handled, status := wfs.tryServerSideWholeFileCopy(cancel, in, fhIn, fhOut); handled {
|
||||
return written, status
|
||||
}
|
||||
|
||||
// Concurrent copy operations could allocate too much memory, so we want to
|
||||
// throttle our concurrency, scaling with the number of writers the mount
|
||||
// was configured with.
|
||||
@@ -155,3 +207,306 @@ func (wfs *WFS) CopyFileRange(cancel <-chan struct{}, in *fuse.CopyFileRangeIn)
|
||||
written = uint32(totalCopied)
|
||||
return written, fuse.OK
|
||||
}
|
||||
|
||||
func (wfs *WFS) tryServerSideWholeFileCopy(cancel <-chan struct{}, in *fuse.CopyFileRangeIn, fhIn, fhOut *FileHandle) (written uint32, handled bool, code fuse.Status) {
|
||||
copyRequest, ok := wholeFileServerCopyCandidate(fhIn, fhOut, in)
|
||||
if !ok {
|
||||
return 0, false, fuse.OK
|
||||
}
|
||||
|
||||
glog.V(1).Infof("CopyFileRange server-side copy %s => %s (%d bytes)", copyRequest.srcPath, copyRequest.dstPath, copyRequest.sourceSize)
|
||||
|
||||
entry, outcome, err := performServerSideWholeFileCopy(cancel, wfs, copyRequest)
|
||||
switch outcome {
|
||||
case serverSideWholeFileCopyCommitted:
|
||||
if err != nil {
|
||||
glog.Warningf("CopyFileRange server-side copy %s => %s committed but local refresh failed: %v", copyRequest.srcPath, copyRequest.dstPath, err)
|
||||
} else {
|
||||
glog.V(1).Infof("CopyFileRange server-side copy %s => %s completed (%d bytes)", copyRequest.srcPath, copyRequest.dstPath, copyRequest.sourceSize)
|
||||
}
|
||||
wfs.applyServerSideWholeFileCopyResult(fhIn, fhOut, copyRequest.dstPath, entry, copyRequest.sourceSize)
|
||||
return uint32(copyRequest.sourceSize), true, fuse.OK
|
||||
case serverSideWholeFileCopyAmbiguous:
|
||||
glog.Warningf("CopyFileRange server-side copy %s => %s outcome ambiguous: %v", copyRequest.srcPath, copyRequest.dstPath, err)
|
||||
return 0, true, fuse.EIO
|
||||
default:
|
||||
glog.V(0).Infof("CopyFileRange server-side copy %s => %s fallback to chunk copy: %v", copyRequest.srcPath, copyRequest.dstPath, err)
|
||||
return 0, false, fuse.OK
|
||||
}
|
||||
}
|
||||
|
||||
func (wfs *WFS) applyServerSideWholeFileCopyResult(fhIn, fhOut *FileHandle, dstPath util.FullPath, entry *filer_pb.Entry, sourceSize int64) {
|
||||
if entry == nil {
|
||||
entry = synthesizeLocalEntryForServerSideWholeFileCopy(fhIn, fhOut, sourceSize)
|
||||
}
|
||||
if entry == nil {
|
||||
glog.Warningf("CopyFileRange server-side copy %s left no local entry to apply", dstPath)
|
||||
return
|
||||
}
|
||||
|
||||
fhOut.SetEntry(entry)
|
||||
fhOut.RememberPath(dstPath)
|
||||
if entry.Attributes != nil {
|
||||
fhOut.contentType = entry.Attributes.Mime
|
||||
}
|
||||
fhOut.dirtyMetadata = false
|
||||
wfs.updateServerSideWholeFileCopyMetaCache(dstPath, entry)
|
||||
wfs.invalidateCopyDestinationCache(fhOut.inode, dstPath)
|
||||
}
|
||||
|
||||
func (wfs *WFS) updateServerSideWholeFileCopyMetaCache(dstPath util.FullPath, entry *filer_pb.Entry) {
|
||||
if wfs.metaCache == nil || entry == nil {
|
||||
return
|
||||
}
|
||||
|
||||
dir, _ := dstPath.DirAndName()
|
||||
event := metadataUpdateEvent(dir, entry)
|
||||
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
|
||||
glog.Warningf("CopyFileRange metadata update %s: %v", dstPath, applyErr)
|
||||
wfs.markDirectoryReadThrough(util.FullPath(dir))
|
||||
}
|
||||
}
|
||||
|
||||
func synthesizeLocalEntryForServerSideWholeFileCopy(fhIn, fhOut *FileHandle, sourceSize int64) *filer_pb.Entry {
|
||||
dstEntry := fhOut.GetEntry().GetEntry()
|
||||
if dstEntry == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
localEntry := proto.Clone(dstEntry).(*filer_pb.Entry)
|
||||
if localEntry.Attributes == nil {
|
||||
localEntry.Attributes = &filer_pb.FuseAttributes{}
|
||||
}
|
||||
|
||||
if srcEntry := fhIn.GetEntry().GetEntry(); srcEntry != nil {
|
||||
srcEntryCopy := proto.Clone(srcEntry).(*filer_pb.Entry)
|
||||
localEntry.Content = srcEntryCopy.Content
|
||||
localEntry.Chunks = srcEntryCopy.Chunks
|
||||
if srcEntryCopy.Attributes != nil {
|
||||
localEntry.Attributes.Mime = srcEntryCopy.Attributes.Mime
|
||||
localEntry.Attributes.Md5 = srcEntryCopy.Attributes.Md5
|
||||
}
|
||||
}
|
||||
|
||||
localEntry.Attributes.FileSize = uint64(sourceSize)
|
||||
localEntry.Attributes.Mtime = time.Now().Unix()
|
||||
return localEntry
|
||||
}
|
||||
|
||||
func wholeFileServerCopyCandidate(fhIn, fhOut *FileHandle, in *fuse.CopyFileRangeIn) (copyRequest wholeFileServerCopyRequest, ok bool) {
|
||||
if fhIn == nil || fhOut == nil || in == nil {
|
||||
glog.V(4).Infof("server-side copy: skipped (nil handle or input)")
|
||||
return wholeFileServerCopyRequest{}, false
|
||||
}
|
||||
if fhIn.fh == fhOut.fh {
|
||||
glog.V(4).Infof("server-side copy: skipped (same file handle)")
|
||||
return wholeFileServerCopyRequest{}, false
|
||||
}
|
||||
if fhIn.dirtyMetadata || fhOut.dirtyMetadata {
|
||||
glog.V(4).Infof("server-side copy: skipped (dirty metadata: in=%v out=%v)", fhIn.dirtyMetadata, fhOut.dirtyMetadata)
|
||||
return wholeFileServerCopyRequest{}, false
|
||||
}
|
||||
if in.OffIn != 0 || in.OffOut != 0 {
|
||||
glog.V(4).Infof("server-side copy: skipped (non-zero offsets: in=%d out=%d)", in.OffIn, in.OffOut)
|
||||
return wholeFileServerCopyRequest{}, false
|
||||
}
|
||||
|
||||
srcEntry := fhIn.GetEntry()
|
||||
dstEntry := fhOut.GetEntry()
|
||||
if srcEntry == nil || dstEntry == nil {
|
||||
glog.V(4).Infof("server-side copy: skipped (nil entry: src=%v dst=%v)", srcEntry == nil, dstEntry == nil)
|
||||
return wholeFileServerCopyRequest{}, false
|
||||
}
|
||||
if srcEntry.IsDirectory || dstEntry.IsDirectory {
|
||||
glog.V(4).Infof("server-side copy: skipped (directory)")
|
||||
return wholeFileServerCopyRequest{}, false
|
||||
}
|
||||
|
||||
srcPbEntry := srcEntry.GetEntry()
|
||||
dstPbEntry := dstEntry.GetEntry()
|
||||
if srcPbEntry == nil || dstPbEntry == nil || srcPbEntry.Attributes == nil || dstPbEntry.Attributes == nil {
|
||||
glog.V(4).Infof("server-side copy: skipped (missing entry attributes)")
|
||||
return wholeFileServerCopyRequest{}, false
|
||||
}
|
||||
|
||||
sourceSize := int64(filer.FileSize(srcPbEntry))
|
||||
// go-fuse exposes CopyFileRange's return value as uint32, so the fast path
|
||||
// should only claim copies that can be reported without truncation.
|
||||
if sourceSize <= 0 || sourceSize > math.MaxUint32 || int64(in.Len) < sourceSize {
|
||||
glog.V(4).Infof("server-side copy: skipped (size mismatch: sourceSize=%d len=%d)", sourceSize, in.Len)
|
||||
return wholeFileServerCopyRequest{}, false
|
||||
}
|
||||
|
||||
dstSize := int64(filer.FileSize(dstPbEntry))
|
||||
if dstSize != 0 || len(dstPbEntry.GetChunks()) > 0 || len(dstPbEntry.Content) > 0 {
|
||||
glog.V(4).Infof("server-side copy: skipped (destination not empty)")
|
||||
return wholeFileServerCopyRequest{}, false
|
||||
}
|
||||
|
||||
srcPath := fhIn.FullPath()
|
||||
dstPath := fhOut.FullPath()
|
||||
if srcPath == "" || dstPath == "" || srcPath == dstPath {
|
||||
glog.V(4).Infof("server-side copy: skipped (invalid paths: src=%q dst=%q)", srcPath, dstPath)
|
||||
return wholeFileServerCopyRequest{}, false
|
||||
}
|
||||
|
||||
if srcPbEntry.Attributes.Inode == 0 || dstPbEntry.Attributes.Inode == 0 {
|
||||
glog.V(4).Infof("server-side copy: skipped (missing inode preconditions: src=%d dst=%d)", srcPbEntry.Attributes.Inode, dstPbEntry.Attributes.Inode)
|
||||
return wholeFileServerCopyRequest{}, false
|
||||
}
|
||||
|
||||
return wholeFileServerCopyRequest{
|
||||
srcPath: srcPath,
|
||||
dstPath: dstPath,
|
||||
sourceSize: sourceSize,
|
||||
srcInode: srcPbEntry.Attributes.Inode,
|
||||
srcMtime: srcPbEntry.Attributes.Mtime,
|
||||
dstInode: dstPbEntry.Attributes.Inode,
|
||||
dstMtime: dstPbEntry.Attributes.Mtime,
|
||||
dstSize: dstSize,
|
||||
sourceMime: srcPbEntry.Attributes.Mime,
|
||||
sourceMd5: append([]byte(nil), srcPbEntry.Attributes.Md5...),
|
||||
copyRequestID: request_id.New(),
|
||||
}, true
|
||||
}
|
||||
|
||||
func (wfs *WFS) copyEntryViaFiler(cancel <-chan struct{}, copyRequest wholeFileServerCopyRequest) (*filer_pb.Entry, serverSideWholeFileCopyOutcome, error) {
|
||||
baseCtx, baseCancel := context.WithCancel(context.Background())
|
||||
defer baseCancel()
|
||||
|
||||
if cancel != nil {
|
||||
go func() {
|
||||
select {
|
||||
case <-cancel:
|
||||
baseCancel()
|
||||
case <-baseCtx.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
postCtx, postCancel := context.WithTimeout(baseCtx, filerCopyRequestTimeout)
|
||||
defer postCancel()
|
||||
|
||||
httpClient := util_http.GetGlobalHttpClient()
|
||||
if httpClient == nil {
|
||||
var err error
|
||||
httpClient, err = util_http.NewGlobalHttpClient()
|
||||
if err != nil {
|
||||
return nil, serverSideWholeFileCopyNotCommitted, fmt.Errorf("create filer copy http client: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
copyURL := &url.URL{
|
||||
Scheme: httpClient.GetHttpScheme(),
|
||||
Host: wfs.getCurrentFiler().ToHttpAddress(),
|
||||
Path: string(copyRequest.dstPath),
|
||||
}
|
||||
query := copyURL.Query()
|
||||
query.Set(filer.CopyQueryParamFrom, string(copyRequest.srcPath))
|
||||
query.Set(filer.CopyQueryParamOverwrite, "true")
|
||||
query.Set(filer.CopyQueryParamDataOnly, "true")
|
||||
query.Set(filer.CopyQueryParamRequestID, copyRequest.copyRequestID)
|
||||
query.Set(filer.CopyQueryParamSourceInode, fmt.Sprintf("%d", copyRequest.srcInode))
|
||||
query.Set(filer.CopyQueryParamSourceMtime, fmt.Sprintf("%d", copyRequest.srcMtime))
|
||||
query.Set(filer.CopyQueryParamSourceSize, fmt.Sprintf("%d", copyRequest.sourceSize))
|
||||
query.Set(filer.CopyQueryParamDestinationInode, fmt.Sprintf("%d", copyRequest.dstInode))
|
||||
query.Set(filer.CopyQueryParamDestinationMtime, fmt.Sprintf("%d", copyRequest.dstMtime))
|
||||
query.Set(filer.CopyQueryParamDestinationSize, fmt.Sprintf("%d", copyRequest.dstSize))
|
||||
copyURL.RawQuery = query.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(postCtx, http.MethodPost, copyURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, serverSideWholeFileCopyNotCommitted, fmt.Errorf("create filer copy request: %w", err)
|
||||
}
|
||||
if jwt := wfs.filerCopyJWT(); jwt != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+string(jwt))
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return wfs.confirmServerSideWholeFileCopyAfterAmbiguousRequest(baseCtx, copyRequest, fmt.Errorf("execute filer copy request: %w", err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, serverSideWholeFileCopyNotCommitted, fmt.Errorf("filer copy %s => %s failed: status %d: %s", copyRequest.srcPath, copyRequest.dstPath, resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
readbackCtx, readbackCancel := context.WithTimeout(baseCtx, filerCopyReadbackTimeout)
|
||||
defer readbackCancel()
|
||||
|
||||
entry, err := filer_pb.GetEntry(readbackCtx, wfs, copyRequest.dstPath)
|
||||
if err != nil {
|
||||
return nil, serverSideWholeFileCopyCommitted, fmt.Errorf("reload copied entry %s: %w", copyRequest.dstPath, err)
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, serverSideWholeFileCopyCommitted, fmt.Errorf("reload copied entry %s: not found", copyRequest.dstPath)
|
||||
}
|
||||
if entry.Attributes != nil && wfs.option != nil && wfs.option.UidGidMapper != nil {
|
||||
entry.Attributes.Uid, entry.Attributes.Gid = wfs.option.UidGidMapper.FilerToLocal(entry.Attributes.Uid, entry.Attributes.Gid)
|
||||
}
|
||||
|
||||
return entry, serverSideWholeFileCopyCommitted, nil
|
||||
}
|
||||
|
||||
func (wfs *WFS) confirmServerSideWholeFileCopyAfterAmbiguousRequest(baseCtx context.Context, copyRequest wholeFileServerCopyRequest, requestErr error) (*filer_pb.Entry, serverSideWholeFileCopyOutcome, error) {
|
||||
readbackCtx, readbackCancel := context.WithTimeout(baseCtx, filerCopyReadbackTimeout)
|
||||
defer readbackCancel()
|
||||
|
||||
entry, err := filer_pb.GetEntry(readbackCtx, wfs, copyRequest.dstPath)
|
||||
if err == nil && entry != nil && entryMatchesServerSideWholeFileCopy(copyRequest, entry) {
|
||||
if entry.Attributes != nil && wfs.option != nil && wfs.option.UidGidMapper != nil {
|
||||
entry.Attributes.Uid, entry.Attributes.Gid = wfs.option.UidGidMapper.FilerToLocal(entry.Attributes.Uid, entry.Attributes.Gid)
|
||||
}
|
||||
return entry, serverSideWholeFileCopyCommitted, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, serverSideWholeFileCopyAmbiguous, fmt.Errorf("%w; post-copy readback failed: %v", requestErr, err)
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, serverSideWholeFileCopyAmbiguous, fmt.Errorf("%w; destination %s was not readable after the ambiguous request", requestErr, copyRequest.dstPath)
|
||||
}
|
||||
return nil, serverSideWholeFileCopyAmbiguous, fmt.Errorf("%w; destination %s did not match the requested copy after the ambiguous request", requestErr, copyRequest.dstPath)
|
||||
}
|
||||
|
||||
func entryMatchesServerSideWholeFileCopy(copyRequest wholeFileServerCopyRequest, entry *filer_pb.Entry) bool {
|
||||
if entry == nil || entry.Attributes == nil {
|
||||
return false
|
||||
}
|
||||
if copyRequest.dstInode != 0 && entry.Attributes.Inode != copyRequest.dstInode {
|
||||
return false
|
||||
}
|
||||
if entry.Attributes.FileSize != uint64(copyRequest.sourceSize) {
|
||||
return false
|
||||
}
|
||||
if copyRequest.sourceMime != "" && entry.Attributes.Mime != copyRequest.sourceMime {
|
||||
return false
|
||||
}
|
||||
if len(copyRequest.sourceMd5) > 0 && !bytes.Equal(entry.Attributes.Md5, copyRequest.sourceMd5) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (wfs *WFS) filerCopyJWT() security.EncodedJwt {
|
||||
if wfs.option == nil || len(wfs.option.FilerSigningKey) == 0 {
|
||||
return ""
|
||||
}
|
||||
return security.GenJwtForFilerServer(wfs.option.FilerSigningKey, wfs.option.FilerSigningExpiresAfterSec)
|
||||
}
|
||||
|
||||
func (wfs *WFS) invalidateCopyDestinationCache(inode uint64, fullPath util.FullPath) {
|
||||
if wfs.fuseServer != nil {
|
||||
if status := wfs.fuseServer.InodeNotify(inode, 0, -1); status != fuse.OK {
|
||||
glog.V(4).Infof("CopyFileRange invalidate inode %d: %v", inode, status)
|
||||
}
|
||||
dir, name := fullPath.DirAndName()
|
||||
if parentInode, found := wfs.inodeToPath.GetInode(util.FullPath(dir)); found {
|
||||
if status := wfs.fuseServer.EntryNotify(parentInode, name); status != fuse.OK {
|
||||
glog.V(4).Infof("CopyFileRange invalidate entry %s: %v", fullPath, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
"github.com/seaweedfs/seaweedfs/weed/mount/meta_cache"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
func TestWholeFileServerCopyCandidate(t *testing.T) {
|
||||
wfs := newCopyRangeTestWFS()
|
||||
|
||||
srcPath := util.FullPath("/src.txt")
|
||||
dstPath := util.FullPath("/dst.txt")
|
||||
srcInode := wfs.inodeToPath.Lookup(srcPath, 1, false, false, 0, true)
|
||||
dstInode := wfs.inodeToPath.Lookup(dstPath, 1, false, false, 0, true)
|
||||
|
||||
srcHandle := wfs.fhMap.AcquireFileHandle(wfs, srcInode, &filer_pb.Entry{
|
||||
Name: "src.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileMode: 0100644,
|
||||
FileSize: 5,
|
||||
Inode: srcInode,
|
||||
},
|
||||
Content: []byte("hello"),
|
||||
})
|
||||
dstHandle := wfs.fhMap.AcquireFileHandle(wfs, dstInode, &filer_pb.Entry{
|
||||
Name: "dst.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileMode: 0100644,
|
||||
Inode: dstInode,
|
||||
},
|
||||
})
|
||||
|
||||
srcHandle.RememberPath(srcPath)
|
||||
dstHandle.RememberPath(dstPath)
|
||||
|
||||
in := &fuse.CopyFileRangeIn{
|
||||
FhIn: uint64(srcHandle.fh),
|
||||
FhOut: uint64(dstHandle.fh),
|
||||
OffIn: 0,
|
||||
OffOut: 0,
|
||||
Len: 8,
|
||||
}
|
||||
|
||||
copyRequest, ok := wholeFileServerCopyCandidate(srcHandle, dstHandle, in)
|
||||
if !ok {
|
||||
t.Fatal("expected whole-file server copy candidate")
|
||||
}
|
||||
if copyRequest.srcPath != srcPath {
|
||||
t.Fatalf("source path = %q, want %q", copyRequest.srcPath, srcPath)
|
||||
}
|
||||
if copyRequest.dstPath != dstPath {
|
||||
t.Fatalf("destination path = %q, want %q", copyRequest.dstPath, dstPath)
|
||||
}
|
||||
if copyRequest.sourceSize != 5 {
|
||||
t.Fatalf("source size = %d, want 5", copyRequest.sourceSize)
|
||||
}
|
||||
if copyRequest.srcInode == 0 || copyRequest.dstInode == 0 {
|
||||
t.Fatalf("expected inode preconditions, got src=%d dst=%d", copyRequest.srcInode, copyRequest.dstInode)
|
||||
}
|
||||
|
||||
srcHandle.dirtyMetadata = true
|
||||
if _, ok := wholeFileServerCopyCandidate(srcHandle, dstHandle, in); ok {
|
||||
t.Fatal("dirty source handle should disable whole-file server copy")
|
||||
}
|
||||
srcHandle.dirtyMetadata = false
|
||||
|
||||
in.Len = 4
|
||||
if _, ok := wholeFileServerCopyCandidate(srcHandle, dstHandle, in); ok {
|
||||
t.Fatal("short copy request should disable whole-file server copy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyFileRangeUsesServerSideWholeFileCopy(t *testing.T) {
|
||||
wfs := newCopyRangeTestWFS()
|
||||
|
||||
srcPath := util.FullPath("/src.txt")
|
||||
dstPath := util.FullPath("/dst.txt")
|
||||
srcInode := wfs.inodeToPath.Lookup(srcPath, 1, false, false, 0, true)
|
||||
dstInode := wfs.inodeToPath.Lookup(dstPath, 1, false, false, 0, true)
|
||||
|
||||
srcHandle := wfs.fhMap.AcquireFileHandle(wfs, srcInode, &filer_pb.Entry{
|
||||
Name: "src.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileMode: 0100644,
|
||||
FileSize: 5,
|
||||
Inode: srcInode,
|
||||
},
|
||||
Content: []byte("hello"),
|
||||
})
|
||||
dstHandle := wfs.fhMap.AcquireFileHandle(wfs, dstInode, &filer_pb.Entry{
|
||||
Name: "dst.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileMode: 0100644,
|
||||
Inode: dstInode,
|
||||
},
|
||||
})
|
||||
|
||||
srcHandle.RememberPath(srcPath)
|
||||
dstHandle.RememberPath(dstPath)
|
||||
|
||||
originalCopy := performServerSideWholeFileCopy
|
||||
defer func() {
|
||||
performServerSideWholeFileCopy = originalCopy
|
||||
}()
|
||||
|
||||
var called bool
|
||||
performServerSideWholeFileCopy = func(cancel <-chan struct{}, gotWFS *WFS, copyRequest wholeFileServerCopyRequest) (*filer_pb.Entry, serverSideWholeFileCopyOutcome, error) {
|
||||
called = true
|
||||
if gotWFS != wfs {
|
||||
t.Fatalf("wfs = %p, want %p", gotWFS, wfs)
|
||||
}
|
||||
if copyRequest.srcPath != srcPath {
|
||||
t.Fatalf("source path = %q, want %q", copyRequest.srcPath, srcPath)
|
||||
}
|
||||
if copyRequest.dstPath != dstPath {
|
||||
t.Fatalf("destination path = %q, want %q", copyRequest.dstPath, dstPath)
|
||||
}
|
||||
return &filer_pb.Entry{
|
||||
Name: "dst.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileMode: 0100644,
|
||||
FileSize: 5,
|
||||
Mime: "text/plain; charset=utf-8",
|
||||
},
|
||||
Content: []byte("hello"),
|
||||
}, serverSideWholeFileCopyCommitted, nil
|
||||
}
|
||||
|
||||
written, status := wfs.CopyFileRange(make(chan struct{}), &fuse.CopyFileRangeIn{
|
||||
FhIn: uint64(srcHandle.fh),
|
||||
FhOut: uint64(dstHandle.fh),
|
||||
OffIn: 0,
|
||||
OffOut: 0,
|
||||
Len: 8,
|
||||
})
|
||||
if status != fuse.OK {
|
||||
t.Fatalf("CopyFileRange status = %v, want OK", status)
|
||||
}
|
||||
if written != 5 {
|
||||
t.Fatalf("CopyFileRange wrote %d bytes, want 5", written)
|
||||
}
|
||||
if !called {
|
||||
t.Fatal("expected server-side whole-file copy path to be used")
|
||||
}
|
||||
|
||||
gotEntry := dstHandle.GetEntry().GetEntry()
|
||||
if gotEntry.Attributes == nil || gotEntry.Attributes.FileSize != 5 {
|
||||
t.Fatalf("destination size = %v, want 5", gotEntry.GetAttributes().GetFileSize())
|
||||
}
|
||||
if string(gotEntry.Content) != "hello" {
|
||||
t.Fatalf("destination content = %q, want %q", string(gotEntry.Content), "hello")
|
||||
}
|
||||
if dstHandle.dirtyMetadata {
|
||||
t.Fatal("server-side whole-file copy should leave destination handle clean")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyFileRangeDoesNotFallbackAfterCommittedServerCopyRefreshFailure(t *testing.T) {
|
||||
wfs := newCopyRangeTestWFSWithMetaCache(t)
|
||||
|
||||
srcPath := util.FullPath("/src.txt")
|
||||
dstPath := util.FullPath("/dst.txt")
|
||||
srcInode := wfs.inodeToPath.Lookup(srcPath, 1, false, false, 0, true)
|
||||
dstInode := wfs.inodeToPath.Lookup(dstPath, 1, false, false, 0, true)
|
||||
|
||||
srcHandle := wfs.fhMap.AcquireFileHandle(wfs, srcInode, &filer_pb.Entry{
|
||||
Name: "src.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileMode: 0100644,
|
||||
FileSize: 5,
|
||||
Mime: "text/plain; charset=utf-8",
|
||||
Md5: []byte("abcde"),
|
||||
Inode: srcInode,
|
||||
},
|
||||
Content: []byte("hello"),
|
||||
})
|
||||
dstHandle := wfs.fhMap.AcquireFileHandle(wfs, dstInode, &filer_pb.Entry{
|
||||
Name: "dst.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileMode: 0100600,
|
||||
Inode: dstInode,
|
||||
},
|
||||
})
|
||||
|
||||
srcHandle.RememberPath(srcPath)
|
||||
dstHandle.RememberPath(dstPath)
|
||||
|
||||
originalCopy := performServerSideWholeFileCopy
|
||||
defer func() {
|
||||
performServerSideWholeFileCopy = originalCopy
|
||||
}()
|
||||
|
||||
performServerSideWholeFileCopy = func(cancel <-chan struct{}, gotWFS *WFS, copyRequest wholeFileServerCopyRequest) (*filer_pb.Entry, serverSideWholeFileCopyOutcome, error) {
|
||||
if gotWFS != wfs || copyRequest.srcPath != srcPath || copyRequest.dstPath != dstPath {
|
||||
t.Fatalf("unexpected server-side copy call: wfs=%p src=%q dst=%q", gotWFS, copyRequest.srcPath, copyRequest.dstPath)
|
||||
}
|
||||
return nil, serverSideWholeFileCopyCommitted, errors.New("reload copied entry: transient filer read failure")
|
||||
}
|
||||
|
||||
written, status := wfs.CopyFileRange(make(chan struct{}), &fuse.CopyFileRangeIn{
|
||||
FhIn: uint64(srcHandle.fh),
|
||||
FhOut: uint64(dstHandle.fh),
|
||||
OffIn: 0,
|
||||
OffOut: 0,
|
||||
Len: 8,
|
||||
})
|
||||
if status != fuse.OK {
|
||||
t.Fatalf("CopyFileRange status = %v, want OK", status)
|
||||
}
|
||||
if written != 5 {
|
||||
t.Fatalf("CopyFileRange wrote %d bytes, want 5", written)
|
||||
}
|
||||
if dstHandle.dirtyMetadata {
|
||||
t.Fatal("committed server-side copy should not fall back to dirty-page copy")
|
||||
}
|
||||
|
||||
gotEntry := dstHandle.GetEntry().GetEntry()
|
||||
if gotEntry.GetAttributes().GetFileSize() != 5 {
|
||||
t.Fatalf("destination size = %d, want 5", gotEntry.GetAttributes().GetFileSize())
|
||||
}
|
||||
if gotEntry.GetAttributes().GetFileMode() != 0100600 {
|
||||
t.Fatalf("destination mode = %#o, want %#o", gotEntry.GetAttributes().GetFileMode(), uint32(0100600))
|
||||
}
|
||||
if string(gotEntry.GetContent()) != "hello" {
|
||||
t.Fatalf("destination content = %q, want %q", string(gotEntry.GetContent()), "hello")
|
||||
}
|
||||
|
||||
cachedEntry, err := wfs.metaCache.FindEntry(context.Background(), dstPath)
|
||||
if err != nil {
|
||||
t.Fatalf("metaCache find entry: %v", err)
|
||||
}
|
||||
if cachedEntry.FileSize != 5 {
|
||||
t.Fatalf("metaCache destination size = %d, want 5", cachedEntry.FileSize)
|
||||
}
|
||||
if cachedEntry.Mime != "text/plain; charset=utf-8" {
|
||||
t.Fatalf("metaCache destination mime = %q, want %q", cachedEntry.Mime, "text/plain; charset=utf-8")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyFileRangeReturnsEIOForAmbiguousServerSideCopy(t *testing.T) {
|
||||
wfs := newCopyRangeTestWFS()
|
||||
|
||||
srcPath := util.FullPath("/src.txt")
|
||||
dstPath := util.FullPath("/dst.txt")
|
||||
srcInode := wfs.inodeToPath.Lookup(srcPath, 1, false, false, 0, true)
|
||||
dstInode := wfs.inodeToPath.Lookup(dstPath, 1, false, false, 0, true)
|
||||
|
||||
srcHandle := wfs.fhMap.AcquireFileHandle(wfs, srcInode, &filer_pb.Entry{
|
||||
Name: "src.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileMode: 0100644,
|
||||
FileSize: 5,
|
||||
Inode: srcInode,
|
||||
},
|
||||
Content: []byte("hello"),
|
||||
})
|
||||
dstHandle := wfs.fhMap.AcquireFileHandle(wfs, dstInode, &filer_pb.Entry{
|
||||
Name: "dst.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileMode: 0100600,
|
||||
Inode: dstInode,
|
||||
},
|
||||
})
|
||||
|
||||
srcHandle.RememberPath(srcPath)
|
||||
dstHandle.RememberPath(dstPath)
|
||||
|
||||
originalCopy := performServerSideWholeFileCopy
|
||||
defer func() {
|
||||
performServerSideWholeFileCopy = originalCopy
|
||||
}()
|
||||
|
||||
performServerSideWholeFileCopy = func(cancel <-chan struct{}, gotWFS *WFS, copyRequest wholeFileServerCopyRequest) (*filer_pb.Entry, serverSideWholeFileCopyOutcome, error) {
|
||||
if gotWFS != wfs || copyRequest.srcPath != srcPath || copyRequest.dstPath != dstPath {
|
||||
t.Fatalf("unexpected server-side copy call: wfs=%p src=%q dst=%q", gotWFS, copyRequest.srcPath, copyRequest.dstPath)
|
||||
}
|
||||
return nil, serverSideWholeFileCopyAmbiguous, errors.New("transport timeout after request dispatch")
|
||||
}
|
||||
|
||||
written, status := wfs.CopyFileRange(make(chan struct{}), &fuse.CopyFileRangeIn{
|
||||
FhIn: uint64(srcHandle.fh),
|
||||
FhOut: uint64(dstHandle.fh),
|
||||
OffIn: 0,
|
||||
OffOut: 0,
|
||||
Len: 8,
|
||||
})
|
||||
if status != fuse.EIO {
|
||||
t.Fatalf("CopyFileRange status = %v, want EIO", status)
|
||||
}
|
||||
if written != 0 {
|
||||
t.Fatalf("CopyFileRange wrote %d bytes, want 0", written)
|
||||
}
|
||||
if dstHandle.dirtyMetadata {
|
||||
t.Fatal("ambiguous server-side copy should not fall back to dirty-page copy")
|
||||
}
|
||||
if dstHandle.GetEntry().GetEntry().GetAttributes().GetFileSize() != 0 {
|
||||
t.Fatalf("destination size = %d, want 0", dstHandle.GetEntry().GetEntry().GetAttributes().GetFileSize())
|
||||
}
|
||||
}
|
||||
|
||||
func newCopyRangeTestWFS() *WFS {
|
||||
wfs := &WFS{
|
||||
option: &Option{
|
||||
ChunkSizeLimit: 1024,
|
||||
ConcurrentReaders: 1,
|
||||
VolumeServerAccess: "filerProxy",
|
||||
FilerAddresses: []pb.ServerAddress{"127.0.0.1:8888"},
|
||||
},
|
||||
inodeToPath: NewInodeToPath(util.FullPath("/"), 0),
|
||||
fhMap: NewFileHandleToInode(),
|
||||
fhLockTable: util.NewLockTable[FileHandleId](),
|
||||
}
|
||||
wfs.copyBufferPool.New = func() any {
|
||||
return make([]byte, 1024)
|
||||
}
|
||||
return wfs
|
||||
}
|
||||
|
||||
func newCopyRangeTestWFSWithMetaCache(t *testing.T) *WFS {
|
||||
t.Helper()
|
||||
|
||||
wfs := newCopyRangeTestWFS()
|
||||
root := util.FullPath("/")
|
||||
wfs.inodeToPath.MarkChildrenCached(root)
|
||||
uidGidMapper, err := meta_cache.NewUidGidMapper("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("create uid/gid mapper: %v", err)
|
||||
}
|
||||
wfs.metaCache = meta_cache.NewMetaCache(
|
||||
filepath.Join(t.TempDir(), "meta"),
|
||||
uidGidMapper,
|
||||
root,
|
||||
func(path util.FullPath) {
|
||||
wfs.inodeToPath.MarkChildrenCached(path)
|
||||
},
|
||||
func(path util.FullPath) bool {
|
||||
return wfs.inodeToPath.IsChildrenCached(path)
|
||||
},
|
||||
func(util.FullPath, *filer_pb.Entry) {},
|
||||
nil,
|
||||
)
|
||||
t.Cleanup(func() {
|
||||
wfs.metaCache.Shutdown()
|
||||
})
|
||||
|
||||
return wfs
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
)
|
||||
import "github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
|
||||
/**
|
||||
* Open a file
|
||||
@@ -66,15 +63,7 @@ func (wfs *WFS) Open(cancel <-chan struct{}, in *fuse.OpenIn, out *fuse.OpenOut)
|
||||
fileHandle, status = wfs.AcquireHandle(in.NodeId, in.Flags, in.Uid, in.Gid)
|
||||
if status == fuse.OK {
|
||||
out.Fh = uint64(fileHandle.fh)
|
||||
out.OpenFlags = in.Flags
|
||||
if wfs.option.IsMacOs {
|
||||
// remove the direct_io flag, as it is not well-supported on macOS
|
||||
// https://code.google.com/archive/p/macfuse/wikis/OPTIONS.wiki recommended to avoid the direct_io flag
|
||||
if in.Flags&fuse.FOPEN_DIRECT_IO != 0 {
|
||||
glog.V(4).Infof("macfuse direct_io mode %v => false\n", in.Flags&fuse.FOPEN_DIRECT_IO != 0)
|
||||
out.OpenFlags &^= fuse.FOPEN_DIRECT_IO
|
||||
}
|
||||
}
|
||||
out.OpenFlags = 0
|
||||
// TODO https://github.com/libfuse/libfuse/blob/master/include/fuse_common.h#L64
|
||||
}
|
||||
return status
|
||||
@@ -106,5 +95,8 @@ func (wfs *WFS) Open(cancel <-chan struct{}, in *fuse.OpenIn, out *fuse.OpenOut)
|
||||
* @param fi file information
|
||||
*/
|
||||
func (wfs *WFS) Release(cancel <-chan struct{}, in *fuse.ReleaseIn) {
|
||||
if in.ReleaseFlags&fuse.FUSE_RELEASE_FLOCK_UNLOCK != 0 {
|
||||
wfs.posixLocks.ReleaseFlockOwner(in.NodeId, in.LockOwner)
|
||||
}
|
||||
wfs.ReleaseHandle(FileHandleId(in.Fh))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
)
|
||||
|
||||
// GetLk queries for a conflicting lock on the file.
|
||||
// If a conflict exists, the conflicting lock is returned in out.
|
||||
// If no conflict, out.Lk.Typ is set to F_UNLCK.
|
||||
func (wfs *WFS) GetLk(cancel <-chan struct{}, in *fuse.LkIn, out *fuse.LkOut) fuse.Status {
|
||||
proposed := lockRange{
|
||||
Start: in.Lk.Start,
|
||||
End: in.Lk.End,
|
||||
Typ: in.Lk.Typ,
|
||||
Owner: in.Owner,
|
||||
Pid: in.Lk.Pid,
|
||||
IsFlock: in.LkFlags&fuse.FUSE_LK_FLOCK != 0,
|
||||
}
|
||||
wfs.posixLocks.GetLk(in.NodeId, proposed, out)
|
||||
return fuse.OK
|
||||
}
|
||||
|
||||
// SetLk sets or clears a POSIX lock (non-blocking).
|
||||
// Returns EAGAIN if the lock conflicts with an existing lock from another owner.
|
||||
func (wfs *WFS) SetLk(cancel <-chan struct{}, in *fuse.LkIn) fuse.Status {
|
||||
lk := lockRange{
|
||||
Start: in.Lk.Start,
|
||||
End: in.Lk.End,
|
||||
Typ: in.Lk.Typ,
|
||||
Owner: in.Owner,
|
||||
Pid: in.Lk.Pid,
|
||||
IsFlock: in.LkFlags&fuse.FUSE_LK_FLOCK != 0,
|
||||
}
|
||||
return wfs.posixLocks.SetLk(in.NodeId, lk)
|
||||
}
|
||||
|
||||
// SetLkw sets a POSIX lock (blocking).
|
||||
// Waits until the lock can be acquired or the request is cancelled.
|
||||
func (wfs *WFS) SetLkw(cancel <-chan struct{}, in *fuse.LkIn) fuse.Status {
|
||||
lk := lockRange{
|
||||
Start: in.Lk.Start,
|
||||
End: in.Lk.End,
|
||||
Typ: in.Lk.Typ,
|
||||
Owner: in.Owner,
|
||||
Pid: in.Lk.Pid,
|
||||
IsFlock: in.LkFlags&fuse.FUSE_LK_FLOCK != 0,
|
||||
}
|
||||
if lk.Typ == syscall.F_UNLCK {
|
||||
return wfs.posixLocks.SetLk(in.NodeId, lk)
|
||||
}
|
||||
return wfs.posixLocks.SetLkw(in.NodeId, lk, cancel)
|
||||
}
|
||||
+251
-74
@@ -6,8 +6,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
"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"
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -21,10 +23,97 @@ import (
|
||||
* will be called instead.
|
||||
*/
|
||||
func (wfs *WFS) Create(cancel <-chan struct{}, in *fuse.CreateIn, name string, out *fuse.CreateOut) (code fuse.Status) {
|
||||
// if implemented, need to use
|
||||
// inode := wfs.inodeToPath.Lookup(entryFullPath)
|
||||
// to ensure nlookup counter
|
||||
return fuse.ENOSYS
|
||||
if 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
|
||||
|
||||
newEntry, code := wfs.maybeLoadEntry(entryFullPath)
|
||||
if code == fuse.OK {
|
||||
if newEntry == nil || newEntry.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, newEntry.Attributes.Uid, newEntry.Attributes.Gid, newEntry.Attributes.FileMode)
|
||||
return fuse.Status(syscall.EEXIST)
|
||||
}
|
||||
inode = wfs.inodeToPath.Lookup(entryFullPath, newEntry.Attributes.Crtime, false, len(newEntry.HardLinkId) > 0, newEntry.Attributes.Inode, true)
|
||||
fileHandle, status := wfs.AcquireHandle(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, newEntry); code != fuse.OK {
|
||||
wfs.ReleaseHandle(fileHandle.fh)
|
||||
return code
|
||||
}
|
||||
newEntry = fileHandle.GetEntry().GetEntry()
|
||||
}
|
||||
|
||||
wfs.outputPbEntry(&out.EntryOut, inode, newEntry)
|
||||
out.Fh = uint64(fileHandle.fh)
|
||||
out.OpenFlags = 0
|
||||
return fuse.OK
|
||||
}
|
||||
if code != fuse.ENOENT {
|
||||
return code
|
||||
}
|
||||
|
||||
inode, newEntry, code = wfs.createRegularFile(dirFullPath, name, in.Mode, in.Uid, in.Gid, 0, 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.
|
||||
newEntry, code = wfs.maybeLoadEntry(entryFullPath)
|
||||
if code != fuse.OK {
|
||||
return code
|
||||
}
|
||||
if newEntry == nil || newEntry.Attributes == nil {
|
||||
return fuse.EIO
|
||||
}
|
||||
inode = wfs.inodeToPath.Lookup(entryFullPath, newEntry.Attributes.Crtime, false, len(newEntry.HardLinkId) > 0, newEntry.Attributes.Inode, true)
|
||||
fileHandle, status := wfs.AcquireHandle(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, newEntry); code != fuse.OK {
|
||||
wfs.ReleaseHandle(fileHandle.fh)
|
||||
return code
|
||||
}
|
||||
newEntry = fileHandle.GetEntry().GetEntry()
|
||||
}
|
||||
wfs.outputPbEntry(&out.EntryOut, inode, newEntry)
|
||||
out.Fh = uint64(fileHandle.fh)
|
||||
out.OpenFlags = 0
|
||||
return fuse.OK
|
||||
} else if code != fuse.OK {
|
||||
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.
|
||||
fileHandle := wfs.fhMap.AcquireFileHandle(wfs, inode, newEntry)
|
||||
fileHandle.RememberPath(entryFullPath)
|
||||
// Mark dirty so the deferred filer create happens on Flush,
|
||||
// even if the file is closed without any writes.
|
||||
fileHandle.dirtyMetadata = true
|
||||
out.Fh = uint64(fileHandle.fh)
|
||||
out.OpenFlags = 0
|
||||
|
||||
return fuse.OK
|
||||
}
|
||||
|
||||
/** Create a file node
|
||||
@@ -35,10 +124,6 @@ func (wfs *WFS) Create(cancel <-chan struct{}, in *fuse.CreateIn, name string, o
|
||||
*/
|
||||
func (wfs *WFS) Mknod(cancel <-chan struct{}, in *fuse.MknodIn, name string, out *fuse.EntryOut) (code fuse.Status) {
|
||||
|
||||
if wfs.IsOverQuotaWithUncommitted() {
|
||||
return fuse.Status(syscall.ENOSPC)
|
||||
}
|
||||
|
||||
if s := checkName(name); s != fuse.OK {
|
||||
return s
|
||||
}
|
||||
@@ -48,65 +133,13 @@ func (wfs *WFS) Mknod(cancel <-chan struct{}, in *fuse.MknodIn, name string, out
|
||||
return
|
||||
}
|
||||
|
||||
entryFullPath := dirFullPath.Child(name)
|
||||
fileMode := toOsFileMode(in.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,
|
||||
FileMode: uint32(fileMode),
|
||||
Uid: in.Uid,
|
||||
Gid: in.Gid,
|
||||
TtlSec: wfs.option.TtlSec,
|
||||
Rdev: in.Rdev,
|
||||
Inode: inode,
|
||||
},
|
||||
}
|
||||
|
||||
err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
|
||||
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("mknod: %v", request)
|
||||
resp, err := filer_pb.CreateEntryWithResponse(context.Background(), client, request)
|
||||
if err != nil {
|
||||
glog.V(0).Infof("mknod %s: %v", entryFullPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
event := resp.GetMetadataEvent()
|
||||
if event == nil {
|
||||
event = metadataCreateEvent(string(dirFullPath), newEntry)
|
||||
}
|
||||
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
|
||||
glog.Warningf("mknod %s: best-effort metadata apply failed: %v", entryFullPath, applyErr)
|
||||
wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
|
||||
}
|
||||
wfs.inodeToPath.TouchDirectory(dirFullPath)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
glog.V(3).Infof("mknod %s: %v", entryFullPath, err)
|
||||
|
||||
if err != nil {
|
||||
return fuse.EIO
|
||||
inode, newEntry, code := wfs.createRegularFile(dirFullPath, name, in.Mode, in.Uid, in.Gid, in.Rdev, 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)
|
||||
@@ -139,11 +172,37 @@ func (wfs *WFS) Unlink(cancel <-chan struct{}, header *fuse.InHeader, name strin
|
||||
return fuse.EPERM
|
||||
}
|
||||
|
||||
// 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.
|
||||
if inode, found := wfs.inodeToPath.GetInode(entryFullPath); found {
|
||||
if fh, fhFound := wfs.fhMap.FindFileHandle(inode); fhFound {
|
||||
fh.isDeleted = true
|
||||
}
|
||||
wfs.waitForPendingAsyncFlush(inode)
|
||||
} else if entry != nil && entry.Attributes != nil && entry.Attributes.Inode != 0 {
|
||||
inodeFromEntry := entry.Attributes.Inode
|
||||
if fh, fhFound := wfs.fhMap.FindFileHandle(inodeFromEntry); fhFound {
|
||||
fh.isDeleted = true
|
||||
}
|
||||
wfs.waitForPendingAsyncFlush(inodeFromEntry)
|
||||
}
|
||||
|
||||
// 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.
|
||||
resp, err := filer_pb.RemoveWithResponse(context.Background(), wfs, string(dirFullPath), name, true, false, false, false, []int32{wfs.signature})
|
||||
deleteReq := &filer_pb.DeleteEntryRequest{
|
||||
Directory: string(dirFullPath),
|
||||
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
|
||||
@@ -161,17 +220,135 @@ func (wfs *WFS) Unlink(cancel <-chan struct{}, header *fuse.InHeader, name strin
|
||||
}
|
||||
wfs.inodeToPath.TouchDirectory(dirFullPath)
|
||||
|
||||
// If there is an async-draining handle for this file, mark it as deleted
|
||||
// so the background flush skips the metadata write instead of recreating
|
||||
// the just-unlinked entry. The handle is still in fhMap during drain.
|
||||
if inode, found := wfs.inodeToPath.GetInode(entryFullPath); found {
|
||||
if fh, fhFound := wfs.fhMap.FindFileHandle(inode); fhFound {
|
||||
fh.isDeleted = true
|
||||
}
|
||||
}
|
||||
|
||||
wfs.inodeToPath.RemovePath(entryFullPath)
|
||||
|
||||
return fuse.OK
|
||||
|
||||
}
|
||||
|
||||
func (wfs *WFS) createRegularFile(dirFullPath util.FullPath, name string, mode uint32, uid, gid, rdev uint32, deferFilerCreate bool) (inode uint64, newEntry *filer_pb.Entry, code fuse.Status) {
|
||||
if wfs.IsOverQuotaWithUncommitted() {
|
||||
return 0, nil, fuse.Status(syscall.ENOSPC)
|
||||
}
|
||||
|
||||
// Verify write+search permission on the parent directory.
|
||||
parentEntry, parentStatus := wfs.maybeLoadEntry(dirFullPath)
|
||||
if parentStatus != fuse.OK {
|
||||
return 0, nil, parentStatus
|
||||
}
|
||||
if parentEntry == nil || parentEntry.Attributes == nil {
|
||||
return 0, nil, fuse.EIO
|
||||
}
|
||||
// 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 _, status := wfs.maybeLoadEntry(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,
|
||||
FileMode: uint32(fileMode),
|
||||
Uid: uid,
|
||||
Gid: gid,
|
||||
TtlSec: wfs.option.TtlSec,
|
||||
Rdev: rdev,
|
||||
Inode: inode,
|
||||
},
|
||||
}
|
||||
|
||||
if deferFilerCreate {
|
||||
// Defer the filer gRPC call to flush time. The caller (Create) will
|
||||
// build a file handle directly from newEntry, bypassing AcquireHandle.
|
||||
// Insert a local placeholder into the metadata cache so that
|
||||
// maybeLoadEntry() can find the file (e.g., duplicate-create checks,
|
||||
// stat, readdir). The actual filer entry is created by flushMetadataToFiler.
|
||||
// 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)); insertErr != nil {
|
||||
glog.Warningf("createFile %s: insert local entry: %v", entryFullPath, insertErr)
|
||||
}
|
||||
glog.V(3).Infof("createFile %s: deferred to flush", 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: %v", request)
|
||||
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)
|
||||
}
|
||||
|
||||
glog.V(3).Infof("createFile %s: %v", entryFullPath, err)
|
||||
|
||||
if err != nil {
|
||||
return 0, nil, grpcErrorToFuseStatus(err)
|
||||
}
|
||||
|
||||
return inode, newEntry, fuse.OK
|
||||
}
|
||||
|
||||
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
|
||||
entry.Attributes.Mtime = time.Now().Unix()
|
||||
|
||||
if code := wfs.saveEntry(entryFullPath, entry); code != fuse.OK {
|
||||
return code
|
||||
}
|
||||
|
||||
if inode, found := wfs.inodeToPath.GetInode(entryFullPath); found {
|
||||
if fh, fhFound := wfs.fhMap.FindFileHandle(inode); fhFound {
|
||||
fhActiveLock := fh.wfs.fhLockTable.AcquireLock("truncateEntry", fh.fh, util.ExclusiveLock)
|
||||
fh.ResetDirtyPages()
|
||||
fh.SetEntry(entry)
|
||||
fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock)
|
||||
}
|
||||
}
|
||||
|
||||
return fuse.OK
|
||||
}
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/mount/meta_cache"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
type createEntryTestServer struct {
|
||||
filer_pb.UnimplementedSeaweedFilerServer
|
||||
mu sync.Mutex
|
||||
lastDirectory string
|
||||
lastName string
|
||||
lastUID uint32
|
||||
lastGID uint32
|
||||
lastMode uint32
|
||||
}
|
||||
|
||||
type createEntrySnapshot struct {
|
||||
directory string
|
||||
name string
|
||||
uid uint32
|
||||
gid uint32
|
||||
mode uint32
|
||||
}
|
||||
|
||||
func (s *createEntryTestServer) CreateEntry(ctx context.Context, req *filer_pb.CreateEntryRequest) (*filer_pb.CreateEntryResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.lastDirectory = req.GetDirectory()
|
||||
if req.GetEntry() != nil {
|
||||
s.lastName = req.GetEntry().GetName()
|
||||
if req.GetEntry().GetAttributes() != nil {
|
||||
s.lastUID = req.GetEntry().GetAttributes().GetUid()
|
||||
s.lastGID = req.GetEntry().GetAttributes().GetGid()
|
||||
s.lastMode = req.GetEntry().GetAttributes().GetFileMode()
|
||||
}
|
||||
}
|
||||
return &filer_pb.CreateEntryResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *createEntryTestServer) UpdateEntry(ctx context.Context, req *filer_pb.UpdateEntryRequest) (*filer_pb.UpdateEntryResponse, error) {
|
||||
return &filer_pb.UpdateEntryResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *createEntryTestServer) snapshot() createEntrySnapshot {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return createEntrySnapshot{
|
||||
directory: s.lastDirectory,
|
||||
name: s.lastName,
|
||||
uid: s.lastUID,
|
||||
gid: s.lastGID,
|
||||
mode: s.lastMode,
|
||||
}
|
||||
}
|
||||
|
||||
func newCreateTestWFS(t *testing.T) (*WFS, *createEntryTestServer) {
|
||||
t.Helper()
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = listener.Close()
|
||||
})
|
||||
|
||||
server := pb.NewGrpcServer()
|
||||
testServer := &createEntryTestServer{}
|
||||
filer_pb.RegisterSeaweedFilerServer(server, testServer)
|
||||
go server.Serve(listener)
|
||||
t.Cleanup(server.Stop)
|
||||
|
||||
uidGidMapper, err := meta_cache.NewUidGidMapper("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("create uid/gid mapper: %v", err)
|
||||
}
|
||||
|
||||
root := util.FullPath("/")
|
||||
option := &Option{
|
||||
ChunkSizeLimit: 1024,
|
||||
ConcurrentReaders: 1,
|
||||
VolumeServerAccess: "filerProxy",
|
||||
FilerAddresses: []pb.ServerAddress{
|
||||
pb.NewServerAddressWithGrpcPort("127.0.0.1:1", listener.Addr().(*net.TCPAddr).Port),
|
||||
},
|
||||
GrpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
FilerMountRootPath: "/",
|
||||
MountUid: 99,
|
||||
MountGid: 100,
|
||||
MountMode: 0o777,
|
||||
MountMtime: time.Now(),
|
||||
MountCtime: time.Now(),
|
||||
UidGidMapper: uidGidMapper,
|
||||
uniqueCacheDirForWrite: t.TempDir(),
|
||||
}
|
||||
|
||||
wfs := &WFS{
|
||||
option: option,
|
||||
signature: 1,
|
||||
inodeToPath: NewInodeToPath(root, 0),
|
||||
fhMap: NewFileHandleToInode(),
|
||||
fhLockTable: util.NewLockTable[FileHandleId](),
|
||||
}
|
||||
wfs.metaCache = meta_cache.NewMetaCache(
|
||||
filepath.Join(t.TempDir(), "meta"),
|
||||
uidGidMapper,
|
||||
root,
|
||||
func(path util.FullPath) {
|
||||
wfs.inodeToPath.MarkChildrenCached(path)
|
||||
},
|
||||
func(path util.FullPath) bool {
|
||||
return wfs.inodeToPath.IsChildrenCached(path)
|
||||
},
|
||||
func(util.FullPath, *filer_pb.Entry) {},
|
||||
nil,
|
||||
)
|
||||
wfs.inodeToPath.MarkChildrenCached(root)
|
||||
t.Cleanup(func() {
|
||||
wfs.metaCache.Shutdown()
|
||||
})
|
||||
|
||||
return wfs, testServer
|
||||
}
|
||||
|
||||
func TestCreateCreatesAndOpensFile(t *testing.T) {
|
||||
wfs, testServer := newCreateTestWFS(t)
|
||||
|
||||
out := &fuse.CreateOut{}
|
||||
status := wfs.Create(make(chan struct{}), &fuse.CreateIn{
|
||||
InHeader: fuse.InHeader{
|
||||
NodeId: 1,
|
||||
Caller: fuse.Caller{
|
||||
Owner: fuse.Owner{
|
||||
Uid: 123,
|
||||
Gid: 456,
|
||||
},
|
||||
},
|
||||
},
|
||||
Flags: syscall.O_WRONLY | syscall.O_CREAT,
|
||||
Mode: 0o640,
|
||||
}, "hello.txt", out)
|
||||
if status != fuse.OK {
|
||||
t.Fatalf("Create status = %v, want OK", status)
|
||||
}
|
||||
if out.NodeId == 0 {
|
||||
t.Fatal("Create returned zero inode")
|
||||
}
|
||||
if out.Fh == 0 {
|
||||
t.Fatal("Create returned zero file handle")
|
||||
}
|
||||
if out.OpenFlags != 0 {
|
||||
t.Fatalf("Create returned OpenFlags = %#x, want 0", out.OpenFlags)
|
||||
}
|
||||
|
||||
fileHandle := wfs.GetHandle(FileHandleId(out.Fh))
|
||||
if fileHandle == nil {
|
||||
t.Fatal("Create did not register an open file handle")
|
||||
}
|
||||
if got := fileHandle.FullPath(); got != "/hello.txt" {
|
||||
t.Fatalf("FullPath = %q, want %q", got, "/hello.txt")
|
||||
}
|
||||
|
||||
// File creation is deferred to flush time. Trigger a synchronous flush
|
||||
// so the CreateEntry gRPC call is sent to the test server.
|
||||
if flushStatus := wfs.Flush(make(chan struct{}), &fuse.FlushIn{
|
||||
InHeader: fuse.InHeader{
|
||||
NodeId: out.NodeId,
|
||||
Caller: fuse.Caller{Owner: fuse.Owner{Uid: 123, Gid: 456}},
|
||||
},
|
||||
Fh: out.Fh,
|
||||
}); flushStatus != fuse.OK {
|
||||
t.Fatalf("Flush status = %v, want OK", flushStatus)
|
||||
}
|
||||
|
||||
snapshot := testServer.snapshot()
|
||||
if snapshot.directory != "/" {
|
||||
t.Fatalf("CreateEntry directory = %q, want %q", snapshot.directory, "/")
|
||||
}
|
||||
if snapshot.name != "hello.txt" {
|
||||
t.Fatalf("CreateEntry name = %q, want %q", snapshot.name, "hello.txt")
|
||||
}
|
||||
if snapshot.uid != 123 || snapshot.gid != 456 {
|
||||
t.Fatalf("CreateEntry uid/gid = %d/%d, want 123/456", snapshot.uid, snapshot.gid)
|
||||
}
|
||||
if snapshot.mode != 0o640 {
|
||||
t.Fatalf("CreateEntry mode = %o, want %o", snapshot.mode, 0o640)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateEntryClearsDirtyPagesForOpenHandle(t *testing.T) {
|
||||
wfs, _ := newCreateTestWFS(t)
|
||||
|
||||
fullPath := util.FullPath("/truncate.txt")
|
||||
inode := wfs.inodeToPath.Lookup(fullPath, 1, false, false, 0, true)
|
||||
entry := &filer_pb.Entry{
|
||||
Name: "truncate.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileMode: 0o644,
|
||||
FileSize: 5,
|
||||
Inode: inode,
|
||||
Crtime: 1,
|
||||
Mtime: 1,
|
||||
},
|
||||
}
|
||||
|
||||
fh := wfs.fhMap.AcquireFileHandle(wfs, inode, entry)
|
||||
fh.RememberPath(fullPath)
|
||||
|
||||
if err := fh.dirtyPages.AddPage(0, []byte("hello"), true, time.Now().UnixNano()); err != nil {
|
||||
t.Fatalf("AddPage: %v", err)
|
||||
}
|
||||
oldDirtyPages := fh.dirtyPages
|
||||
|
||||
truncatedEntry := &filer_pb.Entry{
|
||||
Name: "truncate.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileMode: 0o644,
|
||||
FileSize: 5,
|
||||
Inode: inode,
|
||||
Crtime: 1,
|
||||
Mtime: 1,
|
||||
},
|
||||
}
|
||||
|
||||
if status := wfs.truncateEntry(fullPath, truncatedEntry); status != fuse.OK {
|
||||
t.Fatalf("truncateEntry status = %v, want OK", status)
|
||||
}
|
||||
if fh.dirtyPages == oldDirtyPages {
|
||||
t.Fatal("truncateEntry should replace the dirtyPages writer for an open handle")
|
||||
}
|
||||
if got := fh.GetEntry().GetEntry().GetAttributes().GetFileSize(); got != 0 {
|
||||
t.Fatalf("file handle size = %d, want 0", got)
|
||||
}
|
||||
buf := make([]byte, 5)
|
||||
if maxStop := fh.dirtyPages.ReadDirtyDataAt(buf, 0, time.Now().UnixNano()); maxStop != 0 {
|
||||
t.Fatalf("dirty pages maxStop = %d, want 0 after truncate", maxStop)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessChecksPermissions(t *testing.T) {
|
||||
wfs := newCopyRangeTestWFS()
|
||||
oldLookupSupplementaryGroupIDs := lookupSupplementaryGroupIDs
|
||||
lookupSupplementaryGroupIDs = func(uint32) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
lookupSupplementaryGroupIDs = oldLookupSupplementaryGroupIDs
|
||||
})
|
||||
|
||||
fullPath := util.FullPath("/visible.txt")
|
||||
inode := wfs.inodeToPath.Lookup(fullPath, 1, false, false, 0, true)
|
||||
handle := wfs.fhMap.AcquireFileHandle(wfs, inode, &filer_pb.Entry{
|
||||
Name: "visible.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileMode: 0o640,
|
||||
Uid: 123,
|
||||
Gid: 456,
|
||||
Inode: inode,
|
||||
},
|
||||
})
|
||||
handle.RememberPath(fullPath)
|
||||
|
||||
if status := wfs.Access(make(chan struct{}), &fuse.AccessIn{
|
||||
InHeader: fuse.InHeader{
|
||||
NodeId: inode,
|
||||
Caller: fuse.Caller{
|
||||
Owner: fuse.Owner{
|
||||
Uid: 123,
|
||||
Gid: 999,
|
||||
},
|
||||
},
|
||||
},
|
||||
Mask: fuse.R_OK | fuse.W_OK,
|
||||
}); status != fuse.OK {
|
||||
t.Fatalf("owner Access status = %v, want OK", status)
|
||||
}
|
||||
|
||||
if status := wfs.Access(make(chan struct{}), &fuse.AccessIn{
|
||||
InHeader: fuse.InHeader{
|
||||
NodeId: inode,
|
||||
Caller: fuse.Caller{
|
||||
Owner: fuse.Owner{
|
||||
Uid: 999,
|
||||
Gid: 999,
|
||||
},
|
||||
},
|
||||
},
|
||||
Mask: fuse.W_OK,
|
||||
}); status != fuse.EACCES {
|
||||
t.Fatalf("other-user Access status = %v, want EACCES", status)
|
||||
}
|
||||
|
||||
if got := hasAccess(123, 999, 123, 456, 0o400, fuse.R_OK|fuse.W_OK); got {
|
||||
t.Fatal("owner should not get write access from a read-only owner mode")
|
||||
}
|
||||
|
||||
if got := hasAccess(999, 456, 123, 456, 0o040, fuse.R_OK|fuse.W_OK); got {
|
||||
t.Fatal("group member should not get write access from a read-only group mode")
|
||||
}
|
||||
|
||||
if got := hasAccess(999, 999, 123, 456, 0o004, fuse.R_OK|fuse.W_OK); got {
|
||||
t.Fatal("other users should not get write access from a read-only other mode")
|
||||
}
|
||||
|
||||
if got := hasAccess(0, 0, 123, 456, 0o644, fuse.X_OK); got {
|
||||
t.Fatal("root should not get execute access when no execute bit is set")
|
||||
}
|
||||
|
||||
if got := hasAccess(0, 0, 123, 456, 0o755, fuse.R_OK|fuse.X_OK); !got {
|
||||
t.Fatal("root should get execute access when at least one execute bit is set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasAccessUsesSupplementaryGroups(t *testing.T) {
|
||||
oldLookupSupplementaryGroupIDs := lookupSupplementaryGroupIDs
|
||||
lookupSupplementaryGroupIDs = func(uint32) ([]string, error) {
|
||||
return []string{"456"}, nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
lookupSupplementaryGroupIDs = oldLookupSupplementaryGroupIDs
|
||||
})
|
||||
|
||||
if got := hasAccess(999, 999, 123, 456, 0o060, fuse.R_OK|fuse.W_OK); !got {
|
||||
t.Fatal("supplementary group membership should grant matching group permissions")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateExistingFileIgnoresQuotaPreflight(t *testing.T) {
|
||||
wfs, _ := newCreateTestWFS(t)
|
||||
wfs.option.Quota = 1
|
||||
wfs.IsOverQuota = true
|
||||
|
||||
entry := &filer_pb.Entry{
|
||||
Name: "existing.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileMode: 0o644,
|
||||
FileSize: 7,
|
||||
Inode: 101,
|
||||
Crtime: 1,
|
||||
Mtime: 1,
|
||||
Uid: 123,
|
||||
Gid: 456,
|
||||
},
|
||||
}
|
||||
if err := wfs.metaCache.InsertEntry(context.Background(), filer.FromPbEntry("/", entry)); err != nil {
|
||||
t.Fatalf("InsertEntry: %v", err)
|
||||
}
|
||||
wfs.inodeToPath.Lookup(util.FullPath("/existing.txt"), entry.Attributes.Crtime, false, false, entry.Attributes.Inode, true)
|
||||
|
||||
out := &fuse.CreateOut{}
|
||||
status := wfs.Create(make(chan struct{}), &fuse.CreateIn{
|
||||
InHeader: fuse.InHeader{
|
||||
NodeId: 1,
|
||||
Caller: fuse.Caller{
|
||||
Owner: fuse.Owner{
|
||||
Uid: 123,
|
||||
Gid: 456,
|
||||
},
|
||||
},
|
||||
},
|
||||
Flags: syscall.O_WRONLY | syscall.O_CREAT | syscall.O_EXCL,
|
||||
Mode: 0o644,
|
||||
}, "existing.txt", out)
|
||||
if status != fuse.Status(syscall.EEXIST) {
|
||||
t.Fatalf("Create status = %v, want EEXIST", status)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -56,10 +57,20 @@ func (wfs *WFS) Flush(cancel <-chan struct{}, in *fuse.FlushIn) fuse.Status {
|
||||
if fh == nil {
|
||||
// If handle is not found, it might have been already released
|
||||
// This is not an error condition for FLUSH
|
||||
if in.LockOwner != 0 {
|
||||
wfs.posixLocks.ReleasePosixOwner(in.NodeId, in.LockOwner)
|
||||
}
|
||||
return fuse.OK
|
||||
}
|
||||
|
||||
return wfs.doFlush(fh, in.Uid, in.Gid, true)
|
||||
// When a closing lock owner is present, flush synchronously before waking any
|
||||
// blocked POSIX lock waiters so write-serialized callers cannot overtake each other.
|
||||
allowAsync := in.LockOwner == 0
|
||||
status := wfs.doFlush(fh, in.Uid, in.Gid, allowAsync)
|
||||
if in.LockOwner != 0 {
|
||||
wfs.posixLocks.ReleasePosixOwner(in.NodeId, in.LockOwner)
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,6 +108,7 @@ func (wfs *WFS) doFlush(fh *FileHandle, uid, gid uint32, allowAsync bool) fuse.S
|
||||
|
||||
// flush works at fh level
|
||||
fileFullPath := fh.FullPath()
|
||||
fh.RememberPath(fileFullPath)
|
||||
dir, name := fileFullPath.DirAndName()
|
||||
// send the data to the OS
|
||||
glog.V(4).Infof("doFlush %s fh %d", fileFullPath, fh.fh)
|
||||
@@ -112,8 +124,6 @@ func (wfs *WFS) doFlush(fh *FileHandle, uid, gid uint32, allowAsync bool) fuse.S
|
||||
fh.asyncFlushPending = true
|
||||
fh.asyncFlushUid = uid
|
||||
fh.asyncFlushGid = gid
|
||||
fh.asyncFlushDir = dir
|
||||
fh.asyncFlushName = name
|
||||
glog.V(3).Infof("doFlush async deferred %s fh %d", fileFullPath, fh.fh)
|
||||
return fuse.OK
|
||||
}
|
||||
@@ -138,9 +148,14 @@ func (wfs *WFS) doFlush(fh *FileHandle, uid, gid uint32, allowAsync bool) fuse.S
|
||||
return fuse.Status(syscall.ENOSPC)
|
||||
}
|
||||
|
||||
if err := wfs.flushMetadataToFiler(fh, dir, name, uid, gid); err != nil {
|
||||
if err := retryMetadataFlush(func() error {
|
||||
return wfs.flushMetadataToFiler(fh, dir, name, uid, gid)
|
||||
}, func(nextAttempt, totalAttempts int, backoff time.Duration, err error) {
|
||||
glog.Warningf("%v fh %d flush: retrying metadata flush (attempt %d/%d) after %v: %v",
|
||||
fileFullPath, fh.fh, nextAttempt, totalAttempts, backoff, err)
|
||||
}); err != nil {
|
||||
glog.Errorf("%v fh %d flush: %v", fileFullPath, fh.fh, err)
|
||||
return fuse.EIO
|
||||
return grpcErrorToFuseStatus(err)
|
||||
}
|
||||
|
||||
if IsDebugFileReadWrite {
|
||||
@@ -154,65 +169,65 @@ func (wfs *WFS) doFlush(fh *FileHandle, uid, gid uint32, allowAsync bool) fuse.S
|
||||
// This is shared between the synchronous doFlush path and the async flush completion.
|
||||
func (wfs *WFS) flushMetadataToFiler(fh *FileHandle, dir, name string, uid, gid uint32) error {
|
||||
fileFullPath := fh.FullPath()
|
||||
glog.V(4).Infof("flushMetadataToFiler %s/%s inode %d fh %d", dir, name, fh.inode, fh.fh)
|
||||
|
||||
fhActiveLock := fh.wfs.fhLockTable.AcquireLock("doFlush", fh.fh, util.ExclusiveLock)
|
||||
defer fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock)
|
||||
|
||||
err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
entry := fh.GetEntry()
|
||||
entry.Name = name // this flush may be just after a rename operation
|
||||
|
||||
entry := fh.GetEntry()
|
||||
entry.Name = name // this flush may be just after a rename operation
|
||||
|
||||
if entry.Attributes != nil {
|
||||
entry.Attributes.Mime = fh.contentType
|
||||
if entry.Attributes.Uid == 0 {
|
||||
entry.Attributes.Uid = uid
|
||||
}
|
||||
if entry.Attributes.Gid == 0 {
|
||||
entry.Attributes.Gid = gid
|
||||
}
|
||||
entry.Attributes.Mtime = time.Now().Unix()
|
||||
if entry.Attributes != nil {
|
||||
entry.Attributes.Mime = fh.contentType
|
||||
if entry.Attributes.Uid == 0 {
|
||||
entry.Attributes.Uid = uid
|
||||
}
|
||||
|
||||
request := &filer_pb.CreateEntryRequest{
|
||||
Directory: string(dir),
|
||||
Entry: entry.GetEntry(),
|
||||
Signatures: []int32{wfs.signature},
|
||||
SkipCheckParentDirectory: true,
|
||||
if entry.Attributes.Gid == 0 {
|
||||
entry.Attributes.Gid = gid
|
||||
}
|
||||
entry.Attributes.Mtime = time.Now().Unix()
|
||||
}
|
||||
|
||||
glog.V(4).Infof("%s set chunks: %v", fileFullPath, len(entry.GetChunks()))
|
||||
glog.V(4).Infof("%s set chunks: %v", fileFullPath, len(entry.GetChunks()))
|
||||
|
||||
manifestChunks, nonManifestChunks := filer.SeparateManifestChunks(entry.GetChunks())
|
||||
manifestChunks, nonManifestChunks := filer.SeparateManifestChunks(entry.GetChunks())
|
||||
|
||||
chunks, _ := filer.CompactFileChunks(context.Background(), wfs.LookupFn(), nonManifestChunks)
|
||||
chunks, manifestErr := filer.MaybeManifestize(wfs.saveDataAsChunk(fileFullPath), chunks)
|
||||
if manifestErr != nil {
|
||||
// not good, but should be ok
|
||||
glog.V(0).Infof("MaybeManifestize: %v", manifestErr)
|
||||
}
|
||||
entry.Chunks = append(chunks, manifestChunks...)
|
||||
chunks, _ := filer.CompactFileChunks(context.Background(), wfs.LookupFn(), nonManifestChunks)
|
||||
chunks, manifestErr := filer.MaybeManifestize(wfs.saveDataAsChunk(fileFullPath), chunks)
|
||||
if manifestErr != nil {
|
||||
// not good, but should be ok
|
||||
glog.V(0).Infof("MaybeManifestize: %v", manifestErr)
|
||||
}
|
||||
entry.Chunks = append(chunks, manifestChunks...)
|
||||
|
||||
wfs.mapPbIdFromLocalToFiler(request.Entry)
|
||||
defer wfs.mapPbIdFromFilerToLocal(request.Entry)
|
||||
// Clone the proto entry for the filer request so that mapPbIdFromLocalToFiler
|
||||
// does not mutate the file handle's live entry. Without the clone, a concurrent
|
||||
// Lookup can observe filer-side uid/gid on the file handle entry and return it
|
||||
// to the kernel, which caches it and then rejects opens by the local user.
|
||||
requestEntry := proto.Clone(entry.GetEntry()).(*filer_pb.Entry)
|
||||
request := &filer_pb.CreateEntryRequest{
|
||||
Directory: string(dir),
|
||||
Entry: requestEntry,
|
||||
Signatures: []int32{wfs.signature},
|
||||
SkipCheckParentDirectory: true,
|
||||
}
|
||||
|
||||
resp, err := filer_pb.CreateEntryWithResponse(context.Background(), client, request)
|
||||
if err != nil {
|
||||
glog.Errorf("fh flush create %s: %v", fileFullPath, err)
|
||||
return fmt.Errorf("fh flush create %s: %v", fileFullPath, err)
|
||||
}
|
||||
wfs.mapPbIdFromLocalToFiler(request.Entry)
|
||||
|
||||
event := resp.GetMetadataEvent()
|
||||
if event == nil {
|
||||
event = metadataUpdateEvent(string(dir), request.Entry)
|
||||
}
|
||||
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
|
||||
glog.Warningf("flush %s: best-effort metadata apply failed: %v", fileFullPath, applyErr)
|
||||
wfs.inodeToPath.InvalidateChildrenCache(util.FullPath(dir))
|
||||
}
|
||||
resp, err := wfs.streamCreateEntry(context.Background(), request)
|
||||
if err != nil {
|
||||
glog.Errorf("fh flush create %s: %v", fileFullPath, err)
|
||||
return fmt.Errorf("fh flush create %s: %v", fileFullPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
event := resp.GetMetadataEvent()
|
||||
if event == nil {
|
||||
event = metadataUpdateEvent(string(dir), request.Entry)
|
||||
}
|
||||
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
|
||||
glog.Warningf("flush %s: best-effort metadata apply failed: %v", fileFullPath, applyErr)
|
||||
wfs.inodeToPath.InvalidateChildrenCache(util.FullPath(dir))
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
fh.dirtyMetadata = false
|
||||
|
||||
@@ -21,8 +21,15 @@ func (wfs *WFS) AcquireHandle(inode uint64, flags, uid, gid uint32) (fileHandle
|
||||
if wormEnforced, _ := wfs.wormEnforcedForEntry(path, entry); wormEnforced && flags&fuse.O_ANYWRITE != 0 {
|
||||
return nil, fuse.EPERM
|
||||
}
|
||||
// Check unix permission bits for the requested access mode.
|
||||
if entry != nil && entry.Attributes != nil {
|
||||
if mask := openFlagsToAccessMask(flags); mask != 0 && !hasAccess(uid, gid, entry.Attributes.Uid, entry.Attributes.Gid, entry.Attributes.FileMode, mask) {
|
||||
return nil, fuse.EACCES
|
||||
}
|
||||
}
|
||||
// need to AcquireFileHandle again to ensure correct handle counter
|
||||
fileHandle = wfs.fhMap.AcquireFileHandle(wfs, inode, entry)
|
||||
fileHandle.RememberPath(path)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -43,22 +50,14 @@ func (wfs *WFS) ReleaseHandle(handleId FileHandleId) {
|
||||
if fhToRelease != nil && fhToRelease.asyncFlushPending {
|
||||
done := make(chan struct{})
|
||||
wfs.pendingAsyncFlush[fhToRelease.inode] = done
|
||||
// Add(1) while holding the mutex so WaitForAsyncFlush cannot
|
||||
// observe a zero counter and close the channel before we send.
|
||||
wfs.asyncFlushWg.Add(1)
|
||||
wfs.pendingAsyncFlushMu.Unlock()
|
||||
|
||||
wfs.asyncFlushWg.Add(1)
|
||||
go func() {
|
||||
defer wfs.asyncFlushWg.Done()
|
||||
defer func() {
|
||||
// Remove from fhMap first (so AcquireFileHandle creates a fresh handle).
|
||||
wfs.fhMap.RemoveFileHandle(fhToRelease.fh, fhToRelease.inode)
|
||||
// Then signal completion (unblocks waitForPendingAsyncFlush).
|
||||
close(done)
|
||||
wfs.pendingAsyncFlushMu.Lock()
|
||||
delete(wfs.pendingAsyncFlush, fhToRelease.inode)
|
||||
wfs.pendingAsyncFlushMu.Unlock()
|
||||
}()
|
||||
wfs.completeAsyncFlush(fhToRelease)
|
||||
}()
|
||||
// Send after unlock to avoid deadlock — workers acquire the
|
||||
// same mutex during cleanup.
|
||||
wfs.asyncFlushCh <- &asyncFlushItem{fh: fhToRelease, done: done}
|
||||
return
|
||||
}
|
||||
wfs.pendingAsyncFlushMu.Unlock()
|
||||
|
||||
+26
-25
@@ -60,8 +60,11 @@ func (wfs *WFS) Link(cancel <-chan struct{}, in *fuse.LinkIn, name string, out *
|
||||
if len(oldEntry.HardLinkId) == 0 {
|
||||
oldEntry.HardLinkId = filer.NewHardLinkId()
|
||||
oldEntry.HardLinkCounter = 1
|
||||
glog.V(4).Infof("Link: new HardLinkId %x for %s", oldEntry.HardLinkId, oldEntryPath)
|
||||
}
|
||||
oldEntry.HardLinkCounter++
|
||||
glog.V(4).Infof("Link: %s -> %s/%s HardLinkId %x counter=%d",
|
||||
oldEntryPath, newParentPath, name, oldEntry.HardLinkId, oldEntry.HardLinkCounter)
|
||||
updateOldEntryRequest := &filer_pb.UpdateEntryRequest{
|
||||
Directory: oldParentPath,
|
||||
Entry: oldEntry,
|
||||
@@ -86,25 +89,23 @@ func (wfs *WFS) Link(cancel <-chan struct{}, in *fuse.LinkIn, name string, out *
|
||||
}
|
||||
|
||||
// apply changes to the filer, and also apply to local metaCache
|
||||
err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
wfs.mapPbIdFromLocalToFiler(request.Entry)
|
||||
|
||||
wfs.mapPbIdFromLocalToFiler(request.Entry)
|
||||
defer wfs.mapPbIdFromFilerToLocal(request.Entry)
|
||||
|
||||
updateResp, err := filer_pb.UpdateEntryWithResponse(context.Background(), client, updateOldEntryRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
updateResp, err := wfs.streamUpdateEntry(ctx, updateOldEntryRequest)
|
||||
if err == nil {
|
||||
updateEvent := updateResp.GetMetadataEvent()
|
||||
if updateEvent == nil {
|
||||
updateEvent = metadataUpdateEvent(oldParentPath, updateOldEntryRequest.Entry)
|
||||
}
|
||||
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), updateEvent); applyErr != nil {
|
||||
if applyErr := wfs.applyLocalMetadataEvent(ctx, updateEvent); applyErr != nil {
|
||||
glog.Warningf("link %s: best-effort metadata apply failed: %v", oldEntryPath, applyErr)
|
||||
wfs.inodeToPath.InvalidateChildrenCache(util.FullPath(oldParentPath))
|
||||
}
|
||||
|
||||
createResp, err := filer_pb.CreateEntryWithResponse(context.Background(), client, request)
|
||||
}
|
||||
if err == nil {
|
||||
var createResp *filer_pb.CreateEntryResponse
|
||||
createResp, err = wfs.streamCreateEntry(ctx, request)
|
||||
if err != nil {
|
||||
// Rollback: restore original HardLinkId/Counter on the source entry
|
||||
oldEntry.HardLinkId = origHardLinkId
|
||||
@@ -114,26 +115,26 @@ func (wfs *WFS) Link(cancel <-chan struct{}, in *fuse.LinkIn, name string, out *
|
||||
Entry: oldEntry,
|
||||
Signatures: []int32{wfs.signature},
|
||||
}
|
||||
if _, rollbackErr := filer_pb.UpdateEntryWithResponse(context.Background(), client, rollbackReq); rollbackErr != nil {
|
||||
if _, rollbackErr := wfs.streamUpdateEntry(ctx, rollbackReq); rollbackErr != nil {
|
||||
glog.Warningf("link rollback %s: %v", oldEntryPath, rollbackErr)
|
||||
}
|
||||
return err
|
||||
} else {
|
||||
createEvent := createResp.GetMetadataEvent()
|
||||
if createEvent == nil {
|
||||
createEvent = metadataCreateEvent(string(newParentPath), request.Entry)
|
||||
}
|
||||
if applyErr := wfs.applyLocalMetadataEvent(ctx, createEvent); applyErr != nil {
|
||||
glog.Warningf("link %s: best-effort metadata apply failed: %v", newParentPath.Child(name), applyErr)
|
||||
wfs.inodeToPath.InvalidateChildrenCache(newParentPath)
|
||||
}
|
||||
}
|
||||
|
||||
createEvent := createResp.GetMetadataEvent()
|
||||
if createEvent == nil {
|
||||
createEvent = metadataCreateEvent(string(newParentPath), request.Entry)
|
||||
}
|
||||
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), createEvent); applyErr != nil {
|
||||
glog.Warningf("link %s: best-effort metadata apply failed: %v", newParentPath.Child(name), applyErr)
|
||||
wfs.inodeToPath.InvalidateChildrenCache(newParentPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
newEntryPath := newParentPath.Child(name)
|
||||
|
||||
// Map back to local uid/gid before writing attributes to the kernel.
|
||||
wfs.mapPbIdFromFilerToLocal(request.Entry)
|
||||
|
||||
if err != nil {
|
||||
glog.V(0).Infof("Link %v -> %s: %v", oldEntryPath, newEntryPath, err)
|
||||
return fuse.EIO
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// loopFlushDirtyMetadata periodically flushes dirty file metadata to the filer.
|
||||
@@ -99,70 +100,65 @@ func (wfs *WFS) flushFileMetadata(fh *FileHandle) error {
|
||||
|
||||
glog.V(4).Infof("flushFileMetadata %s fh %d", fileFullPath, fh.fh)
|
||||
|
||||
err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
entry := fh.GetEntry()
|
||||
if entry == nil {
|
||||
return nil
|
||||
}
|
||||
entry.Name = name
|
||||
|
||||
if entry.Attributes != nil {
|
||||
entry.Attributes.Mtime = time.Now().Unix()
|
||||
}
|
||||
|
||||
// Get current chunks - these include chunks that have been uploaded
|
||||
// but not yet persisted to filer metadata
|
||||
chunks := entry.GetChunks()
|
||||
if len(chunks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Separate manifest and non-manifest chunks
|
||||
manifestChunks, nonManifestChunks := filer.SeparateManifestChunks(chunks)
|
||||
|
||||
// Compact chunks to remove fully overlapped ones
|
||||
compactedChunks, _ := filer.CompactFileChunks(context.Background(), wfs.LookupFn(), nonManifestChunks)
|
||||
|
||||
// Try to create manifest chunks for large files
|
||||
compactedChunks, manifestErr := filer.MaybeManifestize(wfs.saveDataAsChunk(fileFullPath), compactedChunks)
|
||||
if manifestErr != nil {
|
||||
glog.V(0).Infof("flushFileMetadata MaybeManifestize: %v", manifestErr)
|
||||
}
|
||||
|
||||
entry.Chunks = append(compactedChunks, manifestChunks...)
|
||||
|
||||
request := &filer_pb.CreateEntryRequest{
|
||||
Directory: string(dir),
|
||||
Entry: entry.GetEntry(),
|
||||
Signatures: []int32{wfs.signature},
|
||||
SkipCheckParentDirectory: true,
|
||||
}
|
||||
|
||||
wfs.mapPbIdFromLocalToFiler(request.Entry)
|
||||
defer wfs.mapPbIdFromFilerToLocal(request.Entry)
|
||||
|
||||
resp, err := filer_pb.CreateEntryWithResponse(context.Background(), client, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
event := resp.GetMetadataEvent()
|
||||
if event == nil {
|
||||
event = metadataUpdateEvent(string(dir), request.Entry)
|
||||
}
|
||||
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
|
||||
glog.Warningf("flushFileMetadata %s: best-effort metadata apply failed: %v", fileFullPath, applyErr)
|
||||
wfs.inodeToPath.InvalidateChildrenCache(util.FullPath(dir))
|
||||
}
|
||||
|
||||
glog.V(3).Infof("flushed metadata for %s with %d chunks", fileFullPath, len(entry.GetChunks()))
|
||||
entry := fh.GetEntry()
|
||||
if entry == nil {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
entry.Name = name
|
||||
|
||||
if entry.Attributes != nil {
|
||||
entry.Attributes.Mtime = time.Now().Unix()
|
||||
}
|
||||
|
||||
// Get current chunks - these include chunks that have been uploaded
|
||||
// but not yet persisted to filer metadata
|
||||
chunks := entry.GetChunks()
|
||||
if len(chunks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Separate manifest and non-manifest chunks
|
||||
manifestChunks, nonManifestChunks := filer.SeparateManifestChunks(chunks)
|
||||
|
||||
// Compact chunks to remove fully overlapped ones
|
||||
compactedChunks, _ := filer.CompactFileChunks(context.Background(), wfs.LookupFn(), nonManifestChunks)
|
||||
|
||||
// Try to create manifest chunks for large files
|
||||
compactedChunks, manifestErr := filer.MaybeManifestize(wfs.saveDataAsChunk(fileFullPath), compactedChunks)
|
||||
if manifestErr != nil {
|
||||
glog.V(0).Infof("flushFileMetadata MaybeManifestize: %v", manifestErr)
|
||||
}
|
||||
|
||||
entry.Chunks = append(compactedChunks, manifestChunks...)
|
||||
|
||||
// Clone the proto entry so mapPbIdFromLocalToFiler does not mutate the
|
||||
// file handle's live entry (same race as in flushMetadataToFiler).
|
||||
requestEntry := proto.Clone(entry.GetEntry()).(*filer_pb.Entry)
|
||||
request := &filer_pb.CreateEntryRequest{
|
||||
Directory: string(dir),
|
||||
Entry: requestEntry,
|
||||
Signatures: []int32{wfs.signature},
|
||||
SkipCheckParentDirectory: true,
|
||||
}
|
||||
|
||||
wfs.mapPbIdFromLocalToFiler(request.Entry)
|
||||
|
||||
resp, err := wfs.streamCreateEntry(context.Background(), request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
event := resp.GetMetadataEvent()
|
||||
if event == nil {
|
||||
event = metadataUpdateEvent(string(dir), request.Entry)
|
||||
}
|
||||
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
|
||||
glog.Warningf("flushFileMetadata %s: best-effort metadata apply failed: %v", fileFullPath, applyErr)
|
||||
wfs.inodeToPath.InvalidateChildrenCache(util.FullPath(dir))
|
||||
}
|
||||
|
||||
glog.V(3).Infof("flushed metadata for %s with %d chunks", fileFullPath, len(entry.GetChunks()))
|
||||
|
||||
// Note: We do NOT clear dirtyMetadata here because:
|
||||
// 1. There may still be dirty pages in the write buffer
|
||||
// 2. The file may receive more writes before close
|
||||
|
||||
+86
-49
@@ -2,6 +2,7 @@ package mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
@@ -14,6 +15,38 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
// doRename tries the streaming mux first, falling back to unary on transport errors.
|
||||
func (wfs *WFS) doRename(ctx context.Context, request *filer_pb.StreamRenameEntryRequest, oldPath, newPath util.FullPath) error {
|
||||
if wfs.streamMutate != nil && wfs.streamMutate.IsAvailable() {
|
||||
err := wfs.streamMutate.Rename(ctx, request, func(resp *filer_pb.StreamRenameEntryResponse) error {
|
||||
return wfs.handleRenameResponse(ctx, resp)
|
||||
})
|
||||
if err == nil || !errors.Is(err, ErrStreamTransport) {
|
||||
return err // success or application error
|
||||
}
|
||||
glog.V(1).Infof("Rename %s => %s: stream failed, falling back to unary: %v", oldPath, newPath, err)
|
||||
}
|
||||
return wfs.WithFilerClient(true, func(client filer_pb.SeaweedFilerClient) error {
|
||||
stream, streamErr := client.StreamRenameEntry(ctx, request)
|
||||
if streamErr != nil {
|
||||
return fmt.Errorf("dir AtomicRenameEntry %s => %s : %v", oldPath, newPath, streamErr)
|
||||
}
|
||||
for {
|
||||
resp, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
if recvErr == io.EOF {
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("dir Rename %s => %s receive: %v", oldPath, newPath, recvErr)
|
||||
}
|
||||
if err := wfs.handleRenameResponse(ctx, resp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
/** Rename a file
|
||||
*
|
||||
* If the target exists it should be atomically replaced. If
|
||||
@@ -171,53 +204,60 @@ func (wfs *WFS) Rename(cancel <-chan struct{}, in *fuse.RenameIn, oldName string
|
||||
|
||||
glog.V(4).Infof("dir Rename %s => %s", oldPath, newPath)
|
||||
|
||||
// Ensure the source file's metadata exists on the filer before renaming.
|
||||
// Two cases can leave the entry only in the local cache:
|
||||
// 1. deferFilerCreate=true — file handle still open, dirtyMetadata set.
|
||||
// 2. writebackCache — close() triggered async flush, handle released.
|
||||
// The filer rename will fail with ENOENT unless we flush/wait first.
|
||||
if inode, found := wfs.inodeToPath.GetInode(oldPath); found {
|
||||
// Case 1: handle still open with deferred metadata — flush synchronously
|
||||
// BEFORE any async flush interference.
|
||||
if fh, ok := wfs.fhMap.FindFileHandle(inode); ok && fh.dirtyMetadata {
|
||||
glog.V(4).Infof("dir Rename %s: flushing deferred metadata before rename", oldPath)
|
||||
if flushStatus := wfs.doFlush(fh, oldEntry.Attributes.Uid, oldEntry.Attributes.Gid, false); flushStatus != fuse.OK {
|
||||
glog.Warningf("dir Rename %s: flush before rename failed: %v", oldPath, flushStatus)
|
||||
return flushStatus
|
||||
}
|
||||
}
|
||||
// Case 2: handle already released, async flush may be in flight.
|
||||
// Mark ALL handles for this inode as renamed so the async flush
|
||||
// skips old-path metadata creation (which would re-insert the
|
||||
// renamed entry into the meta cache after rename events clean it up).
|
||||
wfs.fhMap.MarkInodeRenamed(inode)
|
||||
wfs.waitForPendingAsyncFlush(inode)
|
||||
} else if oldEntry != nil && oldEntry.Attributes != nil && oldEntry.Attributes.Inode != 0 {
|
||||
// GetInode failed (Forget already removed the mapping), but the
|
||||
// entry's stored inode can still identify pending async flushes.
|
||||
inode = oldEntry.Attributes.Inode
|
||||
wfs.fhMap.MarkInodeRenamed(inode)
|
||||
wfs.waitForPendingAsyncFlush(inode)
|
||||
}
|
||||
|
||||
// update remote filer
|
||||
err := wfs.WithFilerClient(true, func(client filer_pb.SeaweedFilerClient) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
request := &filer_pb.StreamRenameEntryRequest{
|
||||
OldDirectory: string(oldDir),
|
||||
OldName: oldName,
|
||||
NewDirectory: string(newDir),
|
||||
NewName: newName,
|
||||
Signatures: []int32{wfs.signature},
|
||||
}
|
||||
|
||||
request := &filer_pb.StreamRenameEntryRequest{
|
||||
OldDirectory: string(oldDir),
|
||||
OldName: oldName,
|
||||
NewDirectory: string(newDir),
|
||||
NewName: newName,
|
||||
Signatures: []int32{wfs.signature},
|
||||
}
|
||||
|
||||
stream, err := client.StreamRenameEntry(ctx, request)
|
||||
if err != nil {
|
||||
code = fuse.EIO
|
||||
return fmt.Errorf("dir AtomicRenameEntry %s => %s : %v", oldPath, newPath, err)
|
||||
}
|
||||
|
||||
for {
|
||||
resp, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
if recvErr == io.EOF {
|
||||
break
|
||||
} else {
|
||||
if strings.Contains(recvErr.Error(), "not empty") {
|
||||
code = fuse.Status(syscall.ENOTEMPTY)
|
||||
} else if strings.Contains(recvErr.Error(), "not directory") {
|
||||
code = fuse.ENOTDIR
|
||||
}
|
||||
return fmt.Errorf("dir Rename %s => %s receive: %v", oldPath, newPath, recvErr)
|
||||
}
|
||||
}
|
||||
|
||||
if err = wfs.handleRenameResponse(ctx, resp); err != nil {
|
||||
glog.V(0).Infof("dir Rename %s => %s : %v", oldPath, newPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
})
|
||||
ctx := context.Background()
|
||||
err := wfs.doRename(ctx, request, oldPath, newPath)
|
||||
if err != nil {
|
||||
glog.V(0).Infof("Link: %v", err)
|
||||
return
|
||||
glog.V(0).Infof("Rename %s => %s: %v", oldPath, newPath, err)
|
||||
// Map error strings to FUSE status codes. String matching is used
|
||||
// instead of raw errno to stay portable across platforms (errno
|
||||
// numeric values differ between Linux and macOS).
|
||||
msg := err.Error()
|
||||
if strings.Contains(msg, "not found") {
|
||||
return fuse.Status(syscall.ENOENT)
|
||||
} else if strings.Contains(msg, "not empty") {
|
||||
return fuse.Status(syscall.ENOTEMPTY)
|
||||
} else if strings.Contains(msg, "not directory") {
|
||||
return fuse.ENOTDIR
|
||||
}
|
||||
return fuse.EIO
|
||||
}
|
||||
wfs.inodeToPath.TouchDirectory(oldDir)
|
||||
wfs.inodeToPath.TouchDirectory(newDir)
|
||||
@@ -253,12 +293,9 @@ func (wfs *WFS) handleRenameResponse(ctx context.Context, resp *filer_pb.StreamR
|
||||
if entry := fh.GetEntry(); entry != nil {
|
||||
entry.Name = newName
|
||||
}
|
||||
// Keep the saved async-flush path current so the fallback
|
||||
// Keep the saved handle path current so any flush fallback
|
||||
// after Forget uses the post-rename location, not the old one.
|
||||
if fh.asyncFlushPending {
|
||||
fh.asyncFlushDir = string(newParent)
|
||||
fh.asyncFlushName = newName
|
||||
}
|
||||
fh.RememberPath(newPath)
|
||||
}
|
||||
// invalidate attr and data
|
||||
// wfs.fuseServer.InodeNotify(sourceInode, 0, -1)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
)
|
||||
|
||||
// streamCreateEntry routes a CreateEntryRequest through the streaming mux
|
||||
// if available, falling back to a unary gRPC call on transport errors.
|
||||
func (wfs *WFS) streamCreateEntry(ctx context.Context, req *filer_pb.CreateEntryRequest) (*filer_pb.CreateEntryResponse, error) {
|
||||
if wfs.streamMutate != nil && wfs.streamMutate.IsAvailable() {
|
||||
resp, err := wfs.streamMutate.CreateEntry(ctx, req)
|
||||
if err == nil || !errors.Is(err, ErrStreamTransport) {
|
||||
return resp, err // success or application error — don't retry
|
||||
}
|
||||
glog.V(1).Infof("streamCreateEntry %s/%s: stream failed, falling back to unary: %v", req.Directory, req.Entry.Name, err)
|
||||
}
|
||||
var resp *filer_pb.CreateEntryResponse
|
||||
err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
var err error
|
||||
resp, err = filer_pb.CreateEntryWithResponse(ctx, client, req)
|
||||
return err
|
||||
})
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// streamUpdateEntry routes an UpdateEntryRequest through the streaming mux
|
||||
// if available, falling back to a unary gRPC call on transport errors.
|
||||
func (wfs *WFS) streamUpdateEntry(ctx context.Context, req *filer_pb.UpdateEntryRequest) (*filer_pb.UpdateEntryResponse, error) {
|
||||
if wfs.streamMutate != nil && wfs.streamMutate.IsAvailable() {
|
||||
resp, err := wfs.streamMutate.UpdateEntry(ctx, req)
|
||||
if err == nil || !errors.Is(err, ErrStreamTransport) {
|
||||
return resp, err
|
||||
}
|
||||
glog.V(1).Infof("streamUpdateEntry %s/%s: stream failed, falling back to unary: %v", req.Directory, req.Entry.Name, err)
|
||||
}
|
||||
var resp *filer_pb.UpdateEntryResponse
|
||||
err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
var err error
|
||||
resp, err = client.UpdateEntry(ctx, req)
|
||||
return err
|
||||
})
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// streamDeleteEntry routes a DeleteEntryRequest through the streaming mux
|
||||
// if available, falling back to a unary gRPC call on transport errors.
|
||||
func (wfs *WFS) streamDeleteEntry(ctx context.Context, req *filer_pb.DeleteEntryRequest) (*filer_pb.DeleteEntryResponse, error) {
|
||||
if wfs.streamMutate != nil && wfs.streamMutate.IsAvailable() {
|
||||
resp, err := wfs.streamMutate.DeleteEntry(ctx, req)
|
||||
if err == nil || !errors.Is(err, ErrStreamTransport) {
|
||||
return resp, err
|
||||
}
|
||||
glog.V(1).Infof("streamDeleteEntry %s/%s: stream failed, falling back to unary: %v", req.Directory, req.Name, err)
|
||||
}
|
||||
var resp *filer_pb.DeleteEntryResponse
|
||||
err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
var err error
|
||||
resp, err = client.DeleteEntry(ctx, req)
|
||||
return err
|
||||
})
|
||||
return resp, err
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
grpcMetadata "google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// streamMutateError is returned when the server reports a structured errno.
|
||||
// It is also used by helpers to distinguish application errors (don't retry
|
||||
// on unary fallback) from transport errors (do retry).
|
||||
type streamMutateError struct {
|
||||
msg string
|
||||
errno syscall.Errno
|
||||
}
|
||||
|
||||
func (e *streamMutateError) Error() string { return e.msg }
|
||||
func (e *streamMutateError) Errno() syscall.Errno { return e.errno }
|
||||
|
||||
// ErrStreamTransport is a sentinel error type for transport-level stream
|
||||
// failures (disconnects, send errors). Callers use errors.Is to decide
|
||||
// whether to fall back to unary RPCs.
|
||||
var ErrStreamTransport = errors.New("stream transport error")
|
||||
|
||||
// streamMutateMux multiplexes filer mutation RPCs (create, update, delete,
|
||||
// rename) over a single bidirectional gRPC stream. Multiple goroutines can
|
||||
// call the mutation methods concurrently; requests are serialized through
|
||||
// sendCh and responses are dispatched back via per-request channels.
|
||||
type streamMutateMux struct {
|
||||
wfs *WFS
|
||||
|
||||
mu sync.Mutex // protects stream, cancel, grpcConn, closed, stopSend, generation
|
||||
stream filer_pb.SeaweedFiler_StreamMutateEntryClient
|
||||
cancel context.CancelFunc
|
||||
grpcConn *grpc.ClientConn // dedicated connection, closed on stream teardown
|
||||
closed bool
|
||||
disabled bool // permanently disabled if filer doesn't support the RPC
|
||||
stopSend chan struct{} // closed to signal the current sendLoop to exit
|
||||
generation uint64 // incremented each time a new stream is created
|
||||
|
||||
nextID atomic.Uint64
|
||||
|
||||
// pending maps request_id → response channel. The recvLoop dispatches
|
||||
// each response to the correct waiter. For rename (multi-response),
|
||||
// the channel receives multiple messages until is_last=true.
|
||||
pending sync.Map // map[uint64]chan *filer_pb.StreamMutateEntryResponse
|
||||
|
||||
sendCh chan *streamMutateReq
|
||||
recvDone chan struct{} // closed when recvLoop exits
|
||||
}
|
||||
|
||||
type streamMutateReq struct {
|
||||
req *filer_pb.StreamMutateEntryRequest
|
||||
errCh chan error // send error feedback
|
||||
gen uint64 // stream generation this request targets
|
||||
}
|
||||
|
||||
func newStreamMutateMux(wfs *WFS) *streamMutateMux {
|
||||
return &streamMutateMux{
|
||||
wfs: wfs,
|
||||
sendCh: make(chan *streamMutateReq, 512),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateEntry sends a CreateEntryRequest over the stream and waits for the response.
|
||||
func (m *streamMutateMux) CreateEntry(ctx context.Context, req *filer_pb.CreateEntryRequest) (*filer_pb.CreateEntryResponse, error) {
|
||||
resp, err := m.doUnary(ctx, &filer_pb.StreamMutateEntryRequest{
|
||||
Request: &filer_pb.StreamMutateEntryRequest_CreateRequest{CreateRequest: req},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, ok := resp.Response.(*filer_pb.StreamMutateEntryResponse_CreateResponse)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected response type %T", resp.Response)
|
||||
}
|
||||
// Check nested error fields (same logic as CreateEntryWithResponse).
|
||||
cr := r.CreateResponse
|
||||
if cr.ErrorCode != filer_pb.FilerError_OK {
|
||||
if sentinel := filer_pb.FilerErrorToSentinel(cr.ErrorCode); sentinel != nil {
|
||||
return nil, fmt.Errorf("CreateEntry %s/%s: %w", req.Directory, req.Entry.Name, sentinel)
|
||||
}
|
||||
return nil, &streamMutateError{msg: cr.Error, errno: syscall.EIO}
|
||||
}
|
||||
if cr.Error != "" {
|
||||
return nil, &streamMutateError{msg: cr.Error, errno: syscall.EIO}
|
||||
}
|
||||
return cr, nil
|
||||
}
|
||||
|
||||
// UpdateEntry sends an UpdateEntryRequest over the stream and waits for the response.
|
||||
func (m *streamMutateMux) UpdateEntry(ctx context.Context, req *filer_pb.UpdateEntryRequest) (*filer_pb.UpdateEntryResponse, error) {
|
||||
resp, err := m.doUnary(ctx, &filer_pb.StreamMutateEntryRequest{
|
||||
Request: &filer_pb.StreamMutateEntryRequest_UpdateRequest{UpdateRequest: req},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r, ok := resp.Response.(*filer_pb.StreamMutateEntryResponse_UpdateResponse); ok {
|
||||
return r.UpdateResponse, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected response type %T", resp.Response)
|
||||
}
|
||||
|
||||
// DeleteEntry sends a DeleteEntryRequest over the stream and waits for the response.
|
||||
func (m *streamMutateMux) DeleteEntry(ctx context.Context, req *filer_pb.DeleteEntryRequest) (*filer_pb.DeleteEntryResponse, error) {
|
||||
resp, err := m.doUnary(ctx, &filer_pb.StreamMutateEntryRequest{
|
||||
Request: &filer_pb.StreamMutateEntryRequest_DeleteRequest{DeleteRequest: req},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, ok := resp.Response.(*filer_pb.StreamMutateEntryResponse_DeleteResponse)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected response type %T", resp.Response)
|
||||
}
|
||||
// Check nested error field.
|
||||
if r.DeleteResponse.Error != "" {
|
||||
return nil, &streamMutateError{msg: r.DeleteResponse.Error, errno: syscall.EIO}
|
||||
}
|
||||
return r.DeleteResponse, nil
|
||||
}
|
||||
|
||||
// Rename sends a StreamRenameEntryRequest over the stream and collects all
|
||||
// response events until is_last=true. The callback is invoked for each
|
||||
// intermediate rename event (same as the current StreamRenameEntry recv loop).
|
||||
func (m *streamMutateMux) Rename(ctx context.Context, req *filer_pb.StreamRenameEntryRequest, onEvent func(*filer_pb.StreamRenameEntryResponse) error) error {
|
||||
gen, err := m.ensureStream()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrStreamTransport, err)
|
||||
}
|
||||
|
||||
id := m.nextID.Add(1)
|
||||
ch := make(chan *filer_pb.StreamMutateEntryResponse, 64)
|
||||
m.pending.Store(id, ch)
|
||||
defer m.pending.Delete(id)
|
||||
|
||||
sendReq := &streamMutateReq{
|
||||
req: &filer_pb.StreamMutateEntryRequest{
|
||||
RequestId: id,
|
||||
Request: &filer_pb.StreamMutateEntryRequest_RenameRequest{RenameRequest: req},
|
||||
},
|
||||
errCh: make(chan error, 1),
|
||||
gen: gen,
|
||||
}
|
||||
select {
|
||||
case m.sendCh <- sendReq:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
select {
|
||||
case err := <-sendReq.errCh:
|
||||
if err != nil {
|
||||
return fmt.Errorf("rename send: %w: %v", ErrStreamTransport, err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// Collect rename events until is_last=true.
|
||||
for {
|
||||
select {
|
||||
case resp, ok := <-ch:
|
||||
if !ok {
|
||||
return fmt.Errorf("rename recv: %w: stream closed", ErrStreamTransport)
|
||||
}
|
||||
if r, ok := resp.Response.(*filer_pb.StreamMutateEntryResponse_RenameResponse); ok {
|
||||
if r.RenameResponse != nil && r.RenameResponse.EventNotification != nil {
|
||||
if err := onEvent(r.RenameResponse); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if resp.IsLast {
|
||||
if resp.Error != "" {
|
||||
return &streamMutateError{
|
||||
msg: resp.Error,
|
||||
errno: syscall.Errno(resp.Errno),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doUnary sends a single-response request and waits for the reply.
|
||||
func (m *streamMutateMux) doUnary(ctx context.Context, req *filer_pb.StreamMutateEntryRequest) (*filer_pb.StreamMutateEntryResponse, error) {
|
||||
gen, err := m.ensureStream()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrStreamTransport, err)
|
||||
}
|
||||
|
||||
id := m.nextID.Add(1)
|
||||
req.RequestId = id
|
||||
ch := make(chan *filer_pb.StreamMutateEntryResponse, 1)
|
||||
m.pending.Store(id, ch)
|
||||
defer m.pending.Delete(id)
|
||||
|
||||
sendReq := &streamMutateReq{
|
||||
req: req,
|
||||
errCh: make(chan error, 1),
|
||||
gen: gen,
|
||||
}
|
||||
select {
|
||||
case m.sendCh <- sendReq:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
select {
|
||||
case err := <-sendReq.errCh:
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrStreamTransport, err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
select {
|
||||
case resp, ok := <-ch:
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: stream closed", ErrStreamTransport)
|
||||
}
|
||||
if resp.Error != "" {
|
||||
return nil, &streamMutateError{
|
||||
msg: resp.Error,
|
||||
errno: syscall.Errno(resp.Errno),
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// ensureStream opens the bidi stream if not already open. It returns the
|
||||
// stream generation so callers can tag outgoing requests.
|
||||
func (m *streamMutateMux) ensureStream() (uint64, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.closed {
|
||||
return 0, fmt.Errorf("stream mux is closed")
|
||||
}
|
||||
if m.disabled {
|
||||
return 0, fmt.Errorf("StreamMutateEntry not supported by filer")
|
||||
}
|
||||
if m.stream != nil {
|
||||
return m.generation, nil
|
||||
}
|
||||
|
||||
// Wait for prior generation's recvLoop to fully tear down before opening
|
||||
// a new stream. This guarantees all pending waiters from the old stream
|
||||
// have been failed before we create a new generation.
|
||||
if m.recvDone != nil {
|
||||
done := m.recvDone
|
||||
m.mu.Unlock()
|
||||
<-done
|
||||
m.mu.Lock()
|
||||
// Re-check after reacquiring the lock.
|
||||
if m.closed {
|
||||
return 0, fmt.Errorf("stream mux is closed")
|
||||
}
|
||||
if m.disabled {
|
||||
return 0, fmt.Errorf("StreamMutateEntry not supported by filer")
|
||||
}
|
||||
if m.stream != nil {
|
||||
return m.generation, nil
|
||||
}
|
||||
}
|
||||
|
||||
var stream filer_pb.SeaweedFiler_StreamMutateEntryClient
|
||||
err := m.openStream(&stream)
|
||||
if err != nil {
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.Unimplemented {
|
||||
m.disabled = true
|
||||
glog.V(0).Infof("filer does not support StreamMutateEntry, falling back to unary RPCs")
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
m.generation++
|
||||
m.stream = stream
|
||||
m.stopSend = make(chan struct{})
|
||||
recvDone := make(chan struct{})
|
||||
m.recvDone = recvDone
|
||||
gen := m.generation
|
||||
go m.sendLoop(stream, m.stopSend, gen)
|
||||
go m.recvLoop(stream, gen, recvDone)
|
||||
return gen, nil
|
||||
}
|
||||
|
||||
func (m *streamMutateMux) openStream(out *filer_pb.SeaweedFiler_StreamMutateEntryClient) error {
|
||||
i := atomic.LoadInt32(&m.wfs.option.filerIndex)
|
||||
n := int32(len(m.wfs.option.FilerAddresses))
|
||||
var lastErr error
|
||||
|
||||
for x := int32(0); x < n; x++ {
|
||||
idx := (i + x) % n
|
||||
filerGrpcAddress := m.wfs.option.FilerAddresses[idx].ToGrpcAddress()
|
||||
|
||||
ctx := context.Background()
|
||||
if m.wfs.signature != 0 {
|
||||
ctx = grpcMetadata.AppendToOutgoingContext(ctx, "sw-client-id", fmt.Sprintf("%d", m.wfs.signature))
|
||||
}
|
||||
grpcConn, err := pb.GrpcDial(ctx, filerGrpcAddress, false, m.wfs.option.GrpcDialOption)
|
||||
if err != nil {
|
||||
lastErr = fmt.Errorf("stream dial %s: %v", filerGrpcAddress, err)
|
||||
continue
|
||||
}
|
||||
|
||||
client := filer_pb.NewSeaweedFilerClient(grpcConn)
|
||||
streamCtx, cancel := context.WithCancel(ctx)
|
||||
stream, err := client.StreamMutateEntry(streamCtx)
|
||||
if err != nil {
|
||||
cancel()
|
||||
grpcConn.Close()
|
||||
lastErr = err
|
||||
// Unimplemented means all filers lack it — stop rotating.
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.Unimplemented {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
atomic.StoreInt32(&m.wfs.option.filerIndex, idx)
|
||||
m.cancel = cancel
|
||||
m.grpcConn = grpcConn
|
||||
*out = stream
|
||||
return nil
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (m *streamMutateMux) sendLoop(stream filer_pb.SeaweedFiler_StreamMutateEntryClient, stop <-chan struct{}, gen uint64) {
|
||||
defer m.drainSendCh()
|
||||
for {
|
||||
select {
|
||||
case req, ok := <-m.sendCh:
|
||||
if !ok {
|
||||
return // defensive: sendCh should not be closed
|
||||
}
|
||||
if req.gen != gen {
|
||||
req.errCh <- fmt.Errorf("%w: stream generation mismatch", ErrStreamTransport)
|
||||
continue
|
||||
}
|
||||
err := stream.Send(req.req)
|
||||
req.errCh <- err
|
||||
if err != nil {
|
||||
m.teardownStream(gen)
|
||||
return
|
||||
}
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *streamMutateMux) recvLoop(stream filer_pb.SeaweedFiler_StreamMutateEntryClient, gen uint64, recvDone chan struct{}) {
|
||||
defer func() {
|
||||
m.failAllPending()
|
||||
close(recvDone)
|
||||
}()
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
glog.V(1).Infof("stream mutate recv error (gen=%d): %v", gen, err)
|
||||
}
|
||||
m.teardownStream(gen)
|
||||
return
|
||||
}
|
||||
|
||||
if ch, ok := m.pending.Load(resp.RequestId); ok {
|
||||
ch.(chan *filer_pb.StreamMutateEntryResponse) <- resp
|
||||
// For single-response ops, the caller deletes from pending after recv.
|
||||
// For rename, the caller collects until is_last.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// teardownStream cleans up the stream for the given generation. It is safe to
|
||||
// call from both sendLoop and recvLoop; only the first call for a given
|
||||
// generation takes effect (idempotent via generation + nil-stream check).
|
||||
func (m *streamMutateMux) teardownStream(gen uint64) {
|
||||
m.mu.Lock()
|
||||
if m.generation != gen || m.stream == nil {
|
||||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
m.stream = nil
|
||||
if m.stopSend != nil {
|
||||
close(m.stopSend)
|
||||
m.stopSend = nil
|
||||
}
|
||||
if m.cancel != nil {
|
||||
m.cancel()
|
||||
m.cancel = nil
|
||||
}
|
||||
conn := m.grpcConn
|
||||
m.grpcConn = nil
|
||||
m.mu.Unlock()
|
||||
|
||||
// Do NOT call failAllPending here — recvLoop is the sole owner of
|
||||
// pending channel teardown. This avoids a race where teardownStream
|
||||
// closes a channel that recvLoop is about to send on.
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// failAllPending closes all pending response channels, causing waiters to
|
||||
// receive ok=false. It is idempotent: entries are deleted before channels are
|
||||
// closed, so concurrent calls cannot double-close.
|
||||
func (m *streamMutateMux) failAllPending() {
|
||||
var channels []chan *filer_pb.StreamMutateEntryResponse
|
||||
m.pending.Range(func(key, value any) bool {
|
||||
m.pending.Delete(key)
|
||||
channels = append(channels, value.(chan *filer_pb.StreamMutateEntryResponse))
|
||||
return true
|
||||
})
|
||||
for _, ch := range channels {
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
|
||||
// drainSendCh drains buffered requests from sendCh, sending an error to each
|
||||
// request's errCh so callers don't block. Called by sendLoop's defer on exit
|
||||
// and by Close for any stragglers.
|
||||
func (m *streamMutateMux) drainSendCh() {
|
||||
for {
|
||||
select {
|
||||
case req, ok := <-m.sendCh:
|
||||
if !ok {
|
||||
return // defensive: sendCh should not be closed
|
||||
}
|
||||
req.errCh <- fmt.Errorf("%w: stream shutting down", ErrStreamTransport)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsAvailable returns true if the stream mux is usable (not permanently disabled).
|
||||
func (m *streamMutateMux) IsAvailable() bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return !m.disabled
|
||||
}
|
||||
|
||||
// Close shuts down the stream. Called during unmount after all flushes complete.
|
||||
func (m *streamMutateMux) Close() {
|
||||
m.mu.Lock()
|
||||
if m.closed {
|
||||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
m.closed = true
|
||||
stream := m.stream
|
||||
m.stream = nil // prevent teardownStream from acting after Close
|
||||
cancel := m.cancel
|
||||
m.cancel = nil
|
||||
grpcConn := m.grpcConn
|
||||
m.grpcConn = nil
|
||||
recvDone := m.recvDone
|
||||
if m.stopSend != nil {
|
||||
close(m.stopSend)
|
||||
m.stopSend = nil
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// CloseSend triggers EOF on recvLoop; cancel ensures Recv unblocks
|
||||
// even if the transport is broken.
|
||||
if stream != nil {
|
||||
stream.CloseSend()
|
||||
}
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
// Wait for recvLoop to finish — it calls failAllPending on exit.
|
||||
if recvDone != nil {
|
||||
<-recvDone
|
||||
}
|
||||
if grpcConn != nil {
|
||||
grpcConn.Close()
|
||||
}
|
||||
// Drain any remaining requests buffered in sendCh. sendLoop's defer
|
||||
// drain handles most items, but stragglers enqueued during shutdown
|
||||
// (between ensureStream and the sendCh send) are caught here.
|
||||
// sendCh is intentionally left open to prevent send-on-closed panics.
|
||||
m.drainSendCh()
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -47,16 +46,10 @@ func (wfs *WFS) Symlink(cancel <-chan struct{}, header *fuse.InHeader, target st
|
||||
SkipCheckParentDirectory: true,
|
||||
}
|
||||
|
||||
err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
|
||||
wfs.mapPbIdFromLocalToFiler(request.Entry)
|
||||
defer wfs.mapPbIdFromFilerToLocal(request.Entry)
|
||||
|
||||
resp, err := filer_pb.CreateEntryWithResponse(context.Background(), client, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("symlink %s: %v", entryFullPath, err)
|
||||
}
|
||||
wfs.mapPbIdFromLocalToFiler(request.Entry)
|
||||
|
||||
resp, err := wfs.streamCreateEntry(context.Background(), request)
|
||||
if err == nil {
|
||||
event := resp.GetMetadataEvent()
|
||||
if event == nil {
|
||||
event = metadataCreateEvent(string(dirPath), request.Entry)
|
||||
@@ -65,9 +58,11 @@ func (wfs *WFS) Symlink(cancel <-chan struct{}, header *fuse.InHeader, target st
|
||||
glog.Warningf("symlink %s: best-effort metadata apply failed: %v", entryFullPath, applyErr)
|
||||
wfs.inodeToPath.InvalidateChildrenCache(dirPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Map back to local uid/gid before writing to the kernel.
|
||||
wfs.mapPbIdFromFilerToLocal(request.Entry)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
glog.V(0).Infof("Symlink %s => %s: %v", entryFullPath, target, err)
|
||||
return fuse.EIO
|
||||
|
||||
@@ -15,28 +15,3 @@ import "github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
func (wfs *WFS) Fallocate(cancel <-chan struct{}, in *fuse.FallocateIn) (code fuse.Status) {
|
||||
return fuse.ENOSYS
|
||||
}
|
||||
|
||||
func (wfs *WFS) GetLk(cancel <-chan struct{}, in *fuse.LkIn, out *fuse.LkOut) (code fuse.Status) {
|
||||
return fuse.ENOSYS
|
||||
}
|
||||
|
||||
func (wfs *WFS) SetLk(cancel <-chan struct{}, in *fuse.LkIn) (code fuse.Status) {
|
||||
return fuse.ENOSYS
|
||||
}
|
||||
|
||||
func (wfs *WFS) SetLkw(cancel <-chan struct{}, in *fuse.LkIn) (code fuse.Status) {
|
||||
return fuse.ENOSYS
|
||||
}
|
||||
|
||||
/**
|
||||
* Check file access permissions
|
||||
*
|
||||
* This will be called for the access() system call. If the
|
||||
* 'default_permissions' mount option is given, this method is not
|
||||
* called.
|
||||
*
|
||||
* This method is not called under Linux kernel versions 2.4.x
|
||||
*/
|
||||
func (wfs *WFS) Access(cancel <-chan struct{}, input *fuse.AccessIn) (code fuse.Status) {
|
||||
return fuse.ENOSYS
|
||||
}
|
||||
|
||||
+13
-18
@@ -15,23 +15,20 @@ func (wfs *WFS) saveEntry(path util.FullPath, entry *filer_pb.Entry) (code fuse.
|
||||
|
||||
parentDir, _ := path.DirAndName()
|
||||
|
||||
err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
wfs.mapPbIdFromLocalToFiler(entry)
|
||||
defer wfs.mapPbIdFromFilerToLocal(entry)
|
||||
|
||||
wfs.mapPbIdFromLocalToFiler(entry)
|
||||
defer wfs.mapPbIdFromFilerToLocal(entry)
|
||||
|
||||
request := &filer_pb.UpdateEntryRequest{
|
||||
Directory: parentDir,
|
||||
Entry: entry,
|
||||
Signatures: []int32{wfs.signature},
|
||||
}
|
||||
|
||||
glog.V(1).Infof("save entry: %v", request)
|
||||
resp, err := filer_pb.UpdateEntryWithResponse(context.Background(), client, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateEntry dir %s: %v", path, err)
|
||||
}
|
||||
request := &filer_pb.UpdateEntryRequest{
|
||||
Directory: parentDir,
|
||||
Entry: entry,
|
||||
Signatures: []int32{wfs.signature},
|
||||
}
|
||||
|
||||
glog.V(1).Infof("save entry: %v", request)
|
||||
resp, err := wfs.streamUpdateEntry(context.Background(), request)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("UpdateEntry dir %s: %v", path, err)
|
||||
} else {
|
||||
event := resp.GetMetadataEvent()
|
||||
if event == nil {
|
||||
event = metadataUpdateEvent(parentDir, entry)
|
||||
@@ -40,9 +37,7 @@ func (wfs *WFS) saveEntry(path util.FullPath, entry *filer_pb.Entry) (code fuse.
|
||||
glog.Warningf("saveEntry %s: best-effort metadata apply failed: %v", path, applyErr)
|
||||
wfs.inodeToPath.InvalidateChildrenCache(util.FullPath(parentDir))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
// glog.V(0).Infof("saveEntry %s: %v", path, err)
|
||||
fuseStatus := grpcErrorToFuseStatus(err)
|
||||
|
||||
@@ -249,7 +249,8 @@ func IsRetriableError(code int16) bool {
|
||||
return GetErrorInfo(code).Retriable
|
||||
}
|
||||
|
||||
// BuildErrorResponse builds a standard Kafka error response
|
||||
// BuildErrorResponse builds a standard Kafka error response (2-byte error code only).
|
||||
// Prefer BuildAPIErrorResponse for API-aware error bodies.
|
||||
func BuildErrorResponse(correlationID uint32, errorCode int16) []byte {
|
||||
response := make([]byte, 0, 8)
|
||||
|
||||
@@ -264,6 +265,228 @@ func BuildErrorResponse(correlationID uint32, errorCode int16) []byte {
|
||||
return response
|
||||
}
|
||||
|
||||
// BuildAPIErrorResponse builds a minimal-but-valid error response body whose
|
||||
// layout matches the schema the client expects for the given API key and
|
||||
// version. The correlation ID and header-level tagged fields are NOT included
|
||||
// (those are added by writeResponseWithHeader).
|
||||
func BuildAPIErrorResponse(apiKey, apiVersion uint16, errorCode int16) []byte {
|
||||
ec := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(ec, uint16(errorCode))
|
||||
throttle := []byte{0, 0, 0, 0} // throttle_time_ms = 0
|
||||
emptyArr := []byte{0, 0, 0, 0} // regular array length = 0
|
||||
nullStr := []byte{0xFF, 0xFF} // nullable string = null
|
||||
emptyStr := []byte{0, 0} // string length = 0
|
||||
|
||||
switch APIKey(apiKey) {
|
||||
|
||||
// --- error_code is the first body field -----------------------------------
|
||||
|
||||
case APIKeyApiVersions:
|
||||
// error_code(2) + api_keys_array + [throttle_time_ms v1+] [+ tagged_fields v3+]
|
||||
buf := append([]byte{}, ec...)
|
||||
if apiVersion >= 3 {
|
||||
buf = append(buf, 1) // compact array length=0 (varint 1 = 0+1)
|
||||
} else {
|
||||
buf = append(buf, emptyArr...)
|
||||
}
|
||||
if apiVersion >= 1 {
|
||||
buf = append(buf, throttle...)
|
||||
}
|
||||
if apiVersion >= 3 {
|
||||
buf = append(buf, 0) // body-level tagged fields
|
||||
}
|
||||
return buf
|
||||
|
||||
// --- throttle_time_ms(4) + error_code(2) + trailing fields ----------------
|
||||
|
||||
case APIKeyFindCoordinator:
|
||||
// [throttle v1+] + error_code + error_msg + node_id + host + port
|
||||
buf := make([]byte, 0, 24)
|
||||
if apiVersion >= 1 {
|
||||
buf = append(buf, throttle...)
|
||||
}
|
||||
buf = append(buf, ec...)
|
||||
buf = append(buf, nullStr...) // error_message
|
||||
buf = append(buf, 0xFF, 0xFF, 0xFF, 0xFF) // node_id = -1
|
||||
buf = append(buf, emptyStr...) // host
|
||||
buf = append(buf, 0, 0, 0, 0) // port = 0
|
||||
return buf
|
||||
|
||||
case APIKeyJoinGroup:
|
||||
// [throttle v2+] + error_code + generation_id + protocol_name + leader + member_id + members[]
|
||||
buf := make([]byte, 0, 24)
|
||||
if apiVersion >= 2 {
|
||||
buf = append(buf, throttle...)
|
||||
}
|
||||
buf = append(buf, ec...)
|
||||
buf = append(buf, 0xFF, 0xFF, 0xFF, 0xFF) // generation_id = -1
|
||||
buf = append(buf, nullStr...) // protocol_name
|
||||
buf = append(buf, emptyStr...) // leader
|
||||
buf = append(buf, emptyStr...) // member_id
|
||||
buf = append(buf, emptyArr...) // members = []
|
||||
return buf
|
||||
|
||||
case APIKeySyncGroup:
|
||||
// [throttle v1+] + error_code + assignment
|
||||
buf := make([]byte, 0, 12)
|
||||
if apiVersion >= 1 {
|
||||
buf = append(buf, throttle...)
|
||||
}
|
||||
buf = append(buf, ec...)
|
||||
buf = append(buf, 0, 0, 0, 0) // assignment bytes length = 0
|
||||
return buf
|
||||
|
||||
case APIKeyHeartbeat:
|
||||
// [throttle v1+] + error_code
|
||||
buf := make([]byte, 0, 8)
|
||||
if apiVersion >= 1 {
|
||||
buf = append(buf, throttle...)
|
||||
}
|
||||
buf = append(buf, ec...)
|
||||
return buf
|
||||
|
||||
case APIKeyLeaveGroup:
|
||||
// [throttle v1+] + error_code [+ members v3+]
|
||||
buf := make([]byte, 0, 12)
|
||||
if apiVersion >= 1 {
|
||||
buf = append(buf, throttle...)
|
||||
}
|
||||
buf = append(buf, ec...)
|
||||
if apiVersion >= 3 {
|
||||
buf = append(buf, emptyArr...) // members = []
|
||||
}
|
||||
return buf
|
||||
|
||||
case APIKeyInitProducerId:
|
||||
// throttle + error_code + producer_id + producer_epoch
|
||||
buf := make([]byte, 0, 18)
|
||||
buf = append(buf, throttle...)
|
||||
buf = append(buf, ec...)
|
||||
buf = append(buf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF) // producer_id = -1
|
||||
buf = append(buf, 0xFF, 0xFF) // producer_epoch = -1
|
||||
return buf
|
||||
|
||||
case APIKeyListGroups:
|
||||
// [throttle v1+] + error_code + groups[]
|
||||
buf := make([]byte, 0, 12)
|
||||
if apiVersion >= 1 {
|
||||
buf = append(buf, throttle...)
|
||||
}
|
||||
buf = append(buf, ec...)
|
||||
buf = append(buf, emptyArr...) // groups = []
|
||||
return buf
|
||||
|
||||
case APIKeyDescribeCluster:
|
||||
// throttle + error_code + error_msg + cluster_id + controller_id + brokers[] + cluster_authorized_operations
|
||||
buf := make([]byte, 0, 24)
|
||||
buf = append(buf, throttle...)
|
||||
buf = append(buf, ec...)
|
||||
buf = append(buf, nullStr...) // error_message
|
||||
buf = append(buf, emptyStr...) // cluster_id
|
||||
buf = append(buf, 0xFF, 0xFF, 0xFF, 0xFF) // controller_id = -1
|
||||
buf = append(buf, emptyArr...) // brokers = []
|
||||
buf = append(buf, 0, 0, 0, 0) // cluster_authorized_operations
|
||||
return buf
|
||||
|
||||
// --- array-based responses (no top-level error_code) ----------------------
|
||||
|
||||
case APIKeyProduce:
|
||||
// topics[] + [throttle v1+]
|
||||
buf := append([]byte{}, emptyArr...)
|
||||
if apiVersion >= 1 {
|
||||
buf = append(buf, throttle...)
|
||||
}
|
||||
return buf
|
||||
|
||||
case APIKeyFetch:
|
||||
// [throttle v1+] [+ error_code + session_id v7+] + topics[]
|
||||
buf := make([]byte, 0, 16)
|
||||
if apiVersion >= 1 {
|
||||
buf = append(buf, throttle...)
|
||||
}
|
||||
if apiVersion >= 7 {
|
||||
buf = append(buf, ec...) // error_code
|
||||
buf = append(buf, 0, 0, 0, 0) // session_id = 0
|
||||
}
|
||||
buf = append(buf, emptyArr...) // topics = []
|
||||
return buf
|
||||
|
||||
case APIKeyMetadata:
|
||||
// [throttle v3+] + brokers[] + [cluster_id v2+] + [controller_id v1+] + topics[]
|
||||
buf := make([]byte, 0, 24)
|
||||
if apiVersion >= 3 {
|
||||
buf = append(buf, throttle...)
|
||||
}
|
||||
buf = append(buf, emptyArr...) // brokers = []
|
||||
if apiVersion >= 2 {
|
||||
buf = append(buf, nullStr...) // cluster_id
|
||||
}
|
||||
if apiVersion >= 1 {
|
||||
buf = append(buf, 0xFF, 0xFF, 0xFF, 0xFF) // controller_id = -1
|
||||
}
|
||||
buf = append(buf, emptyArr...) // topics = []
|
||||
return buf
|
||||
|
||||
case APIKeyListOffsets:
|
||||
// [throttle v2+] + topics[]
|
||||
buf := make([]byte, 0, 8)
|
||||
if apiVersion >= 2 {
|
||||
buf = append(buf, throttle...)
|
||||
}
|
||||
buf = append(buf, emptyArr...)
|
||||
return buf
|
||||
|
||||
case APIKeyOffsetCommit:
|
||||
// [throttle v3+] + topics[]
|
||||
buf := make([]byte, 0, 8)
|
||||
if apiVersion >= 3 {
|
||||
buf = append(buf, throttle...)
|
||||
}
|
||||
buf = append(buf, emptyArr...)
|
||||
return buf
|
||||
|
||||
case APIKeyOffsetFetch:
|
||||
// [throttle v3+] + topics[] [+ error_code v2+]
|
||||
buf := make([]byte, 0, 12)
|
||||
if apiVersion >= 3 {
|
||||
buf = append(buf, throttle...)
|
||||
}
|
||||
buf = append(buf, emptyArr...) // topics = []
|
||||
if apiVersion >= 2 {
|
||||
buf = append(buf, ec...) // error_code
|
||||
}
|
||||
return buf
|
||||
|
||||
case APIKeyCreateTopics:
|
||||
// throttle + topics[]
|
||||
buf := append([]byte{}, throttle...)
|
||||
buf = append(buf, emptyArr...)
|
||||
return buf
|
||||
|
||||
case APIKeyDeleteTopics:
|
||||
// throttle + topics[]
|
||||
buf := append([]byte{}, throttle...)
|
||||
buf = append(buf, emptyArr...)
|
||||
return buf
|
||||
|
||||
case APIKeyDescribeGroups:
|
||||
// throttle + groups[]
|
||||
buf := append([]byte{}, throttle...)
|
||||
buf = append(buf, emptyArr...)
|
||||
return buf
|
||||
|
||||
case APIKeyDescribeConfigs:
|
||||
// throttle + resources[]
|
||||
buf := append([]byte{}, throttle...)
|
||||
buf = append(buf, emptyArr...)
|
||||
return buf
|
||||
|
||||
default:
|
||||
// Unknown API — emit just the error code as a best-effort fallback
|
||||
return append([]byte{}, ec...)
|
||||
}
|
||||
}
|
||||
|
||||
// BuildErrorResponseWithMessage builds a Kafka error response with error message
|
||||
func BuildErrorResponseWithMessage(correlationID uint32, errorCode int16, message string) []byte {
|
||||
response := BuildErrorResponse(correlationID, errorCode)
|
||||
|
||||
@@ -626,14 +626,21 @@ func (h *Handler) HandleConn(ctx context.Context, conn net.Conn) error {
|
||||
}
|
||||
|
||||
// Send this response
|
||||
var respBody []byte
|
||||
if readyResp.err != nil {
|
||||
glog.Errorf("[%s] Error processing correlation=%d: %v", connectionID, readyResp.correlationID, readyResp.err)
|
||||
// Build an API-versioned error response so the body matches
|
||||
// the schema the client expects for this API key/version.
|
||||
// A generic 2-byte error code would corrupt the protocol
|
||||
// stream for APIs that start with throttle_time or arrays.
|
||||
respBody = BuildAPIErrorResponse(readyResp.apiKey, readyResp.apiVersion, ErrorCodeUnknownServerError)
|
||||
} else {
|
||||
if writeErr := h.writeResponseWithHeader(w, readyResp.correlationID, readyResp.apiKey, readyResp.apiVersion, readyResp.response, timeoutConfig.WriteTimeout); writeErr != nil {
|
||||
glog.Errorf("[%s] Response writer WRITE ERROR correlation=%d: %v - EXITING", connectionID, readyResp.correlationID, writeErr)
|
||||
correlationQueueMu.Unlock()
|
||||
return
|
||||
}
|
||||
respBody = readyResp.response
|
||||
}
|
||||
if writeErr := h.writeResponseWithHeader(w, readyResp.correlationID, readyResp.apiKey, readyResp.apiVersion, respBody, timeoutConfig.WriteTimeout); writeErr != nil {
|
||||
glog.Errorf("[%s] Response writer WRITE ERROR correlation=%d: %v - EXITING", connectionID, readyResp.correlationID, writeErr)
|
||||
correlationQueueMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Remove from pending and advance
|
||||
@@ -689,7 +696,10 @@ func (h *Handler) HandleConn(ctx context.Context, conn net.Conn) error {
|
||||
// Connection closed, stop processing
|
||||
return
|
||||
case <-time.After(5 * time.Second):
|
||||
glog.Warningf("[%s] Control plane: timeout sending response correlation=%d", connectionID, req.correlationID)
|
||||
// responseChan stuck — cancel context to tear down connection.
|
||||
// The orphaned correlationID would stall the ordered writer permanently.
|
||||
glog.Warningf("[%s] Control plane: timeout sending response correlation=%d, tearing down connection", connectionID, req.correlationID)
|
||||
cancel()
|
||||
}
|
||||
case <-ctx.Done():
|
||||
// Context cancelled, drain remaining requests before exiting
|
||||
@@ -767,7 +777,10 @@ func (h *Handler) HandleConn(ctx context.Context, conn net.Conn) error {
|
||||
// Connection closed, stop processing
|
||||
return
|
||||
case <-time.After(5 * time.Second):
|
||||
glog.Warningf("[%s] Data plane: timeout sending response correlation=%d", connectionID, req.correlationID)
|
||||
// responseChan stuck — cancel context to tear down connection.
|
||||
// The orphaned correlationID would stall the ordered writer permanently.
|
||||
glog.Warningf("[%s] Data plane: timeout sending response correlation=%d, tearing down connection", connectionID, req.correlationID)
|
||||
cancel()
|
||||
}
|
||||
case <-ctx.Done():
|
||||
// Context cancelled, drain remaining requests before exiting
|
||||
@@ -804,13 +817,18 @@ func (h *Handler) HandleConn(ctx context.Context, conn net.Conn) error {
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
// Close channels in correct order to avoid panics
|
||||
// 1. Close input channels to stop accepting new requests
|
||||
// Cancel context FIRST so the response writer (and any worker stuck
|
||||
// in an inner select) sees ctx.Done() and can exit. Previously
|
||||
// cancel() ran in a separate defer registered earlier (LIFO: later
|
||||
// defer runs first), so wg.Wait() below would deadlock waiting for
|
||||
// the response writer which was itself waiting for ctx.Done().
|
||||
cancel()
|
||||
// Close input channels to stop accepting new requests
|
||||
close(controlChan)
|
||||
close(dataChan)
|
||||
// 2. Wait for worker goroutines to finish processing and sending responses
|
||||
// Wait for worker goroutines to finish processing and sending responses
|
||||
wg.Wait()
|
||||
// 3. NOW close responseChan to signal response writer to exit
|
||||
// NOW close responseChan (safe — all senders have exited)
|
||||
close(responseChan)
|
||||
}()
|
||||
|
||||
@@ -872,14 +890,21 @@ func (h *Handler) HandleConn(ctx context.Context, conn net.Conn) error {
|
||||
if err := conn.SetReadDeadline(time.Now().Add(timeoutConfig.ReadTimeout)); err != nil {
|
||||
}
|
||||
|
||||
// Read the message
|
||||
// OPTIMIZATION: Use buffer pool to reduce GC pressure (was 1MB/sec at 1000 req/s)
|
||||
messageBuf := mem.Allocate(int(size))
|
||||
defer mem.Free(messageBuf)
|
||||
if _, err := io.ReadFull(r, messageBuf); err != nil {
|
||||
// Read the message into a pooled buffer, then copy to owned memory
|
||||
// and return the pool buffer immediately. The previous code used
|
||||
// defer mem.Free which accumulated one deferred free per iteration,
|
||||
// leaking pool buffers for the connection lifetime and risking
|
||||
// use-after-free when the defers ran before processing goroutines
|
||||
// finished draining.
|
||||
poolBuf := mem.Allocate(int(size))
|
||||
if _, err := io.ReadFull(r, poolBuf); err != nil {
|
||||
mem.Free(poolBuf)
|
||||
_ = HandleTimeoutError(err, "read") // errorCode
|
||||
return fmt.Errorf("read message: %w", err)
|
||||
}
|
||||
messageBuf := make([]byte, int(size))
|
||||
copy(messageBuf, poolBuf)
|
||||
mem.Free(poolBuf)
|
||||
|
||||
// Parse at least the basic header to get API key and correlation ID
|
||||
if len(messageBuf) < 8 {
|
||||
@@ -898,6 +923,13 @@ func (h *Handler) HandleConn(ctx context.Context, conn net.Conn) error {
|
||||
if writeErr != nil {
|
||||
return fmt.Errorf("build error response: %w", writeErr)
|
||||
}
|
||||
// Add to correlation queue BEFORE sending to responseChan so the
|
||||
// response writer can match and send it. Without this the response
|
||||
// sits in pendingResponses forever and the client hangs.
|
||||
correlationQueueMu.Lock()
|
||||
correlationQueue = append(correlationQueue, correlationID)
|
||||
correlationQueueMu.Unlock()
|
||||
|
||||
// Send error response through response queue to maintain sequential ordering
|
||||
select {
|
||||
case responseChan <- &kafkaResponse{
|
||||
@@ -1016,28 +1048,49 @@ func (h *Handler) HandleConn(ctx context.Context, conn net.Conn) error {
|
||||
|
||||
// Route to appropriate channel based on API key
|
||||
var targetChan chan *kafkaRequest
|
||||
if apiKey == 2 { // ListOffsets
|
||||
}
|
||||
if isDataPlaneAPI(apiKey) {
|
||||
targetChan = dataChan
|
||||
} else {
|
||||
targetChan = controlChan
|
||||
}
|
||||
|
||||
// Only add to correlation queue AFTER successful channel send
|
||||
// If we add before and the channel blocks, the correlation ID is in the queue
|
||||
// but the request never gets processed, causing response writer deadlock
|
||||
// Add correlation ID BEFORE channel send to prevent race condition:
|
||||
// If we add after, the processor can finish and send the response before the
|
||||
// ID is in the queue. The response writer then can't match the response to a
|
||||
// queue entry, causing a deadlock for sequential clients (e.g., Sarama).
|
||||
// If the channel send fails, we send an error response so the response writer
|
||||
// can advance past this entry.
|
||||
correlationQueueMu.Lock()
|
||||
correlationQueue = append(correlationQueue, correlationID)
|
||||
correlationQueueMu.Unlock()
|
||||
|
||||
select {
|
||||
case targetChan <- req:
|
||||
// Request queued successfully - NOW add to correlation tracking
|
||||
correlationQueueMu.Lock()
|
||||
correlationQueue = append(correlationQueue, correlationID)
|
||||
correlationQueueMu.Unlock()
|
||||
// Request queued successfully
|
||||
case <-ctx.Done():
|
||||
// Context cancelled - send error response so response writer can advance
|
||||
select {
|
||||
case responseChan <- &kafkaResponse{
|
||||
correlationID: correlationID,
|
||||
apiKey: apiKey,
|
||||
apiVersion: apiVersion,
|
||||
err: ctx.Err(),
|
||||
}:
|
||||
default:
|
||||
}
|
||||
return ctx.Err()
|
||||
case <-time.After(10 * time.Second):
|
||||
// Channel full for too long - this shouldn't happen with proper backpressure
|
||||
// Channel full for too long - send error response so response writer can advance
|
||||
glog.Errorf("[%s] Failed to queue correlation=%d - channel full (10s timeout)", connectionID, correlationID)
|
||||
select {
|
||||
case responseChan <- &kafkaResponse{
|
||||
correlationID: correlationID,
|
||||
apiKey: apiKey,
|
||||
apiVersion: apiVersion,
|
||||
err: fmt.Errorf("request queue full"),
|
||||
}:
|
||||
default:
|
||||
}
|
||||
return fmt.Errorf("request queue full: correlation=%d", correlationID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,31 +28,21 @@ func (h *Handler) handleProduce(ctx context.Context, correlationID uint32, apiVe
|
||||
}
|
||||
|
||||
func (h *Handler) handleProduceV0V1(ctx context.Context, correlationID uint32, apiVersion uint16, requestBody []byte) ([]byte, error) {
|
||||
// Parse Produce v0/v1 request
|
||||
// Request format: client_id + acks(2) + timeout(4) + topics_array
|
||||
// Parse Produce v0/v1 request body (client_id already stripped in HandleConn)
|
||||
// Body format: acks(INT16) + timeout_ms(INT32) + topics(ARRAY)
|
||||
|
||||
if len(requestBody) < 8 { // client_id_size(2) + acks(2) + timeout(4)
|
||||
if len(requestBody) < 10 { // acks(2) + timeout_ms(4) + topics_count(4)
|
||||
return nil, fmt.Errorf("Produce request too short")
|
||||
}
|
||||
|
||||
// Skip client_id
|
||||
clientIDSize := binary.BigEndian.Uint16(requestBody[0:2])
|
||||
offset := 0
|
||||
|
||||
if len(requestBody) < 2+int(clientIDSize) {
|
||||
return nil, fmt.Errorf("Produce request client_id too short")
|
||||
}
|
||||
|
||||
_ = string(requestBody[2 : 2+int(clientIDSize)]) // clientID
|
||||
offset := 2 + int(clientIDSize)
|
||||
|
||||
if len(requestBody) < offset+10 { // acks(2) + timeout(4) + topics_count(4)
|
||||
return nil, fmt.Errorf("Produce request missing data")
|
||||
}
|
||||
|
||||
// Parse acks and timeout
|
||||
_ = int16(binary.BigEndian.Uint16(requestBody[offset : offset+2])) // acks
|
||||
offset += 2
|
||||
|
||||
_ = binary.BigEndian.Uint32(requestBody[offset : offset+4]) // timeout_ms
|
||||
offset += 4
|
||||
|
||||
topicsCount := binary.BigEndian.Uint32(requestBody[offset : offset+4])
|
||||
offset += 4
|
||||
|
||||
|
||||
+45
-1
@@ -33,6 +33,9 @@ service SeaweedFiler {
|
||||
rpc StreamRenameEntry (StreamRenameEntryRequest) returns (stream StreamRenameEntryResponse) {
|
||||
}
|
||||
|
||||
rpc StreamMutateEntry (stream StreamMutateEntryRequest) returns (stream StreamMutateEntryResponse) {
|
||||
}
|
||||
|
||||
rpc AssignVolume (AssignVolumeRequest) returns (AssignVolumeResponse) {
|
||||
}
|
||||
|
||||
@@ -203,9 +206,21 @@ message CreateEntryRequest {
|
||||
bool skip_check_parent_directory = 6;
|
||||
}
|
||||
|
||||
// Structured error codes for filer entry operations.
|
||||
// Values are stable — do not reorder or reuse numbers.
|
||||
enum FilerError {
|
||||
OK = 0;
|
||||
ENTRY_NAME_TOO_LONG = 1; // name exceeds max_file_name_length
|
||||
PARENT_IS_FILE = 2; // parent path component is a file, not a directory
|
||||
EXISTING_IS_DIRECTORY = 3; // cannot overwrite directory with file
|
||||
EXISTING_IS_FILE = 4; // cannot overwrite file with directory
|
||||
ENTRY_ALREADY_EXISTS = 5; // O_EXCL and entry already exists
|
||||
}
|
||||
|
||||
message CreateEntryResponse {
|
||||
string error = 1;
|
||||
string error = 1; // kept for human readability + backward compat
|
||||
SubscribeMetadataResponse metadata_event = 2;
|
||||
FilerError error_code = 3; // machine-readable error code
|
||||
}
|
||||
|
||||
message UpdateEntryRequest {
|
||||
@@ -523,3 +538,32 @@ message TransferLocksRequest {
|
||||
}
|
||||
message TransferLocksResponse {
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// StreamMutateEntry: ordered bidirectional streaming for all filer mutations.
|
||||
// All create/update/delete/rename operations from a single mount go through
|
||||
// one stream, preserving mutation ordering and eliminating per-request
|
||||
// connection overhead.
|
||||
|
||||
message StreamMutateEntryRequest {
|
||||
uint64 request_id = 1;
|
||||
oneof request {
|
||||
CreateEntryRequest create_request = 2;
|
||||
UpdateEntryRequest update_request = 3;
|
||||
DeleteEntryRequest delete_request = 4;
|
||||
StreamRenameEntryRequest rename_request = 5;
|
||||
}
|
||||
}
|
||||
|
||||
message StreamMutateEntryResponse {
|
||||
uint64 request_id = 1;
|
||||
bool is_last = 2; // always true except for rename, which sends multiple events
|
||||
oneof response {
|
||||
CreateEntryResponse create_response = 3;
|
||||
UpdateEntryResponse update_response = 4;
|
||||
DeleteEntryResponse delete_response = 5;
|
||||
StreamRenameEntryResponse rename_response = 6;
|
||||
}
|
||||
string error = 7; // human-readable error message when the operation failed
|
||||
int32 errno = 8; // POSIX errno (e.g. ENOENT=2, ENOTEMPTY=66) for direct FUSE status mapping
|
||||
}
|
||||
|
||||
+565
-172
@@ -73,6 +73,66 @@ func (SSEType) EnumDescriptor() ([]byte, []int) {
|
||||
return file_filer_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
// Structured error codes for filer entry operations.
|
||||
// Values are stable — do not reorder or reuse numbers.
|
||||
type FilerError int32
|
||||
|
||||
const (
|
||||
FilerError_OK FilerError = 0
|
||||
FilerError_ENTRY_NAME_TOO_LONG FilerError = 1 // name exceeds max_file_name_length
|
||||
FilerError_PARENT_IS_FILE FilerError = 2 // parent path component is a file, not a directory
|
||||
FilerError_EXISTING_IS_DIRECTORY FilerError = 3 // cannot overwrite directory with file
|
||||
FilerError_EXISTING_IS_FILE FilerError = 4 // cannot overwrite file with directory
|
||||
FilerError_ENTRY_ALREADY_EXISTS FilerError = 5 // O_EXCL and entry already exists
|
||||
)
|
||||
|
||||
// Enum value maps for FilerError.
|
||||
var (
|
||||
FilerError_name = map[int32]string{
|
||||
0: "OK",
|
||||
1: "ENTRY_NAME_TOO_LONG",
|
||||
2: "PARENT_IS_FILE",
|
||||
3: "EXISTING_IS_DIRECTORY",
|
||||
4: "EXISTING_IS_FILE",
|
||||
5: "ENTRY_ALREADY_EXISTS",
|
||||
}
|
||||
FilerError_value = map[string]int32{
|
||||
"OK": 0,
|
||||
"ENTRY_NAME_TOO_LONG": 1,
|
||||
"PARENT_IS_FILE": 2,
|
||||
"EXISTING_IS_DIRECTORY": 3,
|
||||
"EXISTING_IS_FILE": 4,
|
||||
"ENTRY_ALREADY_EXISTS": 5,
|
||||
}
|
||||
)
|
||||
|
||||
func (x FilerError) Enum() *FilerError {
|
||||
p := new(FilerError)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x FilerError) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (FilerError) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_filer_proto_enumTypes[1].Descriptor()
|
||||
}
|
||||
|
||||
func (FilerError) Type() protoreflect.EnumType {
|
||||
return &file_filer_proto_enumTypes[1]
|
||||
}
|
||||
|
||||
func (x FilerError) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use FilerError.Descriptor instead.
|
||||
func (FilerError) EnumDescriptor() ([]byte, []int) {
|
||||
return file_filer_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
type LookupDirectoryEntryRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Directory string `protobuf:"bytes,1,opt,name=directory,proto3" json:"directory,omitempty"`
|
||||
@@ -1119,8 +1179,9 @@ func (x *CreateEntryRequest) GetSkipCheckParentDirectory() bool {
|
||||
|
||||
type CreateEntryResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
|
||||
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` // kept for human readability + backward compat
|
||||
MetadataEvent *SubscribeMetadataResponse `protobuf:"bytes,2,opt,name=metadata_event,json=metadataEvent,proto3" json:"metadata_event,omitempty"`
|
||||
ErrorCode FilerError `protobuf:"varint,3,opt,name=error_code,json=errorCode,proto3,enum=filer_pb.FilerError" json:"error_code,omitempty"` // machine-readable error code
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -1169,6 +1230,13 @@ func (x *CreateEntryResponse) GetMetadataEvent() *SubscribeMetadataResponse {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CreateEntryResponse) GetErrorCode() FilerError {
|
||||
if x != nil {
|
||||
return x.ErrorCode
|
||||
}
|
||||
return FilerError_OK
|
||||
}
|
||||
|
||||
type UpdateEntryRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Directory string `protobuf:"bytes,1,opt,name=directory,proto3" json:"directory,omitempty"`
|
||||
@@ -4246,6 +4314,274 @@ func (*TransferLocksResponse) Descriptor() ([]byte, []int) {
|
||||
return file_filer_proto_rawDescGZIP(), []int{65}
|
||||
}
|
||||
|
||||
type StreamMutateEntryRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RequestId uint64 `protobuf:"varint,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
|
||||
// Types that are valid to be assigned to Request:
|
||||
//
|
||||
// *StreamMutateEntryRequest_CreateRequest
|
||||
// *StreamMutateEntryRequest_UpdateRequest
|
||||
// *StreamMutateEntryRequest_DeleteRequest
|
||||
// *StreamMutateEntryRequest_RenameRequest
|
||||
Request isStreamMutateEntryRequest_Request `protobuf_oneof:"request"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryRequest) Reset() {
|
||||
*x = StreamMutateEntryRequest{}
|
||||
mi := &file_filer_proto_msgTypes[66]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*StreamMutateEntryRequest) ProtoMessage() {}
|
||||
|
||||
func (x *StreamMutateEntryRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_filer_proto_msgTypes[66]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use StreamMutateEntryRequest.ProtoReflect.Descriptor instead.
|
||||
func (*StreamMutateEntryRequest) Descriptor() ([]byte, []int) {
|
||||
return file_filer_proto_rawDescGZIP(), []int{66}
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryRequest) GetRequestId() uint64 {
|
||||
if x != nil {
|
||||
return x.RequestId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryRequest) GetRequest() isStreamMutateEntryRequest_Request {
|
||||
if x != nil {
|
||||
return x.Request
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryRequest) GetCreateRequest() *CreateEntryRequest {
|
||||
if x != nil {
|
||||
if x, ok := x.Request.(*StreamMutateEntryRequest_CreateRequest); ok {
|
||||
return x.CreateRequest
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryRequest) GetUpdateRequest() *UpdateEntryRequest {
|
||||
if x != nil {
|
||||
if x, ok := x.Request.(*StreamMutateEntryRequest_UpdateRequest); ok {
|
||||
return x.UpdateRequest
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryRequest) GetDeleteRequest() *DeleteEntryRequest {
|
||||
if x != nil {
|
||||
if x, ok := x.Request.(*StreamMutateEntryRequest_DeleteRequest); ok {
|
||||
return x.DeleteRequest
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryRequest) GetRenameRequest() *StreamRenameEntryRequest {
|
||||
if x != nil {
|
||||
if x, ok := x.Request.(*StreamMutateEntryRequest_RenameRequest); ok {
|
||||
return x.RenameRequest
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isStreamMutateEntryRequest_Request interface {
|
||||
isStreamMutateEntryRequest_Request()
|
||||
}
|
||||
|
||||
type StreamMutateEntryRequest_CreateRequest struct {
|
||||
CreateRequest *CreateEntryRequest `protobuf:"bytes,2,opt,name=create_request,json=createRequest,proto3,oneof"`
|
||||
}
|
||||
|
||||
type StreamMutateEntryRequest_UpdateRequest struct {
|
||||
UpdateRequest *UpdateEntryRequest `protobuf:"bytes,3,opt,name=update_request,json=updateRequest,proto3,oneof"`
|
||||
}
|
||||
|
||||
type StreamMutateEntryRequest_DeleteRequest struct {
|
||||
DeleteRequest *DeleteEntryRequest `protobuf:"bytes,4,opt,name=delete_request,json=deleteRequest,proto3,oneof"`
|
||||
}
|
||||
|
||||
type StreamMutateEntryRequest_RenameRequest struct {
|
||||
RenameRequest *StreamRenameEntryRequest `protobuf:"bytes,5,opt,name=rename_request,json=renameRequest,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*StreamMutateEntryRequest_CreateRequest) isStreamMutateEntryRequest_Request() {}
|
||||
|
||||
func (*StreamMutateEntryRequest_UpdateRequest) isStreamMutateEntryRequest_Request() {}
|
||||
|
||||
func (*StreamMutateEntryRequest_DeleteRequest) isStreamMutateEntryRequest_Request() {}
|
||||
|
||||
func (*StreamMutateEntryRequest_RenameRequest) isStreamMutateEntryRequest_Request() {}
|
||||
|
||||
type StreamMutateEntryResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RequestId uint64 `protobuf:"varint,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
|
||||
IsLast bool `protobuf:"varint,2,opt,name=is_last,json=isLast,proto3" json:"is_last,omitempty"` // always true except for rename, which sends multiple events
|
||||
// Types that are valid to be assigned to Response:
|
||||
//
|
||||
// *StreamMutateEntryResponse_CreateResponse
|
||||
// *StreamMutateEntryResponse_UpdateResponse
|
||||
// *StreamMutateEntryResponse_DeleteResponse
|
||||
// *StreamMutateEntryResponse_RenameResponse
|
||||
Response isStreamMutateEntryResponse_Response `protobuf_oneof:"response"`
|
||||
Error string `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"` // human-readable error message when the operation failed
|
||||
Errno int32 `protobuf:"varint,8,opt,name=errno,proto3" json:"errno,omitempty"` // POSIX errno (e.g. ENOENT=2, ENOTEMPTY=66) for direct FUSE status mapping
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryResponse) Reset() {
|
||||
*x = StreamMutateEntryResponse{}
|
||||
mi := &file_filer_proto_msgTypes[67]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*StreamMutateEntryResponse) ProtoMessage() {}
|
||||
|
||||
func (x *StreamMutateEntryResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_filer_proto_msgTypes[67]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use StreamMutateEntryResponse.ProtoReflect.Descriptor instead.
|
||||
func (*StreamMutateEntryResponse) Descriptor() ([]byte, []int) {
|
||||
return file_filer_proto_rawDescGZIP(), []int{67}
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryResponse) GetRequestId() uint64 {
|
||||
if x != nil {
|
||||
return x.RequestId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryResponse) GetIsLast() bool {
|
||||
if x != nil {
|
||||
return x.IsLast
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryResponse) GetResponse() isStreamMutateEntryResponse_Response {
|
||||
if x != nil {
|
||||
return x.Response
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryResponse) GetCreateResponse() *CreateEntryResponse {
|
||||
if x != nil {
|
||||
if x, ok := x.Response.(*StreamMutateEntryResponse_CreateResponse); ok {
|
||||
return x.CreateResponse
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryResponse) GetUpdateResponse() *UpdateEntryResponse {
|
||||
if x != nil {
|
||||
if x, ok := x.Response.(*StreamMutateEntryResponse_UpdateResponse); ok {
|
||||
return x.UpdateResponse
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryResponse) GetDeleteResponse() *DeleteEntryResponse {
|
||||
if x != nil {
|
||||
if x, ok := x.Response.(*StreamMutateEntryResponse_DeleteResponse); ok {
|
||||
return x.DeleteResponse
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryResponse) GetRenameResponse() *StreamRenameEntryResponse {
|
||||
if x != nil {
|
||||
if x, ok := x.Response.(*StreamMutateEntryResponse_RenameResponse); ok {
|
||||
return x.RenameResponse
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryResponse) GetError() string {
|
||||
if x != nil {
|
||||
return x.Error
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *StreamMutateEntryResponse) GetErrno() int32 {
|
||||
if x != nil {
|
||||
return x.Errno
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type isStreamMutateEntryResponse_Response interface {
|
||||
isStreamMutateEntryResponse_Response()
|
||||
}
|
||||
|
||||
type StreamMutateEntryResponse_CreateResponse struct {
|
||||
CreateResponse *CreateEntryResponse `protobuf:"bytes,3,opt,name=create_response,json=createResponse,proto3,oneof"`
|
||||
}
|
||||
|
||||
type StreamMutateEntryResponse_UpdateResponse struct {
|
||||
UpdateResponse *UpdateEntryResponse `protobuf:"bytes,4,opt,name=update_response,json=updateResponse,proto3,oneof"`
|
||||
}
|
||||
|
||||
type StreamMutateEntryResponse_DeleteResponse struct {
|
||||
DeleteResponse *DeleteEntryResponse `protobuf:"bytes,5,opt,name=delete_response,json=deleteResponse,proto3,oneof"`
|
||||
}
|
||||
|
||||
type StreamMutateEntryResponse_RenameResponse struct {
|
||||
RenameResponse *StreamRenameEntryResponse `protobuf:"bytes,6,opt,name=rename_response,json=renameResponse,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*StreamMutateEntryResponse_CreateResponse) isStreamMutateEntryResponse_Response() {}
|
||||
|
||||
func (*StreamMutateEntryResponse_UpdateResponse) isStreamMutateEntryResponse_Response() {}
|
||||
|
||||
func (*StreamMutateEntryResponse_DeleteResponse) isStreamMutateEntryResponse_Response() {}
|
||||
|
||||
func (*StreamMutateEntryResponse_RenameResponse) isStreamMutateEntryResponse_Response() {}
|
||||
|
||||
// if found, send the exact address
|
||||
// if not found, send the full list of existing brokers
|
||||
type LocateBrokerResponse_Resource struct {
|
||||
@@ -4258,7 +4594,7 @@ type LocateBrokerResponse_Resource struct {
|
||||
|
||||
func (x *LocateBrokerResponse_Resource) Reset() {
|
||||
*x = LocateBrokerResponse_Resource{}
|
||||
mi := &file_filer_proto_msgTypes[69]
|
||||
mi := &file_filer_proto_msgTypes[71]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -4270,7 +4606,7 @@ func (x *LocateBrokerResponse_Resource) String() string {
|
||||
func (*LocateBrokerResponse_Resource) ProtoMessage() {}
|
||||
|
||||
func (x *LocateBrokerResponse_Resource) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_filer_proto_msgTypes[69]
|
||||
mi := &file_filer_proto_msgTypes[71]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -4324,7 +4660,7 @@ type FilerConf_PathConf struct {
|
||||
|
||||
func (x *FilerConf_PathConf) Reset() {
|
||||
*x = FilerConf_PathConf{}
|
||||
mi := &file_filer_proto_msgTypes[70]
|
||||
mi := &file_filer_proto_msgTypes[72]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -4336,7 +4672,7 @@ func (x *FilerConf_PathConf) String() string {
|
||||
func (*FilerConf_PathConf) ProtoMessage() {}
|
||||
|
||||
func (x *FilerConf_PathConf) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_filer_proto_msgTypes[70]
|
||||
mi := &file_filer_proto_msgTypes[72]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -4571,10 +4907,12 @@ const file_filer_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"signatures\x18\x05 \x03(\x05R\n" +
|
||||
"signatures\x12=\n" +
|
||||
"\x1bskip_check_parent_directory\x18\x06 \x01(\bR\x18skipCheckParentDirectory\"w\n" +
|
||||
"\x1bskip_check_parent_directory\x18\x06 \x01(\bR\x18skipCheckParentDirectory\"\xac\x01\n" +
|
||||
"\x13CreateEntryResponse\x12\x14\n" +
|
||||
"\x05error\x18\x01 \x01(\tR\x05error\x12J\n" +
|
||||
"\x0emetadata_event\x18\x02 \x01(\v2#.filer_pb.SubscribeMetadataResponseR\rmetadataEvent\"\xd2\x02\n" +
|
||||
"\x0emetadata_event\x18\x02 \x01(\v2#.filer_pb.SubscribeMetadataResponseR\rmetadataEvent\x123\n" +
|
||||
"\n" +
|
||||
"error_code\x18\x03 \x01(\x0e2\x14.filer_pb.FilerErrorR\terrorCode\"\xd2\x02\n" +
|
||||
"\x12UpdateEntryRequest\x12\x1c\n" +
|
||||
"\tdirectory\x18\x01 \x01(\tR\tdirectory\x12%\n" +
|
||||
"\x05entry\x18\x02 \x01(\v2\x0f.filer_pb.EntryR\x05entry\x121\n" +
|
||||
@@ -4845,13 +5183,41 @@ const file_filer_proto_rawDesc = "" +
|
||||
"\x05owner\x18\x04 \x01(\tR\x05owner\"<\n" +
|
||||
"\x14TransferLocksRequest\x12$\n" +
|
||||
"\x05locks\x18\x01 \x03(\v2\x0e.filer_pb.LockR\x05locks\"\x17\n" +
|
||||
"\x15TransferLocksResponse*7\n" +
|
||||
"\x15TransferLocksResponse\"\xe6\x02\n" +
|
||||
"\x18StreamMutateEntryRequest\x12\x1d\n" +
|
||||
"\n" +
|
||||
"request_id\x18\x01 \x01(\x04R\trequestId\x12E\n" +
|
||||
"\x0ecreate_request\x18\x02 \x01(\v2\x1c.filer_pb.CreateEntryRequestH\x00R\rcreateRequest\x12E\n" +
|
||||
"\x0eupdate_request\x18\x03 \x01(\v2\x1c.filer_pb.UpdateEntryRequestH\x00R\rupdateRequest\x12E\n" +
|
||||
"\x0edelete_request\x18\x04 \x01(\v2\x1c.filer_pb.DeleteEntryRequestH\x00R\rdeleteRequest\x12K\n" +
|
||||
"\x0erename_request\x18\x05 \x01(\v2\".filer_pb.StreamRenameEntryRequestH\x00R\rrenameRequestB\t\n" +
|
||||
"\arequest\"\xb9\x03\n" +
|
||||
"\x19StreamMutateEntryResponse\x12\x1d\n" +
|
||||
"\n" +
|
||||
"request_id\x18\x01 \x01(\x04R\trequestId\x12\x17\n" +
|
||||
"\ais_last\x18\x02 \x01(\bR\x06isLast\x12H\n" +
|
||||
"\x0fcreate_response\x18\x03 \x01(\v2\x1d.filer_pb.CreateEntryResponseH\x00R\x0ecreateResponse\x12H\n" +
|
||||
"\x0fupdate_response\x18\x04 \x01(\v2\x1d.filer_pb.UpdateEntryResponseH\x00R\x0eupdateResponse\x12H\n" +
|
||||
"\x0fdelete_response\x18\x05 \x01(\v2\x1d.filer_pb.DeleteEntryResponseH\x00R\x0edeleteResponse\x12N\n" +
|
||||
"\x0frename_response\x18\x06 \x01(\v2#.filer_pb.StreamRenameEntryResponseH\x00R\x0erenameResponse\x12\x14\n" +
|
||||
"\x05error\x18\a \x01(\tR\x05error\x12\x14\n" +
|
||||
"\x05errno\x18\b \x01(\x05R\x05errnoB\n" +
|
||||
"\n" +
|
||||
"\bresponse*7\n" +
|
||||
"\aSSEType\x12\b\n" +
|
||||
"\x04NONE\x10\x00\x12\t\n" +
|
||||
"\x05SSE_C\x10\x01\x12\v\n" +
|
||||
"\aSSE_KMS\x10\x02\x12\n" +
|
||||
"\n" +
|
||||
"\x06SSE_S3\x10\x032\xf7\x10\n" +
|
||||
"\x06SSE_S3\x10\x03*\x8c\x01\n" +
|
||||
"\n" +
|
||||
"FilerError\x12\x06\n" +
|
||||
"\x02OK\x10\x00\x12\x17\n" +
|
||||
"\x13ENTRY_NAME_TOO_LONG\x10\x01\x12\x12\n" +
|
||||
"\x0ePARENT_IS_FILE\x10\x02\x12\x19\n" +
|
||||
"\x15EXISTING_IS_DIRECTORY\x10\x03\x12\x14\n" +
|
||||
"\x10EXISTING_IS_FILE\x10\x04\x12\x18\n" +
|
||||
"\x14ENTRY_ALREADY_EXISTS\x10\x052\xdb\x11\n" +
|
||||
"\fSeaweedFiler\x12g\n" +
|
||||
"\x14LookupDirectoryEntry\x12%.filer_pb.LookupDirectoryEntryRequest\x1a&.filer_pb.LookupDirectoryEntryResponse\"\x00\x12N\n" +
|
||||
"\vListEntries\x12\x1c.filer_pb.ListEntriesRequest\x1a\x1d.filer_pb.ListEntriesResponse\"\x000\x01\x12L\n" +
|
||||
@@ -4860,7 +5226,8 @@ const file_filer_proto_rawDesc = "" +
|
||||
"\rAppendToEntry\x12\x1e.filer_pb.AppendToEntryRequest\x1a\x1f.filer_pb.AppendToEntryResponse\"\x00\x12L\n" +
|
||||
"\vDeleteEntry\x12\x1c.filer_pb.DeleteEntryRequest\x1a\x1d.filer_pb.DeleteEntryResponse\"\x00\x12^\n" +
|
||||
"\x11AtomicRenameEntry\x12\".filer_pb.AtomicRenameEntryRequest\x1a#.filer_pb.AtomicRenameEntryResponse\"\x00\x12`\n" +
|
||||
"\x11StreamRenameEntry\x12\".filer_pb.StreamRenameEntryRequest\x1a#.filer_pb.StreamRenameEntryResponse\"\x000\x01\x12O\n" +
|
||||
"\x11StreamRenameEntry\x12\".filer_pb.StreamRenameEntryRequest\x1a#.filer_pb.StreamRenameEntryResponse\"\x000\x01\x12b\n" +
|
||||
"\x11StreamMutateEntry\x12\".filer_pb.StreamMutateEntryRequest\x1a#.filer_pb.StreamMutateEntryResponse\"\x00(\x010\x01\x12O\n" +
|
||||
"\fAssignVolume\x12\x1d.filer_pb.AssignVolumeRequest\x1a\x1e.filer_pb.AssignVolumeResponse\"\x00\x12O\n" +
|
||||
"\fLookupVolume\x12\x1d.filer_pb.LookupVolumeRequest\x1a\x1e.filer_pb.LookupVolumeResponse\"\x00\x12U\n" +
|
||||
"\x0eCollectionList\x12\x1f.filer_pb.CollectionListRequest\x1a .filer_pb.CollectionListResponse\"\x00\x12[\n" +
|
||||
@@ -4894,171 +5261,185 @@ func file_filer_proto_rawDescGZIP() []byte {
|
||||
return file_filer_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_filer_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_filer_proto_msgTypes = make([]protoimpl.MessageInfo, 71)
|
||||
var file_filer_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
|
||||
var file_filer_proto_msgTypes = make([]protoimpl.MessageInfo, 73)
|
||||
var file_filer_proto_goTypes = []any{
|
||||
(SSEType)(0), // 0: filer_pb.SSEType
|
||||
(*LookupDirectoryEntryRequest)(nil), // 1: filer_pb.LookupDirectoryEntryRequest
|
||||
(*LookupDirectoryEntryResponse)(nil), // 2: filer_pb.LookupDirectoryEntryResponse
|
||||
(*ListEntriesRequest)(nil), // 3: filer_pb.ListEntriesRequest
|
||||
(*ListEntriesResponse)(nil), // 4: filer_pb.ListEntriesResponse
|
||||
(*RemoteEntry)(nil), // 5: filer_pb.RemoteEntry
|
||||
(*Entry)(nil), // 6: filer_pb.Entry
|
||||
(*FullEntry)(nil), // 7: filer_pb.FullEntry
|
||||
(*EventNotification)(nil), // 8: filer_pb.EventNotification
|
||||
(*FileChunk)(nil), // 9: filer_pb.FileChunk
|
||||
(*FileChunkManifest)(nil), // 10: filer_pb.FileChunkManifest
|
||||
(*FileId)(nil), // 11: filer_pb.FileId
|
||||
(*FuseAttributes)(nil), // 12: filer_pb.FuseAttributes
|
||||
(*CreateEntryRequest)(nil), // 13: filer_pb.CreateEntryRequest
|
||||
(*CreateEntryResponse)(nil), // 14: filer_pb.CreateEntryResponse
|
||||
(*UpdateEntryRequest)(nil), // 15: filer_pb.UpdateEntryRequest
|
||||
(*UpdateEntryResponse)(nil), // 16: filer_pb.UpdateEntryResponse
|
||||
(*AppendToEntryRequest)(nil), // 17: filer_pb.AppendToEntryRequest
|
||||
(*AppendToEntryResponse)(nil), // 18: filer_pb.AppendToEntryResponse
|
||||
(*DeleteEntryRequest)(nil), // 19: filer_pb.DeleteEntryRequest
|
||||
(*DeleteEntryResponse)(nil), // 20: filer_pb.DeleteEntryResponse
|
||||
(*AtomicRenameEntryRequest)(nil), // 21: filer_pb.AtomicRenameEntryRequest
|
||||
(*AtomicRenameEntryResponse)(nil), // 22: filer_pb.AtomicRenameEntryResponse
|
||||
(*StreamRenameEntryRequest)(nil), // 23: filer_pb.StreamRenameEntryRequest
|
||||
(*StreamRenameEntryResponse)(nil), // 24: filer_pb.StreamRenameEntryResponse
|
||||
(*AssignVolumeRequest)(nil), // 25: filer_pb.AssignVolumeRequest
|
||||
(*AssignVolumeResponse)(nil), // 26: filer_pb.AssignVolumeResponse
|
||||
(*LookupVolumeRequest)(nil), // 27: filer_pb.LookupVolumeRequest
|
||||
(*Locations)(nil), // 28: filer_pb.Locations
|
||||
(*Location)(nil), // 29: filer_pb.Location
|
||||
(*LookupVolumeResponse)(nil), // 30: filer_pb.LookupVolumeResponse
|
||||
(*Collection)(nil), // 31: filer_pb.Collection
|
||||
(*CollectionListRequest)(nil), // 32: filer_pb.CollectionListRequest
|
||||
(*CollectionListResponse)(nil), // 33: filer_pb.CollectionListResponse
|
||||
(*DeleteCollectionRequest)(nil), // 34: filer_pb.DeleteCollectionRequest
|
||||
(*DeleteCollectionResponse)(nil), // 35: filer_pb.DeleteCollectionResponse
|
||||
(*StatisticsRequest)(nil), // 36: filer_pb.StatisticsRequest
|
||||
(*StatisticsResponse)(nil), // 37: filer_pb.StatisticsResponse
|
||||
(*PingRequest)(nil), // 38: filer_pb.PingRequest
|
||||
(*PingResponse)(nil), // 39: filer_pb.PingResponse
|
||||
(*GetFilerConfigurationRequest)(nil), // 40: filer_pb.GetFilerConfigurationRequest
|
||||
(*GetFilerConfigurationResponse)(nil), // 41: filer_pb.GetFilerConfigurationResponse
|
||||
(*SubscribeMetadataRequest)(nil), // 42: filer_pb.SubscribeMetadataRequest
|
||||
(*SubscribeMetadataResponse)(nil), // 43: filer_pb.SubscribeMetadataResponse
|
||||
(*TraverseBfsMetadataRequest)(nil), // 44: filer_pb.TraverseBfsMetadataRequest
|
||||
(*TraverseBfsMetadataResponse)(nil), // 45: filer_pb.TraverseBfsMetadataResponse
|
||||
(*LogEntry)(nil), // 46: filer_pb.LogEntry
|
||||
(*KeepConnectedRequest)(nil), // 47: filer_pb.KeepConnectedRequest
|
||||
(*KeepConnectedResponse)(nil), // 48: filer_pb.KeepConnectedResponse
|
||||
(*LocateBrokerRequest)(nil), // 49: filer_pb.LocateBrokerRequest
|
||||
(*LocateBrokerResponse)(nil), // 50: filer_pb.LocateBrokerResponse
|
||||
(*KvGetRequest)(nil), // 51: filer_pb.KvGetRequest
|
||||
(*KvGetResponse)(nil), // 52: filer_pb.KvGetResponse
|
||||
(*KvPutRequest)(nil), // 53: filer_pb.KvPutRequest
|
||||
(*KvPutResponse)(nil), // 54: filer_pb.KvPutResponse
|
||||
(*FilerConf)(nil), // 55: filer_pb.FilerConf
|
||||
(*CacheRemoteObjectToLocalClusterRequest)(nil), // 56: filer_pb.CacheRemoteObjectToLocalClusterRequest
|
||||
(*CacheRemoteObjectToLocalClusterResponse)(nil), // 57: filer_pb.CacheRemoteObjectToLocalClusterResponse
|
||||
(*LockRequest)(nil), // 58: filer_pb.LockRequest
|
||||
(*LockResponse)(nil), // 59: filer_pb.LockResponse
|
||||
(*UnlockRequest)(nil), // 60: filer_pb.UnlockRequest
|
||||
(*UnlockResponse)(nil), // 61: filer_pb.UnlockResponse
|
||||
(*FindLockOwnerRequest)(nil), // 62: filer_pb.FindLockOwnerRequest
|
||||
(*FindLockOwnerResponse)(nil), // 63: filer_pb.FindLockOwnerResponse
|
||||
(*Lock)(nil), // 64: filer_pb.Lock
|
||||
(*TransferLocksRequest)(nil), // 65: filer_pb.TransferLocksRequest
|
||||
(*TransferLocksResponse)(nil), // 66: filer_pb.TransferLocksResponse
|
||||
nil, // 67: filer_pb.Entry.ExtendedEntry
|
||||
nil, // 68: filer_pb.UpdateEntryRequest.ExpectedExtendedEntry
|
||||
nil, // 69: filer_pb.LookupVolumeResponse.LocationsMapEntry
|
||||
(*LocateBrokerResponse_Resource)(nil), // 70: filer_pb.LocateBrokerResponse.Resource
|
||||
(*FilerConf_PathConf)(nil), // 71: filer_pb.FilerConf.PathConf
|
||||
(FilerError)(0), // 1: filer_pb.FilerError
|
||||
(*LookupDirectoryEntryRequest)(nil), // 2: filer_pb.LookupDirectoryEntryRequest
|
||||
(*LookupDirectoryEntryResponse)(nil), // 3: filer_pb.LookupDirectoryEntryResponse
|
||||
(*ListEntriesRequest)(nil), // 4: filer_pb.ListEntriesRequest
|
||||
(*ListEntriesResponse)(nil), // 5: filer_pb.ListEntriesResponse
|
||||
(*RemoteEntry)(nil), // 6: filer_pb.RemoteEntry
|
||||
(*Entry)(nil), // 7: filer_pb.Entry
|
||||
(*FullEntry)(nil), // 8: filer_pb.FullEntry
|
||||
(*EventNotification)(nil), // 9: filer_pb.EventNotification
|
||||
(*FileChunk)(nil), // 10: filer_pb.FileChunk
|
||||
(*FileChunkManifest)(nil), // 11: filer_pb.FileChunkManifest
|
||||
(*FileId)(nil), // 12: filer_pb.FileId
|
||||
(*FuseAttributes)(nil), // 13: filer_pb.FuseAttributes
|
||||
(*CreateEntryRequest)(nil), // 14: filer_pb.CreateEntryRequest
|
||||
(*CreateEntryResponse)(nil), // 15: filer_pb.CreateEntryResponse
|
||||
(*UpdateEntryRequest)(nil), // 16: filer_pb.UpdateEntryRequest
|
||||
(*UpdateEntryResponse)(nil), // 17: filer_pb.UpdateEntryResponse
|
||||
(*AppendToEntryRequest)(nil), // 18: filer_pb.AppendToEntryRequest
|
||||
(*AppendToEntryResponse)(nil), // 19: filer_pb.AppendToEntryResponse
|
||||
(*DeleteEntryRequest)(nil), // 20: filer_pb.DeleteEntryRequest
|
||||
(*DeleteEntryResponse)(nil), // 21: filer_pb.DeleteEntryResponse
|
||||
(*AtomicRenameEntryRequest)(nil), // 22: filer_pb.AtomicRenameEntryRequest
|
||||
(*AtomicRenameEntryResponse)(nil), // 23: filer_pb.AtomicRenameEntryResponse
|
||||
(*StreamRenameEntryRequest)(nil), // 24: filer_pb.StreamRenameEntryRequest
|
||||
(*StreamRenameEntryResponse)(nil), // 25: filer_pb.StreamRenameEntryResponse
|
||||
(*AssignVolumeRequest)(nil), // 26: filer_pb.AssignVolumeRequest
|
||||
(*AssignVolumeResponse)(nil), // 27: filer_pb.AssignVolumeResponse
|
||||
(*LookupVolumeRequest)(nil), // 28: filer_pb.LookupVolumeRequest
|
||||
(*Locations)(nil), // 29: filer_pb.Locations
|
||||
(*Location)(nil), // 30: filer_pb.Location
|
||||
(*LookupVolumeResponse)(nil), // 31: filer_pb.LookupVolumeResponse
|
||||
(*Collection)(nil), // 32: filer_pb.Collection
|
||||
(*CollectionListRequest)(nil), // 33: filer_pb.CollectionListRequest
|
||||
(*CollectionListResponse)(nil), // 34: filer_pb.CollectionListResponse
|
||||
(*DeleteCollectionRequest)(nil), // 35: filer_pb.DeleteCollectionRequest
|
||||
(*DeleteCollectionResponse)(nil), // 36: filer_pb.DeleteCollectionResponse
|
||||
(*StatisticsRequest)(nil), // 37: filer_pb.StatisticsRequest
|
||||
(*StatisticsResponse)(nil), // 38: filer_pb.StatisticsResponse
|
||||
(*PingRequest)(nil), // 39: filer_pb.PingRequest
|
||||
(*PingResponse)(nil), // 40: filer_pb.PingResponse
|
||||
(*GetFilerConfigurationRequest)(nil), // 41: filer_pb.GetFilerConfigurationRequest
|
||||
(*GetFilerConfigurationResponse)(nil), // 42: filer_pb.GetFilerConfigurationResponse
|
||||
(*SubscribeMetadataRequest)(nil), // 43: filer_pb.SubscribeMetadataRequest
|
||||
(*SubscribeMetadataResponse)(nil), // 44: filer_pb.SubscribeMetadataResponse
|
||||
(*TraverseBfsMetadataRequest)(nil), // 45: filer_pb.TraverseBfsMetadataRequest
|
||||
(*TraverseBfsMetadataResponse)(nil), // 46: filer_pb.TraverseBfsMetadataResponse
|
||||
(*LogEntry)(nil), // 47: filer_pb.LogEntry
|
||||
(*KeepConnectedRequest)(nil), // 48: filer_pb.KeepConnectedRequest
|
||||
(*KeepConnectedResponse)(nil), // 49: filer_pb.KeepConnectedResponse
|
||||
(*LocateBrokerRequest)(nil), // 50: filer_pb.LocateBrokerRequest
|
||||
(*LocateBrokerResponse)(nil), // 51: filer_pb.LocateBrokerResponse
|
||||
(*KvGetRequest)(nil), // 52: filer_pb.KvGetRequest
|
||||
(*KvGetResponse)(nil), // 53: filer_pb.KvGetResponse
|
||||
(*KvPutRequest)(nil), // 54: filer_pb.KvPutRequest
|
||||
(*KvPutResponse)(nil), // 55: filer_pb.KvPutResponse
|
||||
(*FilerConf)(nil), // 56: filer_pb.FilerConf
|
||||
(*CacheRemoteObjectToLocalClusterRequest)(nil), // 57: filer_pb.CacheRemoteObjectToLocalClusterRequest
|
||||
(*CacheRemoteObjectToLocalClusterResponse)(nil), // 58: filer_pb.CacheRemoteObjectToLocalClusterResponse
|
||||
(*LockRequest)(nil), // 59: filer_pb.LockRequest
|
||||
(*LockResponse)(nil), // 60: filer_pb.LockResponse
|
||||
(*UnlockRequest)(nil), // 61: filer_pb.UnlockRequest
|
||||
(*UnlockResponse)(nil), // 62: filer_pb.UnlockResponse
|
||||
(*FindLockOwnerRequest)(nil), // 63: filer_pb.FindLockOwnerRequest
|
||||
(*FindLockOwnerResponse)(nil), // 64: filer_pb.FindLockOwnerResponse
|
||||
(*Lock)(nil), // 65: filer_pb.Lock
|
||||
(*TransferLocksRequest)(nil), // 66: filer_pb.TransferLocksRequest
|
||||
(*TransferLocksResponse)(nil), // 67: filer_pb.TransferLocksResponse
|
||||
(*StreamMutateEntryRequest)(nil), // 68: filer_pb.StreamMutateEntryRequest
|
||||
(*StreamMutateEntryResponse)(nil), // 69: filer_pb.StreamMutateEntryResponse
|
||||
nil, // 70: filer_pb.Entry.ExtendedEntry
|
||||
nil, // 71: filer_pb.UpdateEntryRequest.ExpectedExtendedEntry
|
||||
nil, // 72: filer_pb.LookupVolumeResponse.LocationsMapEntry
|
||||
(*LocateBrokerResponse_Resource)(nil), // 73: filer_pb.LocateBrokerResponse.Resource
|
||||
(*FilerConf_PathConf)(nil), // 74: filer_pb.FilerConf.PathConf
|
||||
}
|
||||
var file_filer_proto_depIdxs = []int32{
|
||||
6, // 0: filer_pb.LookupDirectoryEntryResponse.entry:type_name -> filer_pb.Entry
|
||||
6, // 1: filer_pb.ListEntriesResponse.entry:type_name -> filer_pb.Entry
|
||||
9, // 2: filer_pb.Entry.chunks:type_name -> filer_pb.FileChunk
|
||||
12, // 3: filer_pb.Entry.attributes:type_name -> filer_pb.FuseAttributes
|
||||
67, // 4: filer_pb.Entry.extended:type_name -> filer_pb.Entry.ExtendedEntry
|
||||
5, // 5: filer_pb.Entry.remote_entry:type_name -> filer_pb.RemoteEntry
|
||||
6, // 6: filer_pb.FullEntry.entry:type_name -> filer_pb.Entry
|
||||
6, // 7: filer_pb.EventNotification.old_entry:type_name -> filer_pb.Entry
|
||||
6, // 8: filer_pb.EventNotification.new_entry:type_name -> filer_pb.Entry
|
||||
11, // 9: filer_pb.FileChunk.fid:type_name -> filer_pb.FileId
|
||||
11, // 10: filer_pb.FileChunk.source_fid:type_name -> filer_pb.FileId
|
||||
7, // 0: filer_pb.LookupDirectoryEntryResponse.entry:type_name -> filer_pb.Entry
|
||||
7, // 1: filer_pb.ListEntriesResponse.entry:type_name -> filer_pb.Entry
|
||||
10, // 2: filer_pb.Entry.chunks:type_name -> filer_pb.FileChunk
|
||||
13, // 3: filer_pb.Entry.attributes:type_name -> filer_pb.FuseAttributes
|
||||
70, // 4: filer_pb.Entry.extended:type_name -> filer_pb.Entry.ExtendedEntry
|
||||
6, // 5: filer_pb.Entry.remote_entry:type_name -> filer_pb.RemoteEntry
|
||||
7, // 6: filer_pb.FullEntry.entry:type_name -> filer_pb.Entry
|
||||
7, // 7: filer_pb.EventNotification.old_entry:type_name -> filer_pb.Entry
|
||||
7, // 8: filer_pb.EventNotification.new_entry:type_name -> filer_pb.Entry
|
||||
12, // 9: filer_pb.FileChunk.fid:type_name -> filer_pb.FileId
|
||||
12, // 10: filer_pb.FileChunk.source_fid:type_name -> filer_pb.FileId
|
||||
0, // 11: filer_pb.FileChunk.sse_type:type_name -> filer_pb.SSEType
|
||||
9, // 12: filer_pb.FileChunkManifest.chunks:type_name -> filer_pb.FileChunk
|
||||
6, // 13: filer_pb.CreateEntryRequest.entry:type_name -> filer_pb.Entry
|
||||
43, // 14: filer_pb.CreateEntryResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
|
||||
6, // 15: filer_pb.UpdateEntryRequest.entry:type_name -> filer_pb.Entry
|
||||
68, // 16: filer_pb.UpdateEntryRequest.expected_extended:type_name -> filer_pb.UpdateEntryRequest.ExpectedExtendedEntry
|
||||
43, // 17: filer_pb.UpdateEntryResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
|
||||
9, // 18: filer_pb.AppendToEntryRequest.chunks:type_name -> filer_pb.FileChunk
|
||||
43, // 19: filer_pb.DeleteEntryResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
|
||||
8, // 20: filer_pb.StreamRenameEntryResponse.event_notification:type_name -> filer_pb.EventNotification
|
||||
29, // 21: filer_pb.AssignVolumeResponse.location:type_name -> filer_pb.Location
|
||||
29, // 22: filer_pb.Locations.locations:type_name -> filer_pb.Location
|
||||
69, // 23: filer_pb.LookupVolumeResponse.locations_map:type_name -> filer_pb.LookupVolumeResponse.LocationsMapEntry
|
||||
31, // 24: filer_pb.CollectionListResponse.collections:type_name -> filer_pb.Collection
|
||||
8, // 25: filer_pb.SubscribeMetadataResponse.event_notification:type_name -> filer_pb.EventNotification
|
||||
6, // 26: filer_pb.TraverseBfsMetadataResponse.entry:type_name -> filer_pb.Entry
|
||||
70, // 27: filer_pb.LocateBrokerResponse.resources:type_name -> filer_pb.LocateBrokerResponse.Resource
|
||||
71, // 28: filer_pb.FilerConf.locations:type_name -> filer_pb.FilerConf.PathConf
|
||||
6, // 29: filer_pb.CacheRemoteObjectToLocalClusterResponse.entry:type_name -> filer_pb.Entry
|
||||
43, // 30: filer_pb.CacheRemoteObjectToLocalClusterResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
|
||||
64, // 31: filer_pb.TransferLocksRequest.locks:type_name -> filer_pb.Lock
|
||||
28, // 32: filer_pb.LookupVolumeResponse.LocationsMapEntry.value:type_name -> filer_pb.Locations
|
||||
1, // 33: filer_pb.SeaweedFiler.LookupDirectoryEntry:input_type -> filer_pb.LookupDirectoryEntryRequest
|
||||
3, // 34: filer_pb.SeaweedFiler.ListEntries:input_type -> filer_pb.ListEntriesRequest
|
||||
13, // 35: filer_pb.SeaweedFiler.CreateEntry:input_type -> filer_pb.CreateEntryRequest
|
||||
15, // 36: filer_pb.SeaweedFiler.UpdateEntry:input_type -> filer_pb.UpdateEntryRequest
|
||||
17, // 37: filer_pb.SeaweedFiler.AppendToEntry:input_type -> filer_pb.AppendToEntryRequest
|
||||
19, // 38: filer_pb.SeaweedFiler.DeleteEntry:input_type -> filer_pb.DeleteEntryRequest
|
||||
21, // 39: filer_pb.SeaweedFiler.AtomicRenameEntry:input_type -> filer_pb.AtomicRenameEntryRequest
|
||||
23, // 40: filer_pb.SeaweedFiler.StreamRenameEntry:input_type -> filer_pb.StreamRenameEntryRequest
|
||||
25, // 41: filer_pb.SeaweedFiler.AssignVolume:input_type -> filer_pb.AssignVolumeRequest
|
||||
27, // 42: filer_pb.SeaweedFiler.LookupVolume:input_type -> filer_pb.LookupVolumeRequest
|
||||
32, // 43: filer_pb.SeaweedFiler.CollectionList:input_type -> filer_pb.CollectionListRequest
|
||||
34, // 44: filer_pb.SeaweedFiler.DeleteCollection:input_type -> filer_pb.DeleteCollectionRequest
|
||||
36, // 45: filer_pb.SeaweedFiler.Statistics:input_type -> filer_pb.StatisticsRequest
|
||||
38, // 46: filer_pb.SeaweedFiler.Ping:input_type -> filer_pb.PingRequest
|
||||
40, // 47: filer_pb.SeaweedFiler.GetFilerConfiguration:input_type -> filer_pb.GetFilerConfigurationRequest
|
||||
44, // 48: filer_pb.SeaweedFiler.TraverseBfsMetadata:input_type -> filer_pb.TraverseBfsMetadataRequest
|
||||
42, // 49: filer_pb.SeaweedFiler.SubscribeMetadata:input_type -> filer_pb.SubscribeMetadataRequest
|
||||
42, // 50: filer_pb.SeaweedFiler.SubscribeLocalMetadata:input_type -> filer_pb.SubscribeMetadataRequest
|
||||
51, // 51: filer_pb.SeaweedFiler.KvGet:input_type -> filer_pb.KvGetRequest
|
||||
53, // 52: filer_pb.SeaweedFiler.KvPut:input_type -> filer_pb.KvPutRequest
|
||||
56, // 53: filer_pb.SeaweedFiler.CacheRemoteObjectToLocalCluster:input_type -> filer_pb.CacheRemoteObjectToLocalClusterRequest
|
||||
58, // 54: filer_pb.SeaweedFiler.DistributedLock:input_type -> filer_pb.LockRequest
|
||||
60, // 55: filer_pb.SeaweedFiler.DistributedUnlock:input_type -> filer_pb.UnlockRequest
|
||||
62, // 56: filer_pb.SeaweedFiler.FindLockOwner:input_type -> filer_pb.FindLockOwnerRequest
|
||||
65, // 57: filer_pb.SeaweedFiler.TransferLocks:input_type -> filer_pb.TransferLocksRequest
|
||||
2, // 58: filer_pb.SeaweedFiler.LookupDirectoryEntry:output_type -> filer_pb.LookupDirectoryEntryResponse
|
||||
4, // 59: filer_pb.SeaweedFiler.ListEntries:output_type -> filer_pb.ListEntriesResponse
|
||||
14, // 60: filer_pb.SeaweedFiler.CreateEntry:output_type -> filer_pb.CreateEntryResponse
|
||||
16, // 61: filer_pb.SeaweedFiler.UpdateEntry:output_type -> filer_pb.UpdateEntryResponse
|
||||
18, // 62: filer_pb.SeaweedFiler.AppendToEntry:output_type -> filer_pb.AppendToEntryResponse
|
||||
20, // 63: filer_pb.SeaweedFiler.DeleteEntry:output_type -> filer_pb.DeleteEntryResponse
|
||||
22, // 64: filer_pb.SeaweedFiler.AtomicRenameEntry:output_type -> filer_pb.AtomicRenameEntryResponse
|
||||
24, // 65: filer_pb.SeaweedFiler.StreamRenameEntry:output_type -> filer_pb.StreamRenameEntryResponse
|
||||
26, // 66: filer_pb.SeaweedFiler.AssignVolume:output_type -> filer_pb.AssignVolumeResponse
|
||||
30, // 67: filer_pb.SeaweedFiler.LookupVolume:output_type -> filer_pb.LookupVolumeResponse
|
||||
33, // 68: filer_pb.SeaweedFiler.CollectionList:output_type -> filer_pb.CollectionListResponse
|
||||
35, // 69: filer_pb.SeaweedFiler.DeleteCollection:output_type -> filer_pb.DeleteCollectionResponse
|
||||
37, // 70: filer_pb.SeaweedFiler.Statistics:output_type -> filer_pb.StatisticsResponse
|
||||
39, // 71: filer_pb.SeaweedFiler.Ping:output_type -> filer_pb.PingResponse
|
||||
41, // 72: filer_pb.SeaweedFiler.GetFilerConfiguration:output_type -> filer_pb.GetFilerConfigurationResponse
|
||||
45, // 73: filer_pb.SeaweedFiler.TraverseBfsMetadata:output_type -> filer_pb.TraverseBfsMetadataResponse
|
||||
43, // 74: filer_pb.SeaweedFiler.SubscribeMetadata:output_type -> filer_pb.SubscribeMetadataResponse
|
||||
43, // 75: filer_pb.SeaweedFiler.SubscribeLocalMetadata:output_type -> filer_pb.SubscribeMetadataResponse
|
||||
52, // 76: filer_pb.SeaweedFiler.KvGet:output_type -> filer_pb.KvGetResponse
|
||||
54, // 77: filer_pb.SeaweedFiler.KvPut:output_type -> filer_pb.KvPutResponse
|
||||
57, // 78: filer_pb.SeaweedFiler.CacheRemoteObjectToLocalCluster:output_type -> filer_pb.CacheRemoteObjectToLocalClusterResponse
|
||||
59, // 79: filer_pb.SeaweedFiler.DistributedLock:output_type -> filer_pb.LockResponse
|
||||
61, // 80: filer_pb.SeaweedFiler.DistributedUnlock:output_type -> filer_pb.UnlockResponse
|
||||
63, // 81: filer_pb.SeaweedFiler.FindLockOwner:output_type -> filer_pb.FindLockOwnerResponse
|
||||
66, // 82: filer_pb.SeaweedFiler.TransferLocks:output_type -> filer_pb.TransferLocksResponse
|
||||
58, // [58:83] is the sub-list for method output_type
|
||||
33, // [33:58] is the sub-list for method input_type
|
||||
33, // [33:33] is the sub-list for extension type_name
|
||||
33, // [33:33] is the sub-list for extension extendee
|
||||
0, // [0:33] is the sub-list for field type_name
|
||||
10, // 12: filer_pb.FileChunkManifest.chunks:type_name -> filer_pb.FileChunk
|
||||
7, // 13: filer_pb.CreateEntryRequest.entry:type_name -> filer_pb.Entry
|
||||
44, // 14: filer_pb.CreateEntryResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
|
||||
1, // 15: filer_pb.CreateEntryResponse.error_code:type_name -> filer_pb.FilerError
|
||||
7, // 16: filer_pb.UpdateEntryRequest.entry:type_name -> filer_pb.Entry
|
||||
71, // 17: filer_pb.UpdateEntryRequest.expected_extended:type_name -> filer_pb.UpdateEntryRequest.ExpectedExtendedEntry
|
||||
44, // 18: filer_pb.UpdateEntryResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
|
||||
10, // 19: filer_pb.AppendToEntryRequest.chunks:type_name -> filer_pb.FileChunk
|
||||
44, // 20: filer_pb.DeleteEntryResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
|
||||
9, // 21: filer_pb.StreamRenameEntryResponse.event_notification:type_name -> filer_pb.EventNotification
|
||||
30, // 22: filer_pb.AssignVolumeResponse.location:type_name -> filer_pb.Location
|
||||
30, // 23: filer_pb.Locations.locations:type_name -> filer_pb.Location
|
||||
72, // 24: filer_pb.LookupVolumeResponse.locations_map:type_name -> filer_pb.LookupVolumeResponse.LocationsMapEntry
|
||||
32, // 25: filer_pb.CollectionListResponse.collections:type_name -> filer_pb.Collection
|
||||
9, // 26: filer_pb.SubscribeMetadataResponse.event_notification:type_name -> filer_pb.EventNotification
|
||||
7, // 27: filer_pb.TraverseBfsMetadataResponse.entry:type_name -> filer_pb.Entry
|
||||
73, // 28: filer_pb.LocateBrokerResponse.resources:type_name -> filer_pb.LocateBrokerResponse.Resource
|
||||
74, // 29: filer_pb.FilerConf.locations:type_name -> filer_pb.FilerConf.PathConf
|
||||
7, // 30: filer_pb.CacheRemoteObjectToLocalClusterResponse.entry:type_name -> filer_pb.Entry
|
||||
44, // 31: filer_pb.CacheRemoteObjectToLocalClusterResponse.metadata_event:type_name -> filer_pb.SubscribeMetadataResponse
|
||||
65, // 32: filer_pb.TransferLocksRequest.locks:type_name -> filer_pb.Lock
|
||||
14, // 33: filer_pb.StreamMutateEntryRequest.create_request:type_name -> filer_pb.CreateEntryRequest
|
||||
16, // 34: filer_pb.StreamMutateEntryRequest.update_request:type_name -> filer_pb.UpdateEntryRequest
|
||||
20, // 35: filer_pb.StreamMutateEntryRequest.delete_request:type_name -> filer_pb.DeleteEntryRequest
|
||||
24, // 36: filer_pb.StreamMutateEntryRequest.rename_request:type_name -> filer_pb.StreamRenameEntryRequest
|
||||
15, // 37: filer_pb.StreamMutateEntryResponse.create_response:type_name -> filer_pb.CreateEntryResponse
|
||||
17, // 38: filer_pb.StreamMutateEntryResponse.update_response:type_name -> filer_pb.UpdateEntryResponse
|
||||
21, // 39: filer_pb.StreamMutateEntryResponse.delete_response:type_name -> filer_pb.DeleteEntryResponse
|
||||
25, // 40: filer_pb.StreamMutateEntryResponse.rename_response:type_name -> filer_pb.StreamRenameEntryResponse
|
||||
29, // 41: filer_pb.LookupVolumeResponse.LocationsMapEntry.value:type_name -> filer_pb.Locations
|
||||
2, // 42: filer_pb.SeaweedFiler.LookupDirectoryEntry:input_type -> filer_pb.LookupDirectoryEntryRequest
|
||||
4, // 43: filer_pb.SeaweedFiler.ListEntries:input_type -> filer_pb.ListEntriesRequest
|
||||
14, // 44: filer_pb.SeaweedFiler.CreateEntry:input_type -> filer_pb.CreateEntryRequest
|
||||
16, // 45: filer_pb.SeaweedFiler.UpdateEntry:input_type -> filer_pb.UpdateEntryRequest
|
||||
18, // 46: filer_pb.SeaweedFiler.AppendToEntry:input_type -> filer_pb.AppendToEntryRequest
|
||||
20, // 47: filer_pb.SeaweedFiler.DeleteEntry:input_type -> filer_pb.DeleteEntryRequest
|
||||
22, // 48: filer_pb.SeaweedFiler.AtomicRenameEntry:input_type -> filer_pb.AtomicRenameEntryRequest
|
||||
24, // 49: filer_pb.SeaweedFiler.StreamRenameEntry:input_type -> filer_pb.StreamRenameEntryRequest
|
||||
68, // 50: filer_pb.SeaweedFiler.StreamMutateEntry:input_type -> filer_pb.StreamMutateEntryRequest
|
||||
26, // 51: filer_pb.SeaweedFiler.AssignVolume:input_type -> filer_pb.AssignVolumeRequest
|
||||
28, // 52: filer_pb.SeaweedFiler.LookupVolume:input_type -> filer_pb.LookupVolumeRequest
|
||||
33, // 53: filer_pb.SeaweedFiler.CollectionList:input_type -> filer_pb.CollectionListRequest
|
||||
35, // 54: filer_pb.SeaweedFiler.DeleteCollection:input_type -> filer_pb.DeleteCollectionRequest
|
||||
37, // 55: filer_pb.SeaweedFiler.Statistics:input_type -> filer_pb.StatisticsRequest
|
||||
39, // 56: filer_pb.SeaweedFiler.Ping:input_type -> filer_pb.PingRequest
|
||||
41, // 57: filer_pb.SeaweedFiler.GetFilerConfiguration:input_type -> filer_pb.GetFilerConfigurationRequest
|
||||
45, // 58: filer_pb.SeaweedFiler.TraverseBfsMetadata:input_type -> filer_pb.TraverseBfsMetadataRequest
|
||||
43, // 59: filer_pb.SeaweedFiler.SubscribeMetadata:input_type -> filer_pb.SubscribeMetadataRequest
|
||||
43, // 60: filer_pb.SeaweedFiler.SubscribeLocalMetadata:input_type -> filer_pb.SubscribeMetadataRequest
|
||||
52, // 61: filer_pb.SeaweedFiler.KvGet:input_type -> filer_pb.KvGetRequest
|
||||
54, // 62: filer_pb.SeaweedFiler.KvPut:input_type -> filer_pb.KvPutRequest
|
||||
57, // 63: filer_pb.SeaweedFiler.CacheRemoteObjectToLocalCluster:input_type -> filer_pb.CacheRemoteObjectToLocalClusterRequest
|
||||
59, // 64: filer_pb.SeaweedFiler.DistributedLock:input_type -> filer_pb.LockRequest
|
||||
61, // 65: filer_pb.SeaweedFiler.DistributedUnlock:input_type -> filer_pb.UnlockRequest
|
||||
63, // 66: filer_pb.SeaweedFiler.FindLockOwner:input_type -> filer_pb.FindLockOwnerRequest
|
||||
66, // 67: filer_pb.SeaweedFiler.TransferLocks:input_type -> filer_pb.TransferLocksRequest
|
||||
3, // 68: filer_pb.SeaweedFiler.LookupDirectoryEntry:output_type -> filer_pb.LookupDirectoryEntryResponse
|
||||
5, // 69: filer_pb.SeaweedFiler.ListEntries:output_type -> filer_pb.ListEntriesResponse
|
||||
15, // 70: filer_pb.SeaweedFiler.CreateEntry:output_type -> filer_pb.CreateEntryResponse
|
||||
17, // 71: filer_pb.SeaweedFiler.UpdateEntry:output_type -> filer_pb.UpdateEntryResponse
|
||||
19, // 72: filer_pb.SeaweedFiler.AppendToEntry:output_type -> filer_pb.AppendToEntryResponse
|
||||
21, // 73: filer_pb.SeaweedFiler.DeleteEntry:output_type -> filer_pb.DeleteEntryResponse
|
||||
23, // 74: filer_pb.SeaweedFiler.AtomicRenameEntry:output_type -> filer_pb.AtomicRenameEntryResponse
|
||||
25, // 75: filer_pb.SeaweedFiler.StreamRenameEntry:output_type -> filer_pb.StreamRenameEntryResponse
|
||||
69, // 76: filer_pb.SeaweedFiler.StreamMutateEntry:output_type -> filer_pb.StreamMutateEntryResponse
|
||||
27, // 77: filer_pb.SeaweedFiler.AssignVolume:output_type -> filer_pb.AssignVolumeResponse
|
||||
31, // 78: filer_pb.SeaweedFiler.LookupVolume:output_type -> filer_pb.LookupVolumeResponse
|
||||
34, // 79: filer_pb.SeaweedFiler.CollectionList:output_type -> filer_pb.CollectionListResponse
|
||||
36, // 80: filer_pb.SeaweedFiler.DeleteCollection:output_type -> filer_pb.DeleteCollectionResponse
|
||||
38, // 81: filer_pb.SeaweedFiler.Statistics:output_type -> filer_pb.StatisticsResponse
|
||||
40, // 82: filer_pb.SeaweedFiler.Ping:output_type -> filer_pb.PingResponse
|
||||
42, // 83: filer_pb.SeaweedFiler.GetFilerConfiguration:output_type -> filer_pb.GetFilerConfigurationResponse
|
||||
46, // 84: filer_pb.SeaweedFiler.TraverseBfsMetadata:output_type -> filer_pb.TraverseBfsMetadataResponse
|
||||
44, // 85: filer_pb.SeaweedFiler.SubscribeMetadata:output_type -> filer_pb.SubscribeMetadataResponse
|
||||
44, // 86: filer_pb.SeaweedFiler.SubscribeLocalMetadata:output_type -> filer_pb.SubscribeMetadataResponse
|
||||
53, // 87: filer_pb.SeaweedFiler.KvGet:output_type -> filer_pb.KvGetResponse
|
||||
55, // 88: filer_pb.SeaweedFiler.KvPut:output_type -> filer_pb.KvPutResponse
|
||||
58, // 89: filer_pb.SeaweedFiler.CacheRemoteObjectToLocalCluster:output_type -> filer_pb.CacheRemoteObjectToLocalClusterResponse
|
||||
60, // 90: filer_pb.SeaweedFiler.DistributedLock:output_type -> filer_pb.LockResponse
|
||||
62, // 91: filer_pb.SeaweedFiler.DistributedUnlock:output_type -> filer_pb.UnlockResponse
|
||||
64, // 92: filer_pb.SeaweedFiler.FindLockOwner:output_type -> filer_pb.FindLockOwnerResponse
|
||||
67, // 93: filer_pb.SeaweedFiler.TransferLocks:output_type -> filer_pb.TransferLocksResponse
|
||||
68, // [68:94] is the sub-list for method output_type
|
||||
42, // [42:68] is the sub-list for method input_type
|
||||
42, // [42:42] is the sub-list for extension type_name
|
||||
42, // [42:42] is the sub-list for extension extendee
|
||||
0, // [0:42] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_filer_proto_init() }
|
||||
@@ -5066,13 +5447,25 @@ func file_filer_proto_init() {
|
||||
if File_filer_proto != nil {
|
||||
return
|
||||
}
|
||||
file_filer_proto_msgTypes[66].OneofWrappers = []any{
|
||||
(*StreamMutateEntryRequest_CreateRequest)(nil),
|
||||
(*StreamMutateEntryRequest_UpdateRequest)(nil),
|
||||
(*StreamMutateEntryRequest_DeleteRequest)(nil),
|
||||
(*StreamMutateEntryRequest_RenameRequest)(nil),
|
||||
}
|
||||
file_filer_proto_msgTypes[67].OneofWrappers = []any{
|
||||
(*StreamMutateEntryResponse_CreateResponse)(nil),
|
||||
(*StreamMutateEntryResponse_UpdateResponse)(nil),
|
||||
(*StreamMutateEntryResponse_DeleteResponse)(nil),
|
||||
(*StreamMutateEntryResponse_RenameResponse)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_filer_proto_rawDesc), len(file_filer_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 71,
|
||||
NumEnums: 2,
|
||||
NumMessages: 73,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@ const (
|
||||
SeaweedFiler_DeleteEntry_FullMethodName = "/filer_pb.SeaweedFiler/DeleteEntry"
|
||||
SeaweedFiler_AtomicRenameEntry_FullMethodName = "/filer_pb.SeaweedFiler/AtomicRenameEntry"
|
||||
SeaweedFiler_StreamRenameEntry_FullMethodName = "/filer_pb.SeaweedFiler/StreamRenameEntry"
|
||||
SeaweedFiler_StreamMutateEntry_FullMethodName = "/filer_pb.SeaweedFiler/StreamMutateEntry"
|
||||
SeaweedFiler_AssignVolume_FullMethodName = "/filer_pb.SeaweedFiler/AssignVolume"
|
||||
SeaweedFiler_LookupVolume_FullMethodName = "/filer_pb.SeaweedFiler/LookupVolume"
|
||||
SeaweedFiler_CollectionList_FullMethodName = "/filer_pb.SeaweedFiler/CollectionList"
|
||||
@@ -58,6 +59,7 @@ type SeaweedFilerClient interface {
|
||||
DeleteEntry(ctx context.Context, in *DeleteEntryRequest, opts ...grpc.CallOption) (*DeleteEntryResponse, error)
|
||||
AtomicRenameEntry(ctx context.Context, in *AtomicRenameEntryRequest, opts ...grpc.CallOption) (*AtomicRenameEntryResponse, error)
|
||||
StreamRenameEntry(ctx context.Context, in *StreamRenameEntryRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StreamRenameEntryResponse], error)
|
||||
StreamMutateEntry(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[StreamMutateEntryRequest, StreamMutateEntryResponse], error)
|
||||
AssignVolume(ctx context.Context, in *AssignVolumeRequest, opts ...grpc.CallOption) (*AssignVolumeResponse, error)
|
||||
LookupVolume(ctx context.Context, in *LookupVolumeRequest, opts ...grpc.CallOption) (*LookupVolumeResponse, error)
|
||||
CollectionList(ctx context.Context, in *CollectionListRequest, opts ...grpc.CallOption) (*CollectionListResponse, error)
|
||||
@@ -184,6 +186,19 @@ func (c *seaweedFilerClient) StreamRenameEntry(ctx context.Context, in *StreamRe
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type SeaweedFiler_StreamRenameEntryClient = grpc.ServerStreamingClient[StreamRenameEntryResponse]
|
||||
|
||||
func (c *seaweedFilerClient) StreamMutateEntry(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[StreamMutateEntryRequest, StreamMutateEntryResponse], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &SeaweedFiler_ServiceDesc.Streams[2], SeaweedFiler_StreamMutateEntry_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[StreamMutateEntryRequest, StreamMutateEntryResponse]{ClientStream: stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type SeaweedFiler_StreamMutateEntryClient = grpc.BidiStreamingClient[StreamMutateEntryRequest, StreamMutateEntryResponse]
|
||||
|
||||
func (c *seaweedFilerClient) AssignVolume(ctx context.Context, in *AssignVolumeRequest, opts ...grpc.CallOption) (*AssignVolumeResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AssignVolumeResponse)
|
||||
@@ -256,7 +271,7 @@ func (c *seaweedFilerClient) GetFilerConfiguration(ctx context.Context, in *GetF
|
||||
|
||||
func (c *seaweedFilerClient) TraverseBfsMetadata(ctx context.Context, in *TraverseBfsMetadataRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TraverseBfsMetadataResponse], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &SeaweedFiler_ServiceDesc.Streams[2], SeaweedFiler_TraverseBfsMetadata_FullMethodName, cOpts...)
|
||||
stream, err := c.cc.NewStream(ctx, &SeaweedFiler_ServiceDesc.Streams[3], SeaweedFiler_TraverseBfsMetadata_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -275,7 +290,7 @@ type SeaweedFiler_TraverseBfsMetadataClient = grpc.ServerStreamingClient[Travers
|
||||
|
||||
func (c *seaweedFilerClient) SubscribeMetadata(ctx context.Context, in *SubscribeMetadataRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SubscribeMetadataResponse], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &SeaweedFiler_ServiceDesc.Streams[3], SeaweedFiler_SubscribeMetadata_FullMethodName, cOpts...)
|
||||
stream, err := c.cc.NewStream(ctx, &SeaweedFiler_ServiceDesc.Streams[4], SeaweedFiler_SubscribeMetadata_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -294,7 +309,7 @@ type SeaweedFiler_SubscribeMetadataClient = grpc.ServerStreamingClient[Subscribe
|
||||
|
||||
func (c *seaweedFilerClient) SubscribeLocalMetadata(ctx context.Context, in *SubscribeMetadataRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SubscribeMetadataResponse], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &SeaweedFiler_ServiceDesc.Streams[4], SeaweedFiler_SubscribeLocalMetadata_FullMethodName, cOpts...)
|
||||
stream, err := c.cc.NewStream(ctx, &SeaweedFiler_ServiceDesc.Streams[5], SeaweedFiler_SubscribeLocalMetadata_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -393,6 +408,7 @@ type SeaweedFilerServer interface {
|
||||
DeleteEntry(context.Context, *DeleteEntryRequest) (*DeleteEntryResponse, error)
|
||||
AtomicRenameEntry(context.Context, *AtomicRenameEntryRequest) (*AtomicRenameEntryResponse, error)
|
||||
StreamRenameEntry(*StreamRenameEntryRequest, grpc.ServerStreamingServer[StreamRenameEntryResponse]) error
|
||||
StreamMutateEntry(grpc.BidiStreamingServer[StreamMutateEntryRequest, StreamMutateEntryResponse]) error
|
||||
AssignVolume(context.Context, *AssignVolumeRequest) (*AssignVolumeResponse, error)
|
||||
LookupVolume(context.Context, *LookupVolumeRequest) (*LookupVolumeResponse, error)
|
||||
CollectionList(context.Context, *CollectionListRequest) (*CollectionListResponse, error)
|
||||
@@ -445,6 +461,9 @@ func (UnimplementedSeaweedFilerServer) AtomicRenameEntry(context.Context, *Atomi
|
||||
func (UnimplementedSeaweedFilerServer) StreamRenameEntry(*StreamRenameEntryRequest, grpc.ServerStreamingServer[StreamRenameEntryResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method StreamRenameEntry not implemented")
|
||||
}
|
||||
func (UnimplementedSeaweedFilerServer) StreamMutateEntry(grpc.BidiStreamingServer[StreamMutateEntryRequest, StreamMutateEntryResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method StreamMutateEntry not implemented")
|
||||
}
|
||||
func (UnimplementedSeaweedFilerServer) AssignVolume(context.Context, *AssignVolumeRequest) (*AssignVolumeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AssignVolume not implemented")
|
||||
}
|
||||
@@ -647,6 +666,13 @@ func _SeaweedFiler_StreamRenameEntry_Handler(srv interface{}, stream grpc.Server
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type SeaweedFiler_StreamRenameEntryServer = grpc.ServerStreamingServer[StreamRenameEntryResponse]
|
||||
|
||||
func _SeaweedFiler_StreamMutateEntry_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(SeaweedFilerServer).StreamMutateEntry(&grpc.GenericServerStream[StreamMutateEntryRequest, StreamMutateEntryResponse]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type SeaweedFiler_StreamMutateEntryServer = grpc.BidiStreamingServer[StreamMutateEntryRequest, StreamMutateEntryResponse]
|
||||
|
||||
func _SeaweedFiler_AssignVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AssignVolumeRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -1031,6 +1057,12 @@ var SeaweedFiler_ServiceDesc = grpc.ServiceDesc{
|
||||
Handler: _SeaweedFiler_StreamRenameEntry_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "StreamMutateEntry",
|
||||
Handler: _SeaweedFiler_StreamMutateEntry_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "TraverseBfsMetadata",
|
||||
Handler: _SeaweedFiler_TraverseBfsMetadata_Handler,
|
||||
|
||||
@@ -145,6 +145,13 @@ func CreateEntryWithResponse(ctx context.Context, client SeaweedFilerClient, req
|
||||
glog.V(1).InfofCtx(ctx, "create entry %s/%s %v: %v", request.Directory, request.Entry.Name, request.OExcl, err)
|
||||
return nil, fmt.Errorf("CreateEntry: %w", err)
|
||||
}
|
||||
if resp.ErrorCode != FilerError_OK {
|
||||
glog.V(1).InfofCtx(ctx, "create entry %s/%s %v: %v (code %v)", request.Directory, request.Entry.Name, request.OExcl, resp.Error, resp.ErrorCode)
|
||||
if sentinel := FilerErrorToSentinel(resp.ErrorCode); sentinel != nil {
|
||||
return nil, fmt.Errorf("CreateEntry %s/%s: %w", request.Directory, request.Entry.Name, sentinel)
|
||||
}
|
||||
return nil, fmt.Errorf("CreateEntry: %w", errors.New(resp.Error))
|
||||
}
|
||||
if resp.Error != "" {
|
||||
glog.V(1).InfofCtx(ctx, "create entry %s/%s %v: %v", request.Directory, request.Entry.Name, request.OExcl, resp.Error)
|
||||
return nil, fmt.Errorf("CreateEntry: %w", errors.New(resp.Error))
|
||||
@@ -183,6 +190,37 @@ func LookupEntry(ctx context.Context, client SeaweedFilerClient, request *Lookup
|
||||
|
||||
var ErrNotFound = errors.New("filer: no entry is found in filer store")
|
||||
|
||||
// Sentinel errors for filer entry operations.
|
||||
// These are set by the filer and reconstructed from FilerError codes after
|
||||
// crossing the gRPC boundary, so consumers can use errors.Is() instead of
|
||||
// parsing error strings.
|
||||
var (
|
||||
ErrEntryNameTooLong = errors.New("entry name too long")
|
||||
ErrParentIsFile = errors.New("parent path is a file")
|
||||
ErrExistingIsDirectory = errors.New("existing entry is a directory")
|
||||
ErrExistingIsFile = errors.New("existing entry is a file")
|
||||
ErrEntryAlreadyExists = errors.New("entry already exists")
|
||||
)
|
||||
|
||||
// FilerErrorToSentinel maps a proto FilerError code to its sentinel error.
|
||||
// Returns nil for OK or unknown codes.
|
||||
func FilerErrorToSentinel(code FilerError) error {
|
||||
switch code {
|
||||
case FilerError_ENTRY_NAME_TOO_LONG:
|
||||
return ErrEntryNameTooLong
|
||||
case FilerError_PARENT_IS_FILE:
|
||||
return ErrParentIsFile
|
||||
case FilerError_EXISTING_IS_DIRECTORY:
|
||||
return ErrExistingIsDirectory
|
||||
case FilerError_EXISTING_IS_FILE:
|
||||
return ErrExistingIsFile
|
||||
case FilerError_ENTRY_ALREADY_EXISTS:
|
||||
return ErrEntryAlreadyExists
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func IsEmpty(event *SubscribeMetadataResponse) bool {
|
||||
return event.EventNotification.NewEntry == nil && event.EventNotification.OldEntry == nil
|
||||
}
|
||||
|
||||
@@ -656,6 +656,7 @@ message ScrubVolumeRequest {
|
||||
VolumeScrubMode mode = 1;
|
||||
// optional list of volume IDs to scrub. if empty, all volumes for the server are scrubbed.
|
||||
repeated uint32 volume_ids = 2;
|
||||
bool mark_broken_volumes_readonly = 3;
|
||||
}
|
||||
message ScrubVolumeResponse {
|
||||
uint64 total_volumes = 1;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc v6.33.4
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc v6.33.4
|
||||
// source: volume_server.proto
|
||||
|
||||
package volume_server_pb
|
||||
|
||||
import (
|
||||
remote_pb "github.com/seaweedfs/seaweedfs/weed/pb/remote_pb"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
|
||||
remote_pb "github.com/seaweedfs/seaweedfs/weed/pb/remote_pb"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -5578,9 +5579,10 @@ type ScrubVolumeRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Mode VolumeScrubMode `protobuf:"varint,1,opt,name=mode,proto3,enum=volume_server_pb.VolumeScrubMode" json:"mode,omitempty"`
|
||||
// optional list of volume IDs to scrub. if empty, all volumes for the server are scrubbed.
|
||||
VolumeIds []uint32 `protobuf:"varint,2,rep,packed,name=volume_ids,json=volumeIds,proto3" json:"volume_ids,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
VolumeIds []uint32 `protobuf:"varint,2,rep,packed,name=volume_ids,json=volumeIds,proto3" json:"volume_ids,omitempty"`
|
||||
MarkBrokenVolumesReadonly bool `protobuf:"varint,3,opt,name=mark_broken_volumes_readonly,json=markBrokenVolumesReadonly,proto3" json:"mark_broken_volumes_readonly,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ScrubVolumeRequest) Reset() {
|
||||
@@ -5627,6 +5629,13 @@ func (x *ScrubVolumeRequest) GetVolumeIds() []uint32 {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ScrubVolumeRequest) GetMarkBrokenVolumesReadonly() bool {
|
||||
if x != nil {
|
||||
return x.MarkBrokenVolumesReadonly
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ScrubVolumeResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
TotalVolumes uint64 `protobuf:"varint,1,opt,name=total_volumes,json=totalVolumes,proto3" json:"total_volumes,omitempty"`
|
||||
@@ -7146,11 +7155,12 @@ const file_volume_server_proto_rawDesc = "" +
|
||||
"public_url\x18\x02 \x01(\tR\tpublicUrl\x12\x1b\n" +
|
||||
"\tgrpc_port\x18\x03 \x01(\x05R\bgrpcPort\"2\n" +
|
||||
"\x1bFetchAndWriteNeedleResponse\x12\x13\n" +
|
||||
"\x05e_tag\x18\x01 \x01(\tR\x04eTag\"j\n" +
|
||||
"\x05e_tag\x18\x01 \x01(\tR\x04eTag\"\xab\x01\n" +
|
||||
"\x12ScrubVolumeRequest\x125\n" +
|
||||
"\x04mode\x18\x01 \x01(\x0e2!.volume_server_pb.VolumeScrubModeR\x04mode\x12\x1d\n" +
|
||||
"\n" +
|
||||
"volume_ids\x18\x02 \x03(\rR\tvolumeIds\"\xa1\x01\n" +
|
||||
"volume_ids\x18\x02 \x03(\rR\tvolumeIds\x12?\n" +
|
||||
"\x1cmark_broken_volumes_readonly\x18\x03 \x01(\bR\x19markBrokenVolumesReadonly\"\xa1\x01\n" +
|
||||
"\x13ScrubVolumeResponse\x12#\n" +
|
||||
"\rtotal_volumes\x18\x01 \x01(\x04R\ftotalVolumes\x12\x1f\n" +
|
||||
"\vtotal_files\x18\x02 \x01(\x04R\n" +
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v6.33.4
|
||||
// - protoc-gen-go-grpc v1.6.1
|
||||
// - protoc v3.21.12
|
||||
// source: volume_server.proto
|
||||
|
||||
package volume_server_pb
|
||||
@@ -781,148 +781,148 @@ type VolumeServerServer interface {
|
||||
type UnimplementedVolumeServerServer struct{}
|
||||
|
||||
func (UnimplementedVolumeServerServer) BatchDelete(context.Context, *BatchDeleteRequest) (*BatchDeleteResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchDelete not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchDelete not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VacuumVolumeCheck(context.Context, *VacuumVolumeCheckRequest) (*VacuumVolumeCheckResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VacuumVolumeCheck not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VacuumVolumeCheck not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VacuumVolumeCompact(*VacuumVolumeCompactRequest, grpc.ServerStreamingServer[VacuumVolumeCompactResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method VacuumVolumeCompact not implemented")
|
||||
return status.Error(codes.Unimplemented, "method VacuumVolumeCompact not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VacuumVolumeCommit(context.Context, *VacuumVolumeCommitRequest) (*VacuumVolumeCommitResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VacuumVolumeCommit not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VacuumVolumeCommit not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VacuumVolumeCleanup(context.Context, *VacuumVolumeCleanupRequest) (*VacuumVolumeCleanupResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VacuumVolumeCleanup not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VacuumVolumeCleanup not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) DeleteCollection(context.Context, *DeleteCollectionRequest) (*DeleteCollectionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteCollection not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteCollection not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) AllocateVolume(context.Context, *AllocateVolumeRequest) (*AllocateVolumeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AllocateVolume not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AllocateVolume not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeSyncStatus(context.Context, *VolumeSyncStatusRequest) (*VolumeSyncStatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeSyncStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeSyncStatus not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeIncrementalCopy(*VolumeIncrementalCopyRequest, grpc.ServerStreamingServer[VolumeIncrementalCopyResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method VolumeIncrementalCopy not implemented")
|
||||
return status.Error(codes.Unimplemented, "method VolumeIncrementalCopy not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeMount(context.Context, *VolumeMountRequest) (*VolumeMountResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeMount not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeMount not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeUnmount(context.Context, *VolumeUnmountRequest) (*VolumeUnmountResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeUnmount not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeUnmount not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeDelete(context.Context, *VolumeDeleteRequest) (*VolumeDeleteResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeDelete not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeDelete not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeMarkReadonly(context.Context, *VolumeMarkReadonlyRequest) (*VolumeMarkReadonlyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeMarkReadonly not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeMarkReadonly not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeMarkWritable(context.Context, *VolumeMarkWritableRequest) (*VolumeMarkWritableResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeMarkWritable not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeMarkWritable not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeConfigure(context.Context, *VolumeConfigureRequest) (*VolumeConfigureResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeConfigure not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeConfigure not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeStatus(context.Context, *VolumeStatusRequest) (*VolumeStatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeStatus not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) GetState(context.Context, *GetStateRequest) (*GetStateResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetState not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetState not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) SetState(context.Context, *SetStateRequest) (*SetStateResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetState not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetState not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeCopy(*VolumeCopyRequest, grpc.ServerStreamingServer[VolumeCopyResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method VolumeCopy not implemented")
|
||||
return status.Error(codes.Unimplemented, "method VolumeCopy not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) ReadVolumeFileStatus(context.Context, *ReadVolumeFileStatusRequest) (*ReadVolumeFileStatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReadVolumeFileStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ReadVolumeFileStatus not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) CopyFile(*CopyFileRequest, grpc.ServerStreamingServer[CopyFileResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method CopyFile not implemented")
|
||||
return status.Error(codes.Unimplemented, "method CopyFile not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) ReceiveFile(grpc.ClientStreamingServer[ReceiveFileRequest, ReceiveFileResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method ReceiveFile not implemented")
|
||||
return status.Error(codes.Unimplemented, "method ReceiveFile not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) ReadNeedleBlob(context.Context, *ReadNeedleBlobRequest) (*ReadNeedleBlobResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReadNeedleBlob not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ReadNeedleBlob not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) ReadNeedleMeta(context.Context, *ReadNeedleMetaRequest) (*ReadNeedleMetaResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReadNeedleMeta not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ReadNeedleMeta not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) WriteNeedleBlob(context.Context, *WriteNeedleBlobRequest) (*WriteNeedleBlobResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method WriteNeedleBlob not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method WriteNeedleBlob not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) ReadAllNeedles(*ReadAllNeedlesRequest, grpc.ServerStreamingServer[ReadAllNeedlesResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method ReadAllNeedles not implemented")
|
||||
return status.Error(codes.Unimplemented, "method ReadAllNeedles not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeTailSender(*VolumeTailSenderRequest, grpc.ServerStreamingServer[VolumeTailSenderResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method VolumeTailSender not implemented")
|
||||
return status.Error(codes.Unimplemented, "method VolumeTailSender not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeTailReceiver(context.Context, *VolumeTailReceiverRequest) (*VolumeTailReceiverResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeTailReceiver not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeTailReceiver not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeEcShardsGenerate(context.Context, *VolumeEcShardsGenerateRequest) (*VolumeEcShardsGenerateResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeEcShardsGenerate not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeEcShardsGenerate not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeEcShardsRebuild(context.Context, *VolumeEcShardsRebuildRequest) (*VolumeEcShardsRebuildResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeEcShardsRebuild not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeEcShardsRebuild not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeEcShardsCopy(context.Context, *VolumeEcShardsCopyRequest) (*VolumeEcShardsCopyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeEcShardsCopy not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeEcShardsCopy not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeEcShardsDelete(context.Context, *VolumeEcShardsDeleteRequest) (*VolumeEcShardsDeleteResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeEcShardsDelete not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeEcShardsDelete not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeEcShardsMount(context.Context, *VolumeEcShardsMountRequest) (*VolumeEcShardsMountResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeEcShardsMount not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeEcShardsMount not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeEcShardsUnmount(context.Context, *VolumeEcShardsUnmountRequest) (*VolumeEcShardsUnmountResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeEcShardsUnmount not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeEcShardsUnmount not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeEcShardRead(*VolumeEcShardReadRequest, grpc.ServerStreamingServer[VolumeEcShardReadResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method VolumeEcShardRead not implemented")
|
||||
return status.Error(codes.Unimplemented, "method VolumeEcShardRead not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeEcBlobDelete(context.Context, *VolumeEcBlobDeleteRequest) (*VolumeEcBlobDeleteResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeEcBlobDelete not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeEcBlobDelete not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeEcShardsToVolume(context.Context, *VolumeEcShardsToVolumeRequest) (*VolumeEcShardsToVolumeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeEcShardsToVolume not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeEcShardsToVolume not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeEcShardsInfo(context.Context, *VolumeEcShardsInfoRequest) (*VolumeEcShardsInfoResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeEcShardsInfo not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeEcShardsInfo not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeTierMoveDatToRemote(*VolumeTierMoveDatToRemoteRequest, grpc.ServerStreamingServer[VolumeTierMoveDatToRemoteResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method VolumeTierMoveDatToRemote not implemented")
|
||||
return status.Error(codes.Unimplemented, "method VolumeTierMoveDatToRemote not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeTierMoveDatFromRemote(*VolumeTierMoveDatFromRemoteRequest, grpc.ServerStreamingServer[VolumeTierMoveDatFromRemoteResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method VolumeTierMoveDatFromRemote not implemented")
|
||||
return status.Error(codes.Unimplemented, "method VolumeTierMoveDatFromRemote not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeServerStatus(context.Context, *VolumeServerStatusRequest) (*VolumeServerStatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeServerStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeServerStatus not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeServerLeave(context.Context, *VolumeServerLeaveRequest) (*VolumeServerLeaveResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeServerLeave not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeServerLeave not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) FetchAndWriteNeedle(context.Context, *FetchAndWriteNeedleRequest) (*FetchAndWriteNeedleResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method FetchAndWriteNeedle not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method FetchAndWriteNeedle not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) ScrubVolume(context.Context, *ScrubVolumeRequest) (*ScrubVolumeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ScrubVolume not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ScrubVolume not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) ScrubEcVolume(context.Context, *ScrubEcVolumeRequest) (*ScrubEcVolumeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ScrubEcVolume not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ScrubEcVolume not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) Query(*QueryRequest, grpc.ServerStreamingServer[QueriedStripe]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method Query not implemented")
|
||||
return status.Error(codes.Unimplemented, "method Query not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) VolumeNeedleStatus(context.Context, *VolumeNeedleStatusRequest) (*VolumeNeedleStatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VolumeNeedleStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method VolumeNeedleStatus not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) Ping(context.Context, *PingRequest) (*PingResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Ping not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServerServer) mustEmbedUnimplementedVolumeServerServer() {}
|
||||
func (UnimplementedVolumeServerServer) testEmbeddedByValue() {}
|
||||
@@ -935,7 +935,7 @@ type UnsafeVolumeServerServer interface {
|
||||
}
|
||||
|
||||
func RegisterVolumeServerServer(s grpc.ServiceRegistrar, srv VolumeServerServer) {
|
||||
// If the following call pancis, it indicates UnimplementedVolumeServerServer was
|
||||
// If the following call panics, it indicates UnimplementedVolumeServerServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -224,14 +223,13 @@ func (s3a *S3ApiServer) updateBucketConfigCacheFromEntry(entry *filer_pb.Entry)
|
||||
// Sync bucket policy to the policy engine for evaluation
|
||||
s3a.syncBucketPolicyToEngine(bucket, config.BucketPolicy)
|
||||
|
||||
// Load CORS configuration from bucket directory content
|
||||
if corsConfig, err := s3a.loadCORSFromBucketContent(bucket); err != nil {
|
||||
if !errors.Is(err, filer_pb.ErrNotFound) {
|
||||
glog.Errorf("updateBucketConfigCacheFromEntry: failed to load CORS configuration for bucket %s: %v", bucket, err)
|
||||
}
|
||||
} else {
|
||||
config.CORS = corsConfig
|
||||
glog.V(2).Infof("updateBucketConfigCacheFromEntry: loaded CORS config for bucket %s", bucket)
|
||||
// Parse CORS configuration directly from the subscription entry's Content field.
|
||||
// This avoids a separate RPC call that could return stale data when racing with
|
||||
// concurrent metadata updates (e.g., PutBucketCors clearing the cache while this
|
||||
// handler is still processing an older event).
|
||||
config.CORS = parseCORSFromEntryContent(entry.Content)
|
||||
if config.CORS != nil {
|
||||
glog.V(2).Infof("updateBucketConfigCacheFromEntry: parsed CORS config for bucket %s from entry content", bucket)
|
||||
}
|
||||
|
||||
// Update timestamp
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func (s3a *S3ApiServer) mkdir(parentDirectoryPath string, dirName string, fn func(entry *filer_pb.Entry)) error {
|
||||
@@ -47,16 +49,32 @@ func (s3a *S3ApiServer) rm(parentDirectoryPath, entryName string, isDeleteData,
|
||||
|
||||
return s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
|
||||
err := doDeleteEntry(client, parentDirectoryPath, entryName, isDeleteData, isRecursive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return doDeleteEntry(client, parentDirectoryPath, entryName, isDeleteData, isRecursive)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) rmObject(parentDirectoryPath, entryName string, isDeleteData, isRecursive bool) error {
|
||||
|
||||
return s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
|
||||
return deleteObjectEntry(client, parentDirectoryPath, entryName, isDeleteData, isRecursive)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func deleteObjectEntry(client filer_pb.SeaweedFilerClient, parentDirectoryPath, entryName string, isDeleteData, isRecursive bool) error {
|
||||
err := doDeleteEntry(client, parentDirectoryPath, entryName, isDeleteData, isRecursive)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !strings.Contains(err.Error(), filer.MsgFailDelNonEmptyFolder) {
|
||||
return err
|
||||
}
|
||||
|
||||
return demoteDirectoryMarkerToImplicitDirectory(client, parentDirectoryPath, entryName)
|
||||
}
|
||||
|
||||
func doDeleteEntry(client filer_pb.SeaweedFilerClient, parentDirectoryPath string, entryName string, isDeleteData bool, isRecursive bool) error {
|
||||
request := &filer_pb.DeleteEntryRequest{
|
||||
Directory: parentDirectoryPath,
|
||||
@@ -78,6 +96,71 @@ func doDeleteEntry(client filer_pb.SeaweedFilerClient, parentDirectoryPath strin
|
||||
return nil
|
||||
}
|
||||
|
||||
func demoteDirectoryMarkerToImplicitDirectory(client filer_pb.SeaweedFilerClient, parentDirectoryPath, entryName string) error {
|
||||
resp, err := filer_pb.LookupEntry(context.Background(), client, &filer_pb.LookupDirectoryEntryRequest{
|
||||
Directory: parentDirectoryPath,
|
||||
Name: entryName,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("lookup entry %s/%s: %w", parentDirectoryPath, entryName, err)
|
||||
}
|
||||
if resp.Entry == nil || !resp.Entry.IsDirectory {
|
||||
return nil
|
||||
}
|
||||
if !resp.Entry.IsDirectoryKeyObject() {
|
||||
return nil
|
||||
}
|
||||
|
||||
clearDirectoryMarkerMetadata(resp.Entry)
|
||||
|
||||
if err := filer_pb.UpdateEntry(context.Background(), client, &filer_pb.UpdateEntryRequest{
|
||||
Directory: parentDirectoryPath,
|
||||
Entry: resp.Entry,
|
||||
}); err != nil {
|
||||
if errors.Is(err, filer_pb.ErrNotFound) || status.Code(err) == codes.NotFound {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("update entry %s/%s: %w", parentDirectoryPath, entryName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func clearDirectoryMarkerMetadata(entry *filer_pb.Entry) {
|
||||
if entry == nil {
|
||||
return
|
||||
}
|
||||
if entry.Attributes == nil {
|
||||
entry.Attributes = &filer_pb.FuseAttributes{}
|
||||
}
|
||||
|
||||
entry.Attributes.Mime = ""
|
||||
entry.Attributes.Md5 = nil
|
||||
entry.Attributes.FileSize = 0
|
||||
entry.Content = nil
|
||||
entry.Chunks = nil
|
||||
|
||||
if len(entry.Extended) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
filtered := make(map[string][]byte)
|
||||
for k, v := range entry.Extended {
|
||||
lowerKey := strings.ToLower(k)
|
||||
if strings.HasPrefix(lowerKey, "xattr-") || strings.HasPrefix(lowerKey, s3_constants.SeaweedFSInternalPrefix) {
|
||||
filtered[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if len(filtered) == 0 {
|
||||
entry.Extended = nil
|
||||
return
|
||||
}
|
||||
entry.Extended = filtered
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) exists(parentDirectoryPath string, entryName string, isDirectory bool) (exists bool, err error) {
|
||||
|
||||
return filer_pb.Exists(context.Background(), s3a, parentDirectoryPath, entryName, isDirectory)
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
grpc "google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type deleteObjectEntryTestClient struct {
|
||||
filer_pb.SeaweedFilerClient
|
||||
|
||||
deleteResp *filer_pb.DeleteEntryResponse
|
||||
deleteErr error
|
||||
lookupResp *filer_pb.LookupDirectoryEntryResponse
|
||||
lookupErr error
|
||||
updateErr error
|
||||
|
||||
deleteReq *filer_pb.DeleteEntryRequest
|
||||
lookupReq *filer_pb.LookupDirectoryEntryRequest
|
||||
updateReq *filer_pb.UpdateEntryRequest
|
||||
}
|
||||
|
||||
func (c *deleteObjectEntryTestClient) DeleteEntry(_ context.Context, req *filer_pb.DeleteEntryRequest, _ ...grpc.CallOption) (*filer_pb.DeleteEntryResponse, error) {
|
||||
c.deleteReq = req
|
||||
if c.deleteResp == nil {
|
||||
return &filer_pb.DeleteEntryResponse{}, c.deleteErr
|
||||
}
|
||||
return c.deleteResp, c.deleteErr
|
||||
}
|
||||
|
||||
func (c *deleteObjectEntryTestClient) LookupDirectoryEntry(_ context.Context, req *filer_pb.LookupDirectoryEntryRequest, _ ...grpc.CallOption) (*filer_pb.LookupDirectoryEntryResponse, error) {
|
||||
c.lookupReq = req
|
||||
if c.lookupResp == nil {
|
||||
return &filer_pb.LookupDirectoryEntryResponse{}, c.lookupErr
|
||||
}
|
||||
return c.lookupResp, c.lookupErr
|
||||
}
|
||||
|
||||
func (c *deleteObjectEntryTestClient) UpdateEntry(_ context.Context, req *filer_pb.UpdateEntryRequest, _ ...grpc.CallOption) (*filer_pb.UpdateEntryResponse, error) {
|
||||
c.updateReq = req
|
||||
return &filer_pb.UpdateEntryResponse{}, c.updateErr
|
||||
}
|
||||
|
||||
func TestDeleteObjectEntryDemotesNonEmptyDirectoryMarker(t *testing.T) {
|
||||
client := &deleteObjectEntryTestClient{
|
||||
deleteResp: &filer_pb.DeleteEntryResponse{
|
||||
Error: filer.MsgFailDelNonEmptyFolder + ": /buckets/test/photos",
|
||||
},
|
||||
lookupResp: &filer_pb.LookupDirectoryEntryResponse{
|
||||
Entry: &filer_pb.Entry{
|
||||
Name: "photos",
|
||||
IsDirectory: true,
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Mime: "application/octet-stream",
|
||||
Md5: []byte{1, 2, 3, 4},
|
||||
FileSize: 4,
|
||||
},
|
||||
Content: []byte("test"),
|
||||
Extended: map[string][]byte{
|
||||
s3_constants.ExtETagKey: []byte("etag"),
|
||||
s3_constants.ExtAmzOwnerKey: []byte("owner"),
|
||||
s3_constants.AmzUserMetaPrefix + "Color": []byte("blue"),
|
||||
s3_constants.AmzObjectTaggingPrefix + "k": []byte("v"),
|
||||
"xattr-keep": []byte("keep-me"),
|
||||
"x-seaweedfs-internal": []byte("keep-me-too"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := deleteObjectEntry(client, "/buckets/test", "photos", true, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client.lookupReq)
|
||||
require.NotNil(t, client.updateReq)
|
||||
|
||||
updated := client.updateReq.Entry
|
||||
require.NotNil(t, updated)
|
||||
assert.False(t, updated.IsDirectoryKeyObject())
|
||||
assert.Equal(t, "", updated.Attributes.Mime)
|
||||
assert.Empty(t, updated.Attributes.Md5)
|
||||
assert.Zero(t, updated.Attributes.FileSize)
|
||||
assert.Nil(t, updated.Content)
|
||||
assert.Nil(t, updated.Chunks)
|
||||
assert.Equal(t, map[string][]byte{
|
||||
"xattr-keep": []byte("keep-me"),
|
||||
"x-seaweedfs-internal": []byte("keep-me-too"),
|
||||
}, updated.Extended)
|
||||
}
|
||||
|
||||
func TestDeleteObjectEntryTreatsImplicitDirectoryAsSuccessfulNoop(t *testing.T) {
|
||||
client := &deleteObjectEntryTestClient{
|
||||
deleteResp: &filer_pb.DeleteEntryResponse{
|
||||
Error: filer.MsgFailDelNonEmptyFolder + ": /buckets/test/photos",
|
||||
},
|
||||
lookupResp: &filer_pb.LookupDirectoryEntryResponse{
|
||||
Entry: &filer_pb.Entry{
|
||||
Name: "photos",
|
||||
IsDirectory: true,
|
||||
Attributes: &filer_pb.FuseAttributes{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := deleteObjectEntry(client, "/buckets/test", "photos", true, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client.lookupReq)
|
||||
assert.Nil(t, client.updateReq)
|
||||
}
|
||||
|
||||
func TestDeleteObjectEntryIgnoresConcurrentUpdateNotFound(t *testing.T) {
|
||||
client := &deleteObjectEntryTestClient{
|
||||
deleteResp: &filer_pb.DeleteEntryResponse{
|
||||
Error: filer.MsgFailDelNonEmptyFolder + ": /buckets/test/photos",
|
||||
},
|
||||
lookupResp: &filer_pb.LookupDirectoryEntryResponse{
|
||||
Entry: &filer_pb.Entry{
|
||||
Name: "photos",
|
||||
IsDirectory: true,
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Mime: "application/octet-stream",
|
||||
},
|
||||
},
|
||||
},
|
||||
updateErr: status.Error(codes.NotFound, "already removed"),
|
||||
}
|
||||
|
||||
err := deleteObjectEntry(client, "/buckets/test", "photos", true, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client.lookupReq)
|
||||
require.NotNil(t, client.updateReq)
|
||||
}
|
||||
|
||||
func TestDeleteObjectEntryPropagatesNonDirectoryDeleteErrors(t *testing.T) {
|
||||
client := &deleteObjectEntryTestClient{
|
||||
deleteErr: errors.New("boom"),
|
||||
}
|
||||
|
||||
err := deleteObjectEntry(client, "/buckets/test", "photos", true, false)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "boom")
|
||||
assert.Nil(t, client.lookupReq)
|
||||
assert.Nil(t, client.updateReq)
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func (s *Server) saveMetadataFile(ctx context.Context, bucketName, tablePath, me
|
||||
if createErr != nil {
|
||||
return fmt.Errorf("failed to create %s: %w", errorContext, createErr)
|
||||
}
|
||||
if resp.Error != "" && !strings.Contains(resp.Error, "exist") {
|
||||
if resp.ErrorCode != filer_pb.FilerError_OK && resp.ErrorCode != filer_pb.FilerError_ENTRY_ALREADY_EXISTS {
|
||||
return fmt.Errorf("failed to create %s: %s", errorContext, resp.Error)
|
||||
}
|
||||
return nil
|
||||
@@ -104,8 +104,11 @@ func (s *Server) saveMetadataFile(ctx context.Context, bucketName, tablePath, me
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write metadata file: %w", err)
|
||||
}
|
||||
if resp.Error != "" {
|
||||
return fmt.Errorf("failed to write metadata file: %s", resp.Error)
|
||||
if resp.ErrorCode != filer_pb.FilerError_OK {
|
||||
if sentinel := filer_pb.FilerErrorToSentinel(resp.ErrorCode); sentinel != nil {
|
||||
return fmt.Errorf("failed to write metadata file: %w", sentinel)
|
||||
}
|
||||
return fmt.Errorf("failed to write metadata file: code=%v %s", resp.ErrorCode, resp.Error)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -198,7 +198,7 @@ func (s *Server) writeStageCreateMarker(ctx context.Context, bucketName string,
|
||||
if createErr != nil {
|
||||
return fmt.Errorf("failed to create %s: %w", errorContext, createErr)
|
||||
}
|
||||
if resp.Error != "" && !strings.Contains(resp.Error, "exist") {
|
||||
if resp.ErrorCode != filer_pb.FilerError_OK && resp.ErrorCode != filer_pb.FilerError_ENTRY_ALREADY_EXISTS {
|
||||
return fmt.Errorf("failed to create %s: %s", errorContext, resp.Error)
|
||||
}
|
||||
return nil
|
||||
@@ -236,8 +236,11 @@ func (s *Server) writeStageCreateMarker(ctx context.Context, bucketName string,
|
||||
if createErr != nil {
|
||||
return createErr
|
||||
}
|
||||
if resp.Error != "" {
|
||||
return errors.New(resp.Error)
|
||||
if resp.ErrorCode != filer_pb.FilerError_OK {
|
||||
if sentinel := filer_pb.FilerErrorToSentinel(resp.ErrorCode); sentinel != nil {
|
||||
return fmt.Errorf("create stage marker: %w", sentinel)
|
||||
}
|
||||
return fmt.Errorf("create stage marker: code=%v %s", resp.ErrorCode, resp.Error)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -427,10 +427,11 @@ func ExtractConditionValuesFromRequest(r *http.Request) map[string][]string {
|
||||
// HTTP method
|
||||
values["s3:RequestMethod"] = []string{r.Method}
|
||||
|
||||
// Extract custom headers
|
||||
// Extract custom headers with s3: prefix for AWS-compatible condition keys
|
||||
for key, headerValues := range r.Header {
|
||||
if strings.HasPrefix(strings.ToLower(key), "x-amz-") {
|
||||
values[strings.ToLower(key)] = headerValues
|
||||
lowerKey := strings.ToLower(key)
|
||||
if strings.HasPrefix(lowerKey, "x-amz-") {
|
||||
values["s3:"+lowerKey] = headerValues
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -444,8 +444,8 @@ func TestExtractConditionValuesFromRequest(t *testing.T) {
|
||||
t.Errorf("Expected RequestMethod to be GET, got %v", values["s3:RequestMethod"])
|
||||
}
|
||||
|
||||
if len(values["x-amz-copy-source"]) != 1 || values["x-amz-copy-source"][0] != "source-bucket/source-object" {
|
||||
t.Errorf("Expected X-Amz-Copy-Source header to be extracted, got %v", values["x-amz-copy-source"])
|
||||
if len(values["s3:x-amz-copy-source"]) != 1 || values["s3:x-amz-copy-source"][0] != "source-bucket/source-object" {
|
||||
t.Errorf("Expected X-Amz-Copy-Source header to be extracted with s3: prefix, got %v", values["s3:x-amz-copy-source"])
|
||||
}
|
||||
|
||||
// Check that aws:CurrentTime is properly set
|
||||
|
||||
@@ -29,6 +29,11 @@ const (
|
||||
S3Namespace = "http://s3.amazonaws.com/doc/2006-03-01/"
|
||||
)
|
||||
|
||||
// S3 object key limits
|
||||
const (
|
||||
MaxS3ObjectKeyLength = 1024
|
||||
)
|
||||
|
||||
// Standard S3 HTTP request constants
|
||||
const (
|
||||
// S3 storage class
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
mathrand "math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -359,8 +358,8 @@ func (km *SSES3KeyManager) generateAndSaveSuperKeyToFiler() error {
|
||||
// Set appropriate permissions for the directory
|
||||
entry.Attributes.FileMode = uint32(0700 | os.ModeDir)
|
||||
}); err != nil {
|
||||
// Only ignore "file exists" errors.
|
||||
if !strings.Contains(err.Error(), "file exists") {
|
||||
// Only ignore "already exists" errors.
|
||||
if !errors.Is(err, filer_pb.ErrEntryAlreadyExists) {
|
||||
return fmt.Errorf("failed to create KEK directory %s: %w", SSES3KEKDirectory, err)
|
||||
}
|
||||
glog.V(3).Infof("Parent directory %s already exists, continuing.", SSES3KEKDirectory)
|
||||
|
||||
@@ -408,18 +408,9 @@ func (s3a *S3ApiServer) getBucketConfig(bucket string) (*BucketConfig, s3err.Err
|
||||
// Sync bucket policy to the policy engine for evaluation
|
||||
s3a.syncBucketPolicyToEngine(bucket, config.BucketPolicy)
|
||||
|
||||
// Load CORS configuration from bucket directory content
|
||||
if corsConfig, err := s3a.loadCORSFromBucketContent(bucket); err != nil {
|
||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
||||
// Missing metadata is not an error; fall back cleanly
|
||||
glog.V(2).Infof("CORS metadata not found for bucket %s, falling back to default behavior", bucket)
|
||||
} else {
|
||||
// Log parsing or validation errors
|
||||
glog.Errorf("Failed to load CORS configuration for bucket %s: %v", bucket, err)
|
||||
}
|
||||
} else {
|
||||
config.CORS = corsConfig
|
||||
}
|
||||
// Parse CORS configuration directly from the entry's Content field.
|
||||
// This avoids a redundant RPC call since we already have the entry.
|
||||
config.CORS = parseCORSFromEntryContent(entry.Content)
|
||||
|
||||
// Cache the result
|
||||
s3a.bucketConfigCache.Set(bucket, config)
|
||||
@@ -588,15 +579,19 @@ func (s3a *S3ApiServer) setBucketOwnership(bucket, ownership string) s3err.Error
|
||||
})
|
||||
}
|
||||
|
||||
// loadCORSFromBucketContent loads CORS configuration from bucket directory content
|
||||
func (s3a *S3ApiServer) loadCORSFromBucketContent(bucket string) (*cors.CORSConfiguration, error) {
|
||||
metadata, err := s3a.GetBucketMetadata(bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// parseCORSFromEntryContent parses CORS configuration directly from an entry's Content field.
|
||||
// This avoids a separate RPC call when the entry is already available (e.g., from a
|
||||
// subscription event or a prior getBucketEntry call).
|
||||
func parseCORSFromEntryContent(content []byte) *cors.CORSConfiguration {
|
||||
if len(content) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Note: corsConfig can be nil if no CORS configuration is set, which is valid
|
||||
return metadata.CORS, nil
|
||||
var protoMetadata s3_pb.BucketMetadata
|
||||
if err := proto.Unmarshal(content, &protoMetadata); err != nil {
|
||||
glog.Errorf("parseCORSFromEntryContent: failed to unmarshal protobuf metadata: %v", err)
|
||||
return nil
|
||||
}
|
||||
return corsConfigFromProto(protoMetadata.Cors)
|
||||
}
|
||||
|
||||
// getCORSConfiguration retrieves CORS configuration with caching
|
||||
|
||||
@@ -49,6 +49,10 @@ func (s3a *S3ApiServer) CopyObjectHandler(w http.ResponseWriter, r *http.Request
|
||||
srcBucket, srcObject, srcVersionId := pathToBucketObjectAndVersion(rawCopySource, cpSrcPath)
|
||||
|
||||
glog.V(3).Infof("CopyObjectHandler %s %s (version: %s) => %s %s", srcBucket, srcObject, srcVersionId, dstBucket, dstObject)
|
||||
if len(dstObject) > s3_constants.MaxS3ObjectKeyLength {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrKeyTooLongError)
|
||||
return
|
||||
}
|
||||
if err := s3a.validateTableBucketObjectPath(dstBucket, dstObject); err != nil {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||
return
|
||||
@@ -347,7 +351,7 @@ func (s3a *S3ApiServer) CopyObjectHandler(w http.ResponseWriter, r *http.Request
|
||||
entry.Attributes = dstEntry.Attributes
|
||||
entry.Extended = dstEntry.Extended
|
||||
}); err != nil {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
||||
s3err.WriteErrorResponse(w, r, filerErrorToS3Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -372,7 +376,7 @@ func (s3a *S3ApiServer) CopyObjectHandler(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// Check if destination exists and remove it first (S3 copy overwrites)
|
||||
if exists, _ := s3a.exists(dstDir, dstName, false); exists {
|
||||
if err := s3a.rm(dstDir, dstName, false, false); err != nil {
|
||||
if err := s3a.rmObject(dstDir, dstName, false, false); err != nil {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
||||
return
|
||||
}
|
||||
@@ -383,7 +387,7 @@ func (s3a *S3ApiServer) CopyObjectHandler(w http.ResponseWriter, r *http.Request
|
||||
entry.Attributes = dstEntry.Attributes
|
||||
entry.Extended = dstEntry.Extended
|
||||
}); err != nil {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
||||
s3err.WriteErrorResponse(w, r, filerErrorToS3Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"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/s3api/s3_constants"
|
||||
@@ -130,7 +129,7 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque
|
||||
dir, name := target.DirAndName()
|
||||
|
||||
err := s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
return doDeleteEntry(client, dir, name, true, false)
|
||||
return deleteObjectEntry(client, dir, name, true, false)
|
||||
// Note: Empty folder cleanup is now handled asynchronously by EmptyFolderCleaner
|
||||
// which listens to metadata events and uses consistent hashing for coordination
|
||||
})
|
||||
@@ -345,11 +344,9 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
|
||||
parentDirectoryPath, entryName := target.DirAndName()
|
||||
isDeleteData, isRecursive := true, false
|
||||
|
||||
err := doDeleteEntry(client, parentDirectoryPath, entryName, isDeleteData, isRecursive)
|
||||
err := deleteObjectEntry(client, parentDirectoryPath, entryName, isDeleteData, isRecursive)
|
||||
if err == nil {
|
||||
deletedObjects = append(deletedObjects, object)
|
||||
} else if strings.Contains(err.Error(), filer.MsgFailDelNonEmptyFolder) {
|
||||
deletedObjects = append(deletedObjects, object)
|
||||
} else {
|
||||
deleteErrors = append(deleteErrors, DeleteError{
|
||||
Code: "",
|
||||
|
||||
@@ -35,6 +35,11 @@ const (
|
||||
func (s3a *S3ApiServer) NewMultipartUploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
bucket, object := s3_constants.GetBucketAndObject(r)
|
||||
|
||||
if len(object) > s3_constants.MaxS3ObjectKeyLength {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrKeyTooLongError)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists, and create it if it doesn't (auto-create bucket)
|
||||
if err := s3a.checkBucket(r, bucket); err == s3err.ErrNoSuchBucket {
|
||||
// Auto-create bucket if it doesn't exist (requires Admin permission)
|
||||
@@ -361,82 +366,75 @@ func (s3a *S3ApiServer) PutObjectPartHandler(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
glog.V(2).Infof("PutObjectPartHandler %s %s %04d", bucket, uploadID, partID)
|
||||
|
||||
// Check for SSE-C headers in the current request first
|
||||
// Verify the multipart upload exists (rejects parts after abort)
|
||||
uploadEntry, err := s3a.getEntry(s3a.genUploadsFolder(bucket), uploadID)
|
||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchUpload)
|
||||
return
|
||||
} else if err != nil {
|
||||
glog.Errorf("Could not retrieve upload entry for %s/%s: %v", bucket, uploadID, err)
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
||||
return
|
||||
}
|
||||
|
||||
// Apply SSE settings from the upload entry (unless SSE-C headers are already present)
|
||||
sseCustomerAlgorithm := r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm)
|
||||
if sseCustomerAlgorithm != "" {
|
||||
// SSE-C part upload - headers are already present, let putToFiler handle it
|
||||
} else {
|
||||
// No SSE-C headers, check for SSE-KMS settings from upload directory
|
||||
if uploadEntry, err := s3a.getEntry(s3a.genUploadsFolder(bucket), uploadID); err == nil {
|
||||
if uploadEntry.Extended != nil {
|
||||
// Check if this upload uses SSE-KMS
|
||||
if keyIDBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSKeyID]; exists {
|
||||
keyID := string(keyIDBytes)
|
||||
if sseCustomerAlgorithm == "" && uploadEntry.Extended != nil {
|
||||
if keyIDBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSKeyID]; exists {
|
||||
keyID := string(keyIDBytes)
|
||||
|
||||
// Build SSE-KMS metadata for this part
|
||||
bucketKeyEnabled := false
|
||||
if bucketKeyBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSBucketKeyEnabled]; exists && string(bucketKeyBytes) == "true" {
|
||||
bucketKeyEnabled = true
|
||||
}
|
||||
bucketKeyEnabled := false
|
||||
if bucketKeyBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSBucketKeyEnabled]; exists && string(bucketKeyBytes) == "true" {
|
||||
bucketKeyEnabled = true
|
||||
}
|
||||
|
||||
var encryptionContext map[string]string
|
||||
if contextBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSEncryptionContext]; exists {
|
||||
// Parse the stored encryption context
|
||||
if err := json.Unmarshal(contextBytes, &encryptionContext); err != nil {
|
||||
glog.Errorf("Failed to parse encryption context for upload %s: %v", uploadID, err)
|
||||
encryptionContext = BuildEncryptionContext(bucket, object, bucketKeyEnabled)
|
||||
}
|
||||
} else {
|
||||
encryptionContext = BuildEncryptionContext(bucket, object, bucketKeyEnabled)
|
||||
}
|
||||
|
||||
// Get the base IV for this multipart upload
|
||||
var baseIV []byte
|
||||
if baseIVBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSBaseIV]; exists {
|
||||
// Decode the base64 encoded base IV
|
||||
decodedIV, decodeErr := base64.StdEncoding.DecodeString(string(baseIVBytes))
|
||||
if decodeErr == nil && len(decodedIV) == s3_constants.AESBlockSize {
|
||||
baseIV = decodedIV
|
||||
glog.V(4).Infof("Using stored base IV %x for multipart upload %s", baseIV[:8], uploadID)
|
||||
} else {
|
||||
glog.Errorf("Failed to decode base IV for multipart upload %s: %v (expected %d bytes, got %d)", uploadID, decodeErr, s3_constants.AESBlockSize, len(decodedIV))
|
||||
}
|
||||
}
|
||||
|
||||
// Base IV is required for SSE-KMS multipart uploads - fail if missing or invalid
|
||||
if len(baseIV) == 0 {
|
||||
glog.Errorf("No valid base IV found for SSE-KMS multipart upload %s - cannot proceed with encryption", uploadID)
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
||||
return
|
||||
}
|
||||
|
||||
// Add SSE-KMS headers to the request for putToFiler to handle encryption
|
||||
r.Header.Set(s3_constants.AmzServerSideEncryption, "aws:kms")
|
||||
r.Header.Set(s3_constants.AmzServerSideEncryptionAwsKmsKeyId, keyID)
|
||||
if bucketKeyEnabled {
|
||||
r.Header.Set(s3_constants.AmzServerSideEncryptionBucketKeyEnabled, "true")
|
||||
}
|
||||
if len(encryptionContext) > 0 {
|
||||
if contextJSON, err := json.Marshal(encryptionContext); err == nil {
|
||||
r.Header.Set(s3_constants.AmzServerSideEncryptionContext, base64.StdEncoding.EncodeToString(contextJSON))
|
||||
}
|
||||
}
|
||||
|
||||
// Pass the base IV to putToFiler via header
|
||||
r.Header.Set(s3_constants.SeaweedFSSSEKMSBaseIVHeader, base64.StdEncoding.EncodeToString(baseIV))
|
||||
var encryptionContext map[string]string
|
||||
if contextBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSEncryptionContext]; exists {
|
||||
if err := json.Unmarshal(contextBytes, &encryptionContext); err != nil {
|
||||
glog.Errorf("Failed to parse encryption context for upload %s: %v", uploadID, err)
|
||||
encryptionContext = BuildEncryptionContext(bucket, object, bucketKeyEnabled)
|
||||
}
|
||||
} else {
|
||||
encryptionContext = BuildEncryptionContext(bucket, object, bucketKeyEnabled)
|
||||
}
|
||||
|
||||
var baseIV []byte
|
||||
if baseIVBytes, exists := uploadEntry.Extended[s3_constants.SeaweedFSSSEKMSBaseIV]; exists {
|
||||
decodedIV, decodeErr := base64.StdEncoding.DecodeString(string(baseIVBytes))
|
||||
if decodeErr != nil {
|
||||
glog.Errorf("Failed to decode base IV for multipart upload %s: %v", uploadID, decodeErr)
|
||||
} else if len(decodedIV) != s3_constants.AESBlockSize {
|
||||
glog.Errorf("Invalid base IV length for multipart upload %s: expected %d bytes, got %d", uploadID, s3_constants.AESBlockSize, len(decodedIV))
|
||||
} else {
|
||||
// Check if this upload uses SSE-S3
|
||||
if err := s3a.handleSSES3MultipartHeaders(r, uploadEntry, uploadID); err != nil {
|
||||
glog.Errorf("Failed to setup SSE-S3 multipart headers: %v", err)
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
||||
return
|
||||
}
|
||||
baseIV = decodedIV
|
||||
glog.V(4).Infof("Using stored base IV %x for multipart upload %s", baseIV[:8], uploadID)
|
||||
}
|
||||
}
|
||||
} else if !errors.Is(err, filer_pb.ErrNotFound) {
|
||||
// Log unexpected errors (but not "not found" which is normal for non-SSE uploads)
|
||||
glog.V(3).Infof("Could not retrieve upload entry for %s/%s: %v (may be non-SSE upload)", bucket, uploadID, err)
|
||||
|
||||
if len(baseIV) == 0 {
|
||||
glog.Errorf("No valid base IV found for SSE-KMS multipart upload %s - cannot proceed with encryption", uploadID)
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
||||
return
|
||||
}
|
||||
|
||||
r.Header.Set(s3_constants.AmzServerSideEncryption, "aws:kms")
|
||||
r.Header.Set(s3_constants.AmzServerSideEncryptionAwsKmsKeyId, keyID)
|
||||
if bucketKeyEnabled {
|
||||
r.Header.Set(s3_constants.AmzServerSideEncryptionBucketKeyEnabled, "true")
|
||||
}
|
||||
if len(encryptionContext) > 0 {
|
||||
if contextJSON, err := json.Marshal(encryptionContext); err == nil {
|
||||
r.Header.Set(s3_constants.AmzServerSideEncryptionContext, base64.StdEncoding.EncodeToString(contextJSON))
|
||||
}
|
||||
}
|
||||
|
||||
r.Header.Set(s3_constants.SeaweedFSSSEKMSBaseIVHeader, base64.StdEncoding.EncodeToString(baseIV))
|
||||
} else {
|
||||
if err := s3a.handleSSES3MultipartHeaders(r, uploadEntry, uploadID); err != nil {
|
||||
glog.Errorf("Failed to setup SSE-S3 multipart headers: %v", err)
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user