mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-20 13:32:35 +00:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2cab6de7d | ||
|
|
3a765df2ff | ||
|
|
bb151d8e57 | ||
|
|
9434d3733d | ||
|
|
d6a872c4b9 | ||
|
|
7f0cf72574 | ||
|
|
90277ceed5 | ||
|
|
ae170f1fbb | ||
|
|
8e7b15a995 | ||
|
|
cc781f57dc | ||
|
|
7fbdb9b7b7 | ||
|
|
ba855f9962 | ||
|
|
002e325b74 | ||
|
|
1413822424 | ||
|
|
5f2244d25d | ||
|
|
51ec0d2122 | ||
|
|
6ccda3e809 | ||
|
|
08b79a30f6 | ||
|
|
80f3079d2a |
@@ -1,54 +0,0 @@
|
||||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Optional: Filter by PR author
|
||||
# if: |
|
||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
||||
# github.event.pull_request.user.login == 'new-developer' ||
|
||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Claude Code
|
||||
run: npm install -g @anthropic-ai/claude-code
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
path_to_claude_code_executable: claude
|
||||
allowed_bots: "*"
|
||||
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
||||
plugins: 'code-review@claude-code-plugins'
|
||||
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
|
||||
@@ -21,213 +21,60 @@ concurrency:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TEST_TIMEOUT: '45m'
|
||||
|
||||
jobs:
|
||||
fuse-integration:
|
||||
name: FUSE Integration Testing
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 50
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
|
||||
- name: Install FUSE and dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y fuse libfuse-dev
|
||||
# fuse3 is pre-installed on ubuntu-22.04 runners and conflicts
|
||||
# with the legacy fuse package, so only install the dev headers.
|
||||
sudo apt-get install -y libfuse3-dev
|
||||
# Allow non-root FUSE mounts with allow_other
|
||||
echo 'user_allow_other' | sudo tee -a /etc/fuse.conf
|
||||
sudo chmod 644 /etc/fuse.conf
|
||||
# Verify FUSE installation
|
||||
fusermount --version || true
|
||||
ls -la /dev/fuse || true
|
||||
|
||||
fusermount3 --version || fusermount --version || true
|
||||
ls -la /dev/fuse
|
||||
|
||||
- name: Build SeaweedFS
|
||||
run: |
|
||||
cd weed
|
||||
go build -tags "elastic gocdk sqlite ydb tarantool tikv rclone" -v .
|
||||
go build -tags "elastic gocdk sqlite ydb tarantool tikv rclone" -o weed .
|
||||
chmod +x weed
|
||||
# Verify binary
|
||||
./weed version
|
||||
|
||||
- name: Prepare FUSE Integration Tests
|
||||
# Make weed binary available in PATH for the test framework
|
||||
sudo cp weed /usr/local/bin/weed
|
||||
|
||||
- name: Install test dependencies
|
||||
run: |
|
||||
# Create isolated test directory to avoid Go module conflicts
|
||||
mkdir -p /tmp/seaweedfs-fuse-tests
|
||||
|
||||
# Copy only the working test files to avoid Go module conflicts
|
||||
# These are the files we've verified work without package name issues
|
||||
cp test/fuse_integration/simple_test.go /tmp/seaweedfs-fuse-tests/ 2>/dev/null || echo "⚠️ simple_test.go not found"
|
||||
cp test/fuse_integration/working_demo_test.go /tmp/seaweedfs-fuse-tests/ 2>/dev/null || echo "⚠️ working_demo_test.go not found"
|
||||
|
||||
# Note: Other test files (framework.go, basic_operations_test.go, etc.)
|
||||
# have Go module conflicts and are skipped until resolved
|
||||
|
||||
echo "📁 Working test files copied:"
|
||||
ls -la /tmp/seaweedfs-fuse-tests/*.go 2>/dev/null || echo "ℹ️ No test files found"
|
||||
|
||||
# Initialize Go module in isolated directory
|
||||
cd /tmp/seaweedfs-fuse-tests
|
||||
go mod init seaweedfs-fuse-tests
|
||||
go mod tidy
|
||||
|
||||
# Verify setup
|
||||
echo "✅ FUSE integration test environment prepared"
|
||||
ls -la /tmp/seaweedfs-fuse-tests/
|
||||
|
||||
echo ""
|
||||
echo "ℹ️ Current Status: Running working subset of FUSE tests"
|
||||
echo " • simple_test.go: Package structure verification"
|
||||
echo " • working_demo_test.go: Framework capability demonstration"
|
||||
echo " • Full framework: Available in test/fuse_integration/ (module conflicts pending resolution)"
|
||||
|
||||
cd test/fuse_integration
|
||||
go mod download
|
||||
|
||||
- name: Run FUSE Integration Tests
|
||||
run: |
|
||||
cd /tmp/seaweedfs-fuse-tests
|
||||
|
||||
echo "🧪 Running FUSE integration tests..."
|
||||
echo "============================================"
|
||||
|
||||
# Run available working test files
|
||||
TESTS_RUN=0
|
||||
|
||||
if [ -f "simple_test.go" ]; then
|
||||
echo "📋 Running simple_test.go..."
|
||||
go test -v -timeout=${{ env.TEST_TIMEOUT }} simple_test.go
|
||||
TESTS_RUN=$((TESTS_RUN + 1))
|
||||
fi
|
||||
|
||||
if [ -f "working_demo_test.go" ]; then
|
||||
echo "📋 Running working_demo_test.go..."
|
||||
go test -v -timeout=${{ env.TEST_TIMEOUT }} working_demo_test.go
|
||||
TESTS_RUN=$((TESTS_RUN + 1))
|
||||
fi
|
||||
|
||||
# Run combined test if multiple files exist
|
||||
if [ -f "simple_test.go" ] && [ -f "working_demo_test.go" ]; then
|
||||
echo "📋 Running combined tests..."
|
||||
go test -v -timeout=${{ env.TEST_TIMEOUT }} simple_test.go working_demo_test.go
|
||||
fi
|
||||
|
||||
if [ $TESTS_RUN -eq 0 ]; then
|
||||
echo "⚠️ No working test files found, running module verification only"
|
||||
go version
|
||||
go mod verify
|
||||
else
|
||||
echo "✅ Successfully ran $TESTS_RUN test file(s)"
|
||||
fi
|
||||
|
||||
echo "============================================"
|
||||
echo "✅ FUSE integration tests completed"
|
||||
|
||||
- name: Run Extended Framework Validation
|
||||
run: |
|
||||
cd /tmp/seaweedfs-fuse-tests
|
||||
|
||||
echo "🔍 Running extended framework validation..."
|
||||
echo "============================================"
|
||||
|
||||
# Test individual components (only run tests that exist)
|
||||
if [ -f "simple_test.go" ]; then
|
||||
echo "Testing simple verification..."
|
||||
go test -v simple_test.go
|
||||
fi
|
||||
|
||||
if [ -f "working_demo_test.go" ]; then
|
||||
echo "Testing framework demo..."
|
||||
go test -v working_demo_test.go
|
||||
fi
|
||||
|
||||
# Test combined execution if both files exist
|
||||
if [ -f "simple_test.go" ] && [ -f "working_demo_test.go" ]; then
|
||||
echo "Testing combined execution..."
|
||||
go test -v simple_test.go working_demo_test.go
|
||||
elif [ -f "simple_test.go" ] || [ -f "working_demo_test.go" ]; then
|
||||
echo "✅ Individual tests already validated above"
|
||||
else
|
||||
echo "⚠️ No working test files found for combined testing"
|
||||
fi
|
||||
|
||||
echo "============================================"
|
||||
echo "✅ Extended validation completed"
|
||||
|
||||
- name: Generate Test Coverage Report
|
||||
run: |
|
||||
cd /tmp/seaweedfs-fuse-tests
|
||||
|
||||
echo "📊 Generating test coverage report..."
|
||||
go test -v -coverprofile=coverage.out .
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
echo "Coverage report generated: coverage.html"
|
||||
|
||||
- name: Verify SeaweedFS Binary Integration
|
||||
run: |
|
||||
# Test that SeaweedFS binary is accessible from test environment
|
||||
WEED_BINARY=$(pwd)/weed/weed
|
||||
|
||||
if [ -f "$WEED_BINARY" ]; then
|
||||
echo "✅ SeaweedFS binary found at: $WEED_BINARY"
|
||||
$WEED_BINARY version
|
||||
echo "Binary is ready for full integration testing"
|
||||
else
|
||||
echo "❌ SeaweedFS binary not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload Test Artifacts
|
||||
set -o pipefail
|
||||
cd test/fuse_integration
|
||||
echo "Running full FUSE integration test suite..."
|
||||
go test -v -count=1 -timeout=45m ./... 2>&1 | tee /tmp/fuse-test-output.log
|
||||
|
||||
- name: Upload Test Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: fuse-integration-test-results
|
||||
path: |
|
||||
/tmp/seaweedfs-fuse-tests/coverage.out
|
||||
/tmp/seaweedfs-fuse-tests/coverage.html
|
||||
/tmp/seaweedfs-fuse-tests/*.log
|
||||
/tmp/fuse-test-output.log
|
||||
retention-days: 7
|
||||
|
||||
- name: Test Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## 🚀 FUSE Integration Test Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Framework Status" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ **Framework Design**: Complete and validated" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ **Working Tests**: Core framework demonstration functional" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ⚠️ **Full Framework**: Available but requires Go module resolution" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ **CI/CD Integration**: Automated testing pipeline established" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Test Capabilities" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 📁 **File Operations**: Create, read, write, delete, permissions" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 📂 **Directory Operations**: Create, list, delete, nested structures" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 📊 **Large Files**: Multi-megabyte file handling" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🔄 **Concurrent Operations**: Multi-threaded stress testing" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ⚠️ **Error Scenarios**: Comprehensive error handling validation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Comparison with Current Tests" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Aspect | Current (FIO) | This Framework |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|--------|---------------|----------------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Scope** | Performance only | Functional + Performance |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Operations** | Read/Write only | All FUSE operations |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Concurrency** | Single-threaded | Multi-threaded stress tests |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Automation** | Manual setup | Fully automated |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Validation** | Speed metrics | Correctness + Performance |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Current Working Tests" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ **Framework Structure**: Package and module verification" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ **Configuration Management**: Test config validation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ **File Operations Demo**: Basic file create/read/write simulation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ **Large File Handling**: 1MB+ file processing demonstration" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ **Concurrency Simulation**: Multi-file operation testing" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Next Steps" >> $GITHUB_STEP_SUMMARY
|
||||
echo "1. **Module Resolution**: Fix Go package conflicts for full framework" >> $GITHUB_STEP_SUMMARY
|
||||
echo "2. **SeaweedFS Integration**: Connect with real cluster for end-to-end testing" >> $GITHUB_STEP_SUMMARY
|
||||
echo "3. **Performance Benchmarks**: Add performance regression testing" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "📈 **Total Framework Size**: ~1,500 lines of comprehensive testing infrastructure" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -47,6 +47,14 @@ jobs:
|
||||
echo "=== Running S3 DeleteBucketNotEmpty Tests ==="
|
||||
go test -v -timeout=60s -run TestS3DeleteBucketNotEmpty ./...
|
||||
|
||||
- name: Run S3 Empty Directory Marker Tests
|
||||
timeout-minutes: 15
|
||||
working-directory: test/s3/normal
|
||||
run: |
|
||||
set -x
|
||||
echo "=== Running S3 Empty Directory Marker Tests ==="
|
||||
go test -v -timeout=180s -run TestS3ListObjectsEmptyDirectoryMarkers ./...
|
||||
|
||||
- name: Run IAM Integration Tests
|
||||
timeout-minutes: 15
|
||||
working-directory: test/s3/normal
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'weed/s3api/**'
|
||||
- 'weed/filer/**'
|
||||
- 'test/s3/spark/**'
|
||||
- 'test/s3tables/testutil/**'
|
||||
- '.github/workflows/s3-spark-tests.yml'
|
||||
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
|
||||
- name: Publish test report
|
||||
if: always()
|
||||
uses: dorny/test-reporter@v2
|
||||
uses: dorny/test-reporter@v3
|
||||
with:
|
||||
name: Spark Test Results
|
||||
path: test/java/spark/target/surefire-reports/*.xml
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Telemetry Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'telemetry/**'
|
||||
- 'weed/telemetry/**'
|
||||
- '.github/workflows/telemetry-integration.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'telemetry/**'
|
||||
- 'weed/telemetry/**'
|
||||
- '.github/workflows/telemetry-integration.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
telemetry-integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Build telemetry server
|
||||
run: cd telemetry/server && go build -o telemetry-server .
|
||||
|
||||
- name: Run telemetry integration test
|
||||
run: go run telemetry/test/integration.go
|
||||
|
||||
- name: Upload test logs on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: telemetry-test-logs
|
||||
path: telemetry-server-test.log
|
||||
retention-days: 7
|
||||
@@ -141,4 +141,5 @@ test/s3/iam/.test_env
|
||||
/test/erasure_coding/admin_dockertest/tmp
|
||||
/test/erasure_coding/admin_dockertest/task_logs
|
||||
weed_bin
|
||||
telemetry/server/telemetry-server
|
||||
.aider*
|
||||
|
||||
@@ -5,7 +5,7 @@ go 1.25.0
|
||||
require (
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/pubsub v1.50.1
|
||||
cloud.google.com/go/storage v1.59.2
|
||||
cloud.google.com/go/storage v1.60.0
|
||||
github.com/Shopify/sarama v1.38.1
|
||||
github.com/aws/aws-sdk-go v1.55.8
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
@@ -28,13 +28,13 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-redsync/redsync/v4 v4.16.0
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/go-zookeeper/zk v1.0.3 // indirect
|
||||
github.com/go-zookeeper/zk v1.0.4 // indirect
|
||||
github.com/golang/protobuf v1.5.4
|
||||
github.com/golang/snappy v1.0.0
|
||||
github.com/google/btree v1.1.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/wire v0.7.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
@@ -93,16 +93,16 @@ require (
|
||||
gocloud.dev/pubsub/natspubsub v0.45.0
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.44.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
|
||||
golang.org/x/image v0.36.0
|
||||
golang.org/x/net v0.49.0
|
||||
golang.org/x/net v0.50.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/tools v0.41.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.258.0
|
||||
google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||
google.golang.org/api v0.267.0
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/grpc v1.79.3
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
@@ -114,20 +114,20 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/kms v1.23.2
|
||||
cloud.google.com/go/kms v1.25.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0
|
||||
github.com/Jille/raft-grpc-transport v1.6.1
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1
|
||||
github.com/a-h/templ v0.3.977
|
||||
github.com/apache/cassandra-gocql-driver/v2 v2.0.0
|
||||
github.com/apache/iceberg-go v0.4.0
|
||||
github.com/apache/iceberg-go v0.5.0
|
||||
github.com/apple/foundationdb/bindings/go v0.0.0-20250911184653-27f7192f47c3
|
||||
github.com/arangodb/go-driver v1.6.9
|
||||
github.com/armon/go-metrics v0.4.1
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.4
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.7
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
|
||||
github.com/cognusion/imaging v1.0.2
|
||||
github.com/fluent/fluent-logger-golang v1.10.1
|
||||
github.com/getsentry/sentry-go v0.43.0
|
||||
@@ -152,7 +152,7 @@ require (
|
||||
github.com/seaweedfs/go-fuse/v2 v2.9.1
|
||||
github.com/shirou/gopsutil/v4 v4.26.2
|
||||
github.com/tarantool/go-tarantool/v2 v2.4.2
|
||||
github.com/testcontainers/testcontainers-go v0.39.0
|
||||
github.com/testcontainers/testcontainers-go v0.40.0
|
||||
github.com/tikv/client-go/v2 v2.0.7
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/ydb-platform/ydb-go-sdk-auth-environ v0.5.1
|
||||
@@ -170,18 +170,16 @@ require (
|
||||
atomicgo.dev/cursor v0.2.0 // indirect
|
||||
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||
atomicgo.dev/schedule v0.1.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.7.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.8.0 // indirect
|
||||
cloud.google.com/go/pubsub/v2 v2.3.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
|
||||
github.com/FilenCloudDienste/filen-sdk-go v0.0.37 // indirect
|
||||
github.com/a1ex3/zstd-seekable-format-go/pkg v0.10.0 // indirect
|
||||
github.com/anchore/go-lzo v0.1.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/apache/arrow-go/v18 v18.4.1 // indirect
|
||||
github.com/apache/arrow-go/v18 v18.5.2-0.20260220015023-a886a5722b87 // indirect
|
||||
github.com/apache/thrift v0.22.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
@@ -207,7 +205,7 @@ require (
|
||||
github.com/dave/dst v0.27.2 // indirect
|
||||
github.com/diskfs/go-diskfs v1.7.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/docker v28.5.0+incompatible // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dromara/dongle v1.0.1 // indirect
|
||||
@@ -220,7 +218,7 @@ require (
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/hamba/avro/v2 v2.30.0 // indirect
|
||||
github.com/hamba/avro/v2 v2.31.0 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
@@ -258,7 +256,7 @@ require (
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
|
||||
github.com/pierrre/geohash v1.0.0 // indirect
|
||||
github.com/pquerna/otp v1.5.0 // indirect
|
||||
github.com/pterm/pterm v0.12.81 // indirect
|
||||
github.com/pterm/pterm v0.12.82 // indirect
|
||||
github.com/quic-go/quic-go v0.57.0 // indirect
|
||||
github.com/rclone/Proton-API-Bridge v1.0.1-0.20260127174007-77f974840d11 // indirect
|
||||
github.com/rclone/go-proton-api v1.0.1-0.20260127173028-eb465cac3b18 // indirect
|
||||
@@ -267,9 +265,9 @@ require (
|
||||
github.com/sasha-s/go-deadlock v0.3.1 // indirect
|
||||
github.com/smarty/assertions v1.15.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/substrait-io/substrait v0.69.0 // indirect
|
||||
github.com/substrait-io/substrait-go/v4 v4.4.0 // indirect
|
||||
github.com/substrait-io/substrait-protobuf/go v0.71.0 // indirect
|
||||
github.com/substrait-io/substrait v0.81.0 // indirect
|
||||
github.com/substrait-io/substrait-go/v7 v7.4.0 // indirect
|
||||
github.com/substrait-io/substrait-protobuf/go v0.81.0 // indirect
|
||||
github.com/twpayne/go-geom v1.6.1 // indirect
|
||||
github.com/twpayne/go-kml/v3 v3.2.1 // indirect
|
||||
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
|
||||
@@ -278,7 +276,7 @@ require (
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.36.0 // indirect
|
||||
@@ -286,14 +284,14 @@ require (
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect
|
||||
gonum.org/v1/gonum v0.16.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect
|
||||
gonum.org/v1/gonum v0.17.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.3 // indirect
|
||||
@@ -308,8 +306,8 @@ require (
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||
github.com/Files-com/files-sdk-go/v3 v3.2.264 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
|
||||
github.com/IBM/go-sdk-core/v5 v5.21.0 // indirect
|
||||
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
@@ -331,11 +329,11 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.39.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.42.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
|
||||
@@ -389,7 +387,7 @@ require (
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
@@ -502,13 +500,13 @@ require (
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
golang.org/x/term v0.40.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // 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
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/validator.v2 v2.0.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/libc v1.68.0 // indirect
|
||||
moul.io/http2curl/v2 v2.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
storj.io/common v0.0.0-20251107171817-6221ae45072c // indirect
|
||||
|
||||
@@ -94,8 +94,8 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo
|
||||
cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=
|
||||
cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
|
||||
cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
|
||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
|
||||
@@ -298,8 +298,8 @@ cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxs
|
||||
cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg=
|
||||
cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0=
|
||||
cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w=
|
||||
cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k=
|
||||
cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g=
|
||||
cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=
|
||||
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
|
||||
cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=
|
||||
cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=
|
||||
cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE=
|
||||
@@ -315,8 +315,8 @@ cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5P
|
||||
cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE=
|
||||
cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc=
|
||||
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
|
||||
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
|
||||
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
|
||||
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||
cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE=
|
||||
cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM=
|
||||
cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA=
|
||||
@@ -485,8 +485,8 @@ cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq
|
||||
cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
|
||||
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
|
||||
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
|
||||
cloud.google.com/go/storage v1.59.2 h1:gmOAuG1opU8YvycMNpP+DvHfT9BfzzK5Cy+arP+Nocw=
|
||||
cloud.google.com/go/storage v1.59.2/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
|
||||
cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8=
|
||||
cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0=
|
||||
cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w=
|
||||
cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I=
|
||||
cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4=
|
||||
@@ -553,8 +553,6 @@ gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zum
|
||||
git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||
@@ -601,12 +599,12 @@ github.com/Files-com/files-sdk-go/v3 v3.2.264 h1:lMHTplAYI9FtmCo/QOcpRxmPA5REVAc
|
||||
github.com/Files-com/files-sdk-go/v3 v3.2.264/go.mod h1:wGqkOzRu/ClJibvDgcfuJNAqI2nLhe8g91tPlDKRCdE=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||
github.com/IBM/go-sdk-core/v5 v5.21.0 h1:DUnYhvC4SoC8T84rx5omnhY3+xcQg/Whyoa3mDPIMkk=
|
||||
github.com/IBM/go-sdk-core/v5 v5.21.0/go.mod h1:Q3BYO6iDA2zweQPDGbNTtqft5tDcEpm6RTuqMlPcvbw=
|
||||
github.com/Jille/raft-grpc-transport v1.6.1 h1:gN3sjapb+fVbiebS7AfQQgbV2ecTOI7ur7NPPC7Mhoc=
|
||||
@@ -621,8 +619,8 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/
|
||||
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
||||
github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
|
||||
github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE=
|
||||
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
@@ -691,13 +689,13 @@ github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op h1:Ucf+QxEKMbPogR
|
||||
github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
github.com/apache/arrow-go/v18 v18.4.1 h1:q/jVkBWCJOB9reDgaIZIdruLQUb1kbkvOnOFezVH1C4=
|
||||
github.com/apache/arrow-go/v18 v18.4.1/go.mod h1:tLyFubsAl17bvFdUAy24bsSvA/6ww95Iqi67fTpGu3E=
|
||||
github.com/apache/arrow-go/v18 v18.5.2-0.20260220015023-a886a5722b87 h1:r/gg2gzUXiXoy72VU3jnODh8l/5rL0aslnTAbtmai/U=
|
||||
github.com/apache/arrow-go/v18 v18.5.2-0.20260220015023-a886a5722b87/go.mod h1:IJTMBTlHe7cDOhRh0ioGuEKBl5iTR6xPfl5BN4AgirU=
|
||||
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
|
||||
github.com/apache/cassandra-gocql-driver/v2 v2.0.0 h1:Omnzb1Z/P90Dr2TbVNu54ICQL7TKVIIsJO231w484HU=
|
||||
github.com/apache/cassandra-gocql-driver/v2 v2.0.0/go.mod h1:QH/asJjB3mHvY6Dot6ZKMMpTcOrWJ8i9GhsvG1g0PK4=
|
||||
github.com/apache/iceberg-go v0.4.0 h1:O3RwMyoc4gZpu9b1aBDJBiH14riga68BWypZVaSHZE0=
|
||||
github.com/apache/iceberg-go v0.4.0/go.mod h1:TInpnxHnMdPrsmW/klToy9mM8gs6/6FsI+O2kwtlHMQ=
|
||||
github.com/apache/iceberg-go v0.5.0 h1:wQj4CK5YiXZcB+tj19gWG+Jf1I6MiORQ/StSL/E5gGQ=
|
||||
github.com/apache/iceberg-go v0.5.0/go.mod h1:F/rdP1yZmnO4mQ0Qew2HTGdc+ZV57cRfxbbq/uJm1eM=
|
||||
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
|
||||
github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
|
||||
github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=
|
||||
@@ -720,8 +718,8 @@ github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1h
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
|
||||
@@ -734,18 +732,18 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDk
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.39.7 h1:fovS7qGMT+BBSuifkySdVaMWxXTyaYT6qaBx/1y6Ij4=
|
||||
@@ -873,15 +871,15 @@ github.com/cognusion/imaging v1.0.2 h1:BQwBV8V8eF3+dwffp8Udl9xF1JKh5Z0z5JkJwAi98
|
||||
github.com/cognusion/imaging v1.0.2/go.mod h1:mj7FvH7cT2dlFogQOSUQRtotBxJ4gFQ2ySMSmBm5dSk=
|
||||
github.com/colinmarc/hdfs/v2 v2.4.0 h1:v6R8oBx/Wu9fHpdPoJJjpGSUxo8NhHIwrwsfhFvU9W0=
|
||||
github.com/colinmarc/hdfs/v2 v2.4.0/go.mod h1:0NAO+/3knbMx6+5pCv+Hcbaz4xn/Zzbn9+WIib2rKVI=
|
||||
github.com/compose-spec/compose-go/v2 v2.6.0 h1:/+oBD2ixSENOeN/TlJqWZmUak0xM8A7J08w/z661Wd4=
|
||||
github.com/compose-spec/compose-go/v2 v2.6.0/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA=
|
||||
github.com/compose-spec/compose-go/v2 v2.9.0 h1:UHSv/QHlo6QJtrT4igF1rdORgIUhDo1gWuyJUoiNNIM=
|
||||
github.com/compose-spec/compose-go/v2 v2.9.0/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
|
||||
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0=
|
||||
github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc=
|
||||
github.com/containerd/containerd/v2 v2.0.5 h1:2vg/TjUXnaohAxiHnthQg8K06L9I4gdYEMcOLiMc8BQ=
|
||||
github.com/containerd/containerd/v2 v2.0.5/go.mod h1:Qqo0UN43i2fX1FLkrSTCg6zcHNfjN7gEnx3NPRZI+N0=
|
||||
github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0=
|
||||
github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI=
|
||||
github.com/containerd/containerd/v2 v2.1.5 h1:pWSmPxUszaLZKQPvOx27iD4iH+aM6o0BoN9+hg77cro=
|
||||
github.com/containerd/containerd/v2 v2.1.5/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM=
|
||||
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
|
||||
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
@@ -936,20 +934,20 @@ github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/docker/buildx v0.22.0 h1:pGTcGZa+kxpYUlM/6ACsp1hXhkEDulz++RNXPdE8Afk=
|
||||
github.com/docker/buildx v0.22.0/go.mod h1:ThbnUe4kNiStlq6cLXruElyEdSTdPL3k/QerNUmPvHE=
|
||||
github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A=
|
||||
github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli-docs-tool v0.9.0 h1:CVwQbE+ZziwlPqrJ7LRyUF6GvCA+6gj7MTCsayaK9t0=
|
||||
github.com/docker/cli-docs-tool v0.9.0/go.mod h1:ClrwlNW+UioiRyH9GiAOe1o3J/TsY3Tr1ipoypjAUtc=
|
||||
github.com/docker/compose/v2 v2.35.0 h1:bU23OeFrbGyHYrKijMSEwkOeDg2TLhAGntU2F3hwX1o=
|
||||
github.com/docker/compose/v2 v2.35.0/go.mod h1:S5ejUILn9KTYC6noX3IxznWu3/sb3FxdZqIYbq4seAk=
|
||||
github.com/docker/buildx v0.29.1 h1:58hxM5Z4mnNje3G5NKfULT9xCr8ooM8XFtlfUK9bKaA=
|
||||
github.com/docker/buildx v0.29.1/go.mod h1:J4EFv6oxlPiV1MjO0VyJx2u5tLM7ImDEl9zyB8d4wPI=
|
||||
github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
|
||||
github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU=
|
||||
github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U=
|
||||
github.com/docker/compose/v2 v2.40.2 h1:h2bDBJkOuqmj93XvT2oI0ArPQonE0lGtWiILXdiXvbA=
|
||||
github.com/docker/compose/v2 v2.40.2/go.mod h1:CbSJpKGw20LInVsPjglZ8z7Squ3OBQOD7Ux5nkjGfIU=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v28.5.0+incompatible h1:ZdSQoRUE9XxhFI/B8YLvhnEFMmYN9Pp8Egd2qcaFk1E=
|
||||
github.com/docker/docker v28.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
@@ -1100,14 +1098,14 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM=
|
||||
github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ=
|
||||
github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8=
|
||||
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
|
||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||
@@ -1141,8 +1139,8 @@ github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncV
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
|
||||
github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg=
|
||||
github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
|
||||
github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I=
|
||||
github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
@@ -1286,8 +1284,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
@@ -1298,8 +1296,8 @@ github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK
|
||||
github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
|
||||
github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
|
||||
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||
@@ -1328,8 +1326,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4Zs
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/hamba/avro/v2 v2.30.0 h1:OaIdh0+dZIJ331FO/+YYBwZZRdGVyyHuRSyHsjZLJoA=
|
||||
github.com/hamba/avro/v2 v2.30.0/go.mod h1:X6gDhYv6DQVAT56VqOKuW+PLnQrEQqGB9l1nhlMdAdQ=
|
||||
github.com/hamba/avro/v2 v2.31.0 h1:wv3nmua7lCEIwWsb6vqsTS3pXktTxcKg5eoyNu0VhrU=
|
||||
github.com/hamba/avro/v2 v2.31.0/go.mod h1:t6lJYAGE5Mswfn17zjtyQsssRQgnqO6TXLBCHHWRqrw=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -1393,10 +1391,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/in-toto/in-toto-golang v0.5.0 h1:hb8bgwr0M2hGdDsLjkJ3ZqJ8JFLL/tgYdAxF/XEFBbY=
|
||||
github.com/in-toto/in-toto-golang v0.5.0/go.mod h1:/Rq0IZHLV7Ku5gielPT4wPHJfH1GdHMCq8+WPxw8/BE=
|
||||
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
|
||||
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s=
|
||||
@@ -1471,7 +1467,6 @@ github.com/karlseguin/ccache/v2 v2.0.8 h1:lT38cE//uyf6KcFok0rlgXtGFBWxkI6h/qg4tb
|
||||
github.com/karlseguin/ccache/v2 v2.0.8/go.mod h1:2BDThcfQMf/c0jnZowt16eW405XIqZPavt+HoYEtcxQ=
|
||||
github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003 h1:vJ0Snvo+SLMY72r5J4sEfkuE7AFbixEP2qRbEcum/wA=
|
||||
github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003/go.mod h1:zNBxMY8P21owkeogJELCLeHIt+voOSduHYTFUbwRAV8=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
@@ -1557,8 +1552,6 @@ github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4
|
||||
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
|
||||
@@ -1579,8 +1572,8 @@ github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmL
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mmcloughlin/geohash v0.9.0 h1:FihR004p/aE1Sju6gcVq5OLDqGcMnpBY+8moBqIsVOs=
|
||||
github.com/mmcloughlin/geohash v0.9.0/go.mod h1:oNZxQo5yWJh0eMQEP/8hwQuVx9Z9tjwFUqcTB1SmG0c=
|
||||
github.com/moby/buildkit v0.20.1 h1:sT0ZXhhNo5rVbMcYfgttma3TdUHfO5JjFA0UAL8p9fY=
|
||||
github.com/moby/buildkit v0.20.1/go.mod h1:Rq9nB/fJImdk6QeM0niKtOHJqwKeYMrK847hTTDVuA4=
|
||||
github.com/moby/buildkit v0.25.1 h1:j7IlVkeNbEo+ZLoxdudYCHpmTsbwKvhgc/6UJ/mY/o8=
|
||||
github.com/moby/buildkit v0.25.1/go.mod h1:phM8sdqnvgK2y1dPDnbwI6veUCXHOZ6KFSl6E164tkc=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
@@ -1589,8 +1582,8 @@ github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
|
||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8=
|
||||
github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
|
||||
@@ -1778,8 +1771,8 @@ github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEej
|
||||
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
|
||||
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
|
||||
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
|
||||
github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA=
|
||||
github.com/pterm/pterm v0.12.81/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw=
|
||||
github.com/pterm/pterm v0.12.82 h1:+D9wYhCaeaK0FIQoZtqbNQuNpe2lB2tajKKsTd5paVQ=
|
||||
github.com/pterm/pterm v0.12.82/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw=
|
||||
github.com/putdotio/go-putio/putio v0.0.0-20200123120452-16d982cac2b8 h1:Y258uzXU/potCYnQd1r6wlAnoMB68BiCkCcCnKx1SH8=
|
||||
github.com/putdotio/go-putio/putio v0.0.0-20200123120452-16d982cac2b8/go.mod h1:bSJjRokAHHOhA+XFxplld8w2R/dXLH7Z3BZ532vhFwU=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
@@ -1788,8 +1781,6 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
|
||||
github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc h1:zAsgcP8MhzAbhMnB1QQ2O7ZhWYVGYSR2iVcjzQuPV+o=
|
||||
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc/go.mod h1:S8xSOnV3CgpNrWd0GQ/OoQfMtlg2uPRSuTzcSGrzwK8=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rclone/Proton-API-Bridge v1.0.1-0.20260127174007-77f974840d11 h1:4MI2alxM/Ye2gIRBlYf28JGWTipZ4Zz7yAziPKrttjs=
|
||||
@@ -1839,6 +1830,8 @@ github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDc
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
|
||||
github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
|
||||
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
|
||||
@@ -1851,8 +1844,8 @@ github.com/seaweedfs/goexif v1.0.3 h1:ve/OjI7dxPW8X9YQsv3JuVMaxEyF9Rvfd04ouL+Bz3
|
||||
github.com/seaweedfs/goexif v1.0.3/go.mod h1:Oni780Z236sXpIQzk1XoJlTwqrJ02smEin9zQeff7Fk=
|
||||
github.com/seaweedfs/raft v1.1.7 h1:3mVJZ2p4rdvBtbbrHROPjYKtH+q5qjMBc56G6VRu1kA=
|
||||
github.com/seaweedfs/raft v1.1.7/go.mod h1:fgs/rAVEzjQ7e04XMzG3eJhwZZRmBW+2uRtjakeCGeU=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
@@ -1927,12 +1920,12 @@ github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08Yu
|
||||
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/substrait-io/substrait v0.69.0 h1:qfwUe1qKa3PsCclMpubQOF6nqIqS14geUuvzJ1P7gsM=
|
||||
github.com/substrait-io/substrait v0.69.0/go.mod h1:MPFNw6sToJgpD5Z2rj0rQrdP/Oq8HG7Z2t3CAEHtkHw=
|
||||
github.com/substrait-io/substrait-go/v4 v4.4.0 h1:mFArMNFxlOLyTuhPcaPzZCwYh6kUopTExTy7XOqtYBM=
|
||||
github.com/substrait-io/substrait-go/v4 v4.4.0/go.mod h1:GzpaFqO5VRtMkEjATgRxGK5p82OmEtCmszAVYxE+iWc=
|
||||
github.com/substrait-io/substrait-protobuf/go v0.71.0 h1:vkYGEEPJ8lWSwaJvX7Y+hEmwmrz5/qeDmGI43JpKJZE=
|
||||
github.com/substrait-io/substrait-protobuf/go v0.71.0/go.mod h1:hn+Szm1NmZZc91FwWK9EXD/lmuGBSRTJ5IvHhlG1YnQ=
|
||||
github.com/substrait-io/substrait v0.81.0 h1:0E+0cCOAlCupfKRH85KVf7R4zrODLMP29NoVY3zSYiU=
|
||||
github.com/substrait-io/substrait v0.81.0/go.mod h1:MPFNw6sToJgpD5Z2rj0rQrdP/Oq8HG7Z2t3CAEHtkHw=
|
||||
github.com/substrait-io/substrait-go/v7 v7.4.0 h1:I8VRblvZeDCMQV13eAzVTyyzoRACSwsK4Bh4p+qCjNc=
|
||||
github.com/substrait-io/substrait-go/v7 v7.4.0/go.mod h1:hWZ349MkCNRPMY0WZ9Mo+a+VGeda/x5bGMOl+rIZI1M=
|
||||
github.com/substrait-io/substrait-protobuf/go v0.81.0 h1:/qC1XYKuO4oPdTwLYySuVZ6rq7xVS4E7U07Dcgm4+6U=
|
||||
github.com/substrait-io/substrait-protobuf/go v0.81.0/go.mod h1:hn+Szm1NmZZc91FwWK9EXD/lmuGBSRTJ5IvHhlG1YnQ=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965 h1:1oFLiOyVl+W7bnBzGhf7BbIv9loSFQcieWWYIjLqcAw=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA=
|
||||
github.com/t3rm1n4l/go-mega v0.0.0-20251031123324-a804aaa87491 h1:rrGZv6xYk37hx0tW2sYfgbO0PqStbHqz6Bq6oc9Hurg=
|
||||
@@ -1942,10 +1935,10 @@ github.com/tarantool/go-iproto v1.1.0 h1:HULVOIHsiehI+FnHfM7wMDntuzUddO09DKqu2Wn
|
||||
github.com/tarantool/go-iproto v1.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo=
|
||||
github.com/tarantool/go-tarantool/v2 v2.4.2 h1:rkzYtFhLJLA9RDIhjzN93MJBN5PBxHW4+soq+RB90gE=
|
||||
github.com/tarantool/go-tarantool/v2 v2.4.2/go.mod h1:MTbhdjFc3Jl63Lgi/UJr5D+QbT+QegqOzsNJGmaw7VM=
|
||||
github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts=
|
||||
github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8=
|
||||
github.com/testcontainers/testcontainers-go/modules/compose v0.39.0 h1:N9Kn9UOIq24o3Y01SFDYF5y3hpq4dNBzDS4pynHb/OQ=
|
||||
github.com/testcontainers/testcontainers-go/modules/compose v0.39.0/go.mod h1:7OreVKOBlnD0EmOYXMSBbJkHM8JZQINr34vB0X/NJRs=
|
||||
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
|
||||
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
|
||||
github.com/testcontainers/testcontainers-go/modules/compose v0.40.0 h1:Bj8W7GieY56sRbVJx1yLh0JVEtOQ8SQMhX+jRtzenLA=
|
||||
github.com/testcontainers/testcontainers-go/modules/compose v0.40.0/go.mod h1:fEEGqtsoH1KS+sUi1WG4+vH3fqdCyip1U9Hd8P3SRMA=
|
||||
github.com/the42/cartconvert v0.0.0-20131203171324-aae784c392b8 h1:I4DY8wLxJXCrMYzDM6lKCGc3IQwJX0PlTLsd3nQqI3c=
|
||||
github.com/the42/cartconvert v0.0.0-20131203171324-aae784c392b8/go.mod h1:fWO/msnJVhHqN1yX6OBoxSyfj7TEj1hHiL8bJSQsK30=
|
||||
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
|
||||
@@ -1974,12 +1967,12 @@ github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9R
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 h1:eUk79E1w8yMtXeHSzjKorxuC8qJOnyXQnLaJehxpJaI=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250113203817-b14e27f4135a h1:EfGw4G0x/8qXWgtcZ6KVaPS+wpWOQMaypczzP8ojkMY=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250113203817-b14e27f4135a/go.mod h1:Dl/9oEjK7IqnjAm21Okx/XIxUCFJzvh+XdVHUlBwXTw=
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8=
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f h1:MoxeMfHAe5Qj/ySSBfL8A7l1V+hxuluj8owsIEEZipI=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE=
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
|
||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
|
||||
@@ -2005,22 +1998,22 @@ github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/unknwon/goconfig v1.0.0 h1:rS7O+CmUdli1T+oDm7fYj1MwqNWtEJfNj+FqcUHML8U=
|
||||
github.com/unknwon/goconfig v1.0.0/go.mod h1:qu2ZQ/wcC/if2u32263HTVC39PeOQRSmidQk3DuDFQ8=
|
||||
github.com/uptrace/bun v1.2.15 h1:Ut68XRBLDgp9qG9QBMa9ELWaZOmzHNdczHQdrOZbEFE=
|
||||
github.com/uptrace/bun v1.2.15/go.mod h1:Eghz7NonZMiTX/Z6oKYytJ0oaMEJ/eq3kEV4vSqG038=
|
||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.15 h1:QbXtaIlBwx8z0PctUzAQrg4uxRRAKUhkOV4WJvkNo74=
|
||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.15/go.mod h1:PJxf6utV3uwiBww37CQVD5jvarUKkJHNqSWDO1GkmN4=
|
||||
github.com/uptrace/bun/dialect/mysqldialect v1.2.15 h1:z/Seg0ljdqoATl0RGPBLHkod1bT0RofL5nNvqdt+UcM=
|
||||
github.com/uptrace/bun/dialect/mysqldialect v1.2.15/go.mod h1:VUi7mXAL3ttEphcdDta+dXeB7wyI/uvQiE6G8S8ipSQ=
|
||||
github.com/uptrace/bun/dialect/oracledialect v1.2.15 h1:M7V0BZXlBWirFg5TM/7KZzvB37bHsjslBufOTb/cQqU=
|
||||
github.com/uptrace/bun/dialect/oracledialect v1.2.15/go.mod h1:M79iy3e2GOkQ4ItatKh5Nz3hKrrV14hnCJVhHbpk538=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.15 h1:er+/3giAIqpfrXJw+KP9B7ujyQIi5XkPnFmgjAVL6bA=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.15/go.mod h1:QSiz6Qpy9wlGFsfpf7UMSL6mXAL1jDJhFwuOVacCnOQ=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.15 h1:7upGMVjFRB1oI78GQw6ruNLblYn5CR+kxqcbbeBBils=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.15/go.mod h1:c7YIDaPNS2CU2uI1p7umFuFWkuKbDcPDDvp+DLHZnkI=
|
||||
github.com/uptrace/bun/driver/sqliteshim v1.2.15 h1:M/rZJSjOPV4OmfTVnDPtL+wJmdMTqDUn8cuk5ycfABA=
|
||||
github.com/uptrace/bun/driver/sqliteshim v1.2.15/go.mod h1:YqwxFyvM992XOCpGJtXyKPkgkb+aZpIIMzGbpaw1hIk=
|
||||
github.com/uptrace/bun/extra/bundebug v1.2.15 h1:IY2Z/pVyVg0ApWnQ/pEnwe6BWxlDDATCz7IFZghutCs=
|
||||
github.com/uptrace/bun/extra/bundebug v1.2.15/go.mod h1:JuE+BT7NjTZ9UKr74eC8s9yZ9dnQCeufDwFRTC8w3Xo=
|
||||
github.com/uptrace/bun v1.2.17 h1:3AV30/MrgVIL8haNbIQ7Z4I/eQGmaSlfK2T8W8ZprhM=
|
||||
github.com/uptrace/bun v1.2.17/go.mod h1:wNltaKJk4JtOt4SG5I5zmA7v0/Mzjh1+/S906Rayd3Y=
|
||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.17 h1:xEUH4WamuY9rXT9d8wHVZanhmLJCrc4s4v7frDH/PMc=
|
||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.17/go.mod h1:i1NRx/5cz1nivwtV7FEb/gP3CIbRTj4AQC9/Q0lNVno=
|
||||
github.com/uptrace/bun/dialect/mysqldialect v1.2.17 h1:+Oh9gT8B5XjftyvFD6FLrY3bJqdD4ldpe9ps5IU5uAU=
|
||||
github.com/uptrace/bun/dialect/mysqldialect v1.2.17/go.mod h1:V17S1GY0g1Hp0GD9BziWXQkcrMx5/KDYjRrthS70p7Q=
|
||||
github.com/uptrace/bun/dialect/oracledialect v1.2.17 h1:6HhsUllCrYbLh4H0DHrMwkZQxK8HO1Rac6tYS+js8hQ=
|
||||
github.com/uptrace/bun/dialect/oracledialect v1.2.17/go.mod h1:PftDwlZfheYw6ka1UFCnkIx+fYDIbKvHrM8Uw1Qw1lo=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.17 h1:DFmhOollvbYHvooxoS8ZIbiGC0wXIzstKeFUmWs+TP4=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.17/go.mod h1:ej8ZDsvLETvyELlRDfUtIoA57sWnATv1GhOEVsuVG/k=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.17 h1:ZipEoNr+wQJQleGy2poKSSoaQDavzc+nXTDp3ZzkA0E=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.17/go.mod h1:phXmrxxeYqUhMU09FgazbfNxq9LlArdqjZqHc1ILy9U=
|
||||
github.com/uptrace/bun/driver/sqliteshim v1.2.17 h1:0Xa4FZp93D1LCCaMCiPjFsO36b4aQ1vFdXYD7Zk/WM4=
|
||||
github.com/uptrace/bun/driver/sqliteshim v1.2.17/go.mod h1:MqvqMCAAKNn6M0HF9YK/Z6xrnCP6sih5OZ37AxdAlHw=
|
||||
github.com/uptrace/bun/extra/bundebug v1.2.17 h1:QQh0d3WgJU0NxDjPbA2GOrvSdfs5Jm1KsAZRRr7KyKM=
|
||||
github.com/uptrace/bun/extra/bundebug v1.2.17/go.mod h1:zIN0ah3VkBYt9VKfnQVRSzd7JYKaK4AGyyD39AGwHwg=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/viant/assertly v0.9.0 h1:uB3jO+qmWQcrSCHQRxA2kk88eXAdaklUUDxxCU5wBHQ=
|
||||
@@ -2090,8 +2083,8 @@ github.com/yunify/qingstor-sdk-go/v3 v3.2.0 h1:9sB2WZMgjwSUNZhrgvaNGazVltoFUUfuS
|
||||
github.com/yunify/qingstor-sdk-go/v3 v3.2.0/go.mod h1:KciFNuMu6F4WLk9nGwwK69sCGKLCdd9f97ac/wfumS4=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zclconf/go-cty v1.16.0 h1:xPKEhst+BW5D0wxebMZkxgapvOE/dw7bFTlgSc9nD6w=
|
||||
github.com/zclconf/go-cty v1.16.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||
github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
|
||||
github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A=
|
||||
github.com/zeebo/assert v1.3.1/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
@@ -2101,8 +2094,9 @@ github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
|
||||
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.einride.tech/aip v0.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps=
|
||||
go.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
@@ -2130,24 +2124,24 @@ go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK2
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 h1:4BZHA+B1wXEQoGNHxW8mURaLhcdGwvRnmhGbm+odRbc=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0/go.mod h1:3qi2EEwMgB4xnKgPLqsDP3j9qxnHDZeHsnAxfjQqTko=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 h1:ZsXq73BERAiNuuFXYqP4MR5hBrjXfMGSO+Cx7qoOZiM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0/go.mod h1:hg1zaDMpyZJuUzjFxFsRYBoccE86tM9Uf4IqNMUxvrY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.36.0 h1:s0n95ya5tOG03exJ5JySOdJFtwGo4ZQ+KeY7Zro4CLI=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.36.0/go.mod h1:m9wRxtKA2MZ1HcnNC4BKI+9aYe434qRZTCvI7QGUN7Y=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
@@ -2231,8 +2225,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
@@ -2281,8 +2275,8 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -2350,8 +2344,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.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
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/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=
|
||||
@@ -2512,8 +2506,8 @@ golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo=
|
||||
golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8=
|
||||
golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0=
|
||||
golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -2630,8 +2624,8 @@ golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk=
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -2648,8 +2642,8 @@ gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJ
|
||||
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
|
||||
gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
|
||||
@@ -2709,8 +2703,8 @@ google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/
|
||||
google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
|
||||
google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
|
||||
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
|
||||
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
|
||||
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
|
||||
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
|
||||
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -2844,12 +2838,12 @@ google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ
|
||||
google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
|
||||
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
|
||||
google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846 h1:dDbsTLIK7EzwUq36kCSAsk0slouq/S0tWHeeGi97cD8=
|
||||
google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846/go.mod h1:PP0g88Dz3C7hRAfbQCQggeWAXjuqGsNPLE4s7jh0RGU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -2916,14 +2910,14 @@ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
|
||||
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
@@ -2962,18 +2956,18 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
|
||||
k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0=
|
||||
k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk=
|
||||
k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=
|
||||
k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc=
|
||||
k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs=
|
||||
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
|
||||
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
|
||||
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
|
||||
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
|
||||
k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
|
||||
k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/b v1.0.0 h1:vpvqeyp17ddcQWF29Czawql4lDdABCDRbXRAS4+aF2o=
|
||||
@@ -2989,15 +2983,15 @@ modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWs
|
||||
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws=
|
||||
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
|
||||
modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
@@ -3008,8 +3002,8 @@ modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
|
||||
modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
|
||||
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
|
||||
modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s=
|
||||
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
|
||||
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
|
||||
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
@@ -3046,10 +3040,10 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
storj.io/common v0.0.0-20251107171817-6221ae45072c h1:UDXSrdeLJe3QFouavSW10fYdpclK0YNu3KvQHzqq2+k=
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
module seaweedfs-rdma-sidecar
|
||||
|
||||
go 1.24.9
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/seaweedfs/seaweedfs v0.0.0-00010101000000-000000000000
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||
)
|
||||
@@ -20,8 +20,8 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/prometheus/common v0.67.2 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/seaweedfs/goexif v1.0.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
@@ -31,15 +31,15 @@ require (
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/image v0.34.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/image v0.36.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
// For local development, this replace directive is required to build the sidecar
|
||||
|
||||
@@ -27,8 +27,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -42,92 +42,80 @@ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
|
||||
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/seaweedfs/goexif v1.0.3 h1:ve/OjI7dxPW8X9YQsv3JuVMaxEyF9Rvfd04ouL+Bz30=
|
||||
github.com/seaweedfs/goexif v1.0.3/go.mod h1:Oni780Z236sXpIQzk1XoJlTwqrJ02smEin9zQeff7Fk=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||
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/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -54,7 +54,7 @@ func (h *Handler) CollectTelemetry(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if data.ClusterId == "" || data.Version == "" || data.Os == "" {
|
||||
if data.TopologyId == "" || data.Version == "" || data.Os == "" {
|
||||
http.Error(w, "Missing required fields", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
module github.com/seaweedfs/seaweedfs/telemetry/server
|
||||
|
||||
go 1.25
|
||||
|
||||
toolchain go1.25.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
@@ -15,10 +13,10 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
github.com/prometheus/common v0.67.2 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/seaweedfs/seaweedfs => ../..
|
||||
|
||||
+11
-14
@@ -6,8 +6,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -22,24 +22,21 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
|
||||
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -82,7 +82,7 @@ func (s *PrometheusStorage) StoreTelemetry(data *proto.TelemetryData) error {
|
||||
|
||||
// Update Prometheus metrics
|
||||
labels := prometheus.Labels{
|
||||
"cluster_id": data.ClusterId,
|
||||
"cluster_id": data.TopologyId,
|
||||
"version": data.Version,
|
||||
"os": data.Os,
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func (s *PrometheusStorage) StoreTelemetry(data *proto.TelemetryData) error {
|
||||
s.brokerCount.With(labels).Set(float64(data.BrokerCount))
|
||||
|
||||
infoLabels := prometheus.Labels{
|
||||
"cluster_id": data.ClusterId,
|
||||
"cluster_id": data.TopologyId,
|
||||
"version": data.Version,
|
||||
"os": data.Os,
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func (s *PrometheusStorage) StoreTelemetry(data *proto.TelemetryData) error {
|
||||
s.telemetryReceived.Inc()
|
||||
|
||||
// Store in memory for API endpoints
|
||||
s.instances[data.ClusterId] = &telemetryData{
|
||||
s.instances[data.TopologyId] = &telemetryData{
|
||||
TelemetryData: data,
|
||||
ReceivedAt: time.Now().UTC(),
|
||||
}
|
||||
@@ -219,7 +219,7 @@ func (s *PrometheusStorage) CleanupOldInstances(maxAge time.Duration) {
|
||||
|
||||
// Remove from Prometheus metrics
|
||||
labels := prometheus.Labels{
|
||||
"cluster_id": instance.TelemetryData.ClusterId,
|
||||
"cluster_id": instance.TelemetryData.TopologyId,
|
||||
"version": instance.TelemetryData.Version,
|
||||
"os": instance.TelemetryData.Os,
|
||||
}
|
||||
@@ -228,6 +228,7 @@ func (s *PrometheusStorage) CleanupOldInstances(maxAge time.Duration) {
|
||||
s.totalVolumeCount.Delete(labels)
|
||||
s.filerCount.Delete(labels)
|
||||
s.brokerCount.Delete(labels)
|
||||
s.clusterInfo.Delete(labels)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,16 +85,25 @@ func startTelemetryServer() (*exec.Cmd, error) {
|
||||
return nil, fmt.Errorf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
// Navigate to the server directory (from main seaweedfs directory)
|
||||
serverDir := filepath.Join(testDir, "telemetry", "server")
|
||||
|
||||
cmd := exec.Command("go", "run", ".",
|
||||
"-port="+serverPort,
|
||||
// Use pre-built binary if available (faster in CI), otherwise fall back to go run
|
||||
args := []string{
|
||||
"-port=" + serverPort,
|
||||
"-dashboard=false",
|
||||
"-cleanup=1m",
|
||||
"-max-age=1h")
|
||||
"-max-age=1h",
|
||||
}
|
||||
|
||||
cmd.Dir = serverDir
|
||||
serverBin := filepath.Join(testDir, "telemetry", "server", "telemetry-server")
|
||||
var cmd *exec.Cmd
|
||||
if _, err := os.Stat(serverBin); err == nil {
|
||||
fmt.Printf("Using pre-built binary: %s\n", serverBin)
|
||||
cmd = exec.Command(serverBin, args...)
|
||||
} else {
|
||||
fmt.Println("No pre-built binary found, using go run")
|
||||
serverDir := filepath.Join(testDir, "telemetry", "server")
|
||||
cmd = exec.Command("go", append([]string{"run", "."}, args...)...)
|
||||
cmd.Dir = serverDir
|
||||
}
|
||||
|
||||
// Create log files for server output
|
||||
logFile, err := os.Create("telemetry-server-test.log")
|
||||
@@ -174,9 +183,9 @@ func testProtobufMarshaling() error {
|
||||
}
|
||||
|
||||
// Verify data
|
||||
if testData2.ClusterId != testData.ClusterId {
|
||||
return fmt.Errorf("protobuf data mismatch: expected %s, got %s",
|
||||
testData.ClusterId, testData2.ClusterId)
|
||||
if testData2.TopologyId != testData.TopologyId {
|
||||
return fmt.Errorf("TopologyId mismatch: expected %s, got %s",
|
||||
testData.TopologyId, testData2.TopologyId)
|
||||
}
|
||||
|
||||
if testData2.VolumeServerCount != testData.VolumeServerCount {
|
||||
@@ -190,6 +199,7 @@ func testProtobufMarshaling() error {
|
||||
func testTelemetryClient() error {
|
||||
// Create telemetry client
|
||||
client := telemetry.NewClient(serverURL+"/api/collect", true)
|
||||
client.SetTopologyId("test-topology-12345")
|
||||
|
||||
// Create test data using protobuf format
|
||||
testData := &proto.TelemetryData{
|
||||
|
||||
@@ -386,18 +386,20 @@ func testHighFrequencySmallWrites(t *testing.T, framework *FuseTestFramework) {
|
||||
// Perform many small writes
|
||||
numWrites := 1000
|
||||
writeSize := 100
|
||||
totalSize := int64(0)
|
||||
|
||||
for i := 0; i < numWrites; i++ {
|
||||
data := []byte(fmt.Sprintf("Write %04d: %s\n", i, bytes.Repeat([]byte("x"), writeSize-20)))
|
||||
_, err := file.Write(data)
|
||||
require.NoError(t, err)
|
||||
totalSize += int64(len(data))
|
||||
}
|
||||
file.Close()
|
||||
|
||||
// Verify file size
|
||||
info, err := os.Stat(mountPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, totalSize, info.Size())
|
||||
assert.Equal(t, totalSize, info.Size(), "file size should match total bytes written")
|
||||
}
|
||||
|
||||
// testManySmallFiles tests creating many small files
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -20,6 +21,7 @@ type FuseTestFramework struct {
|
||||
tempDir string
|
||||
mountPoint string
|
||||
dataDir string
|
||||
logDir string
|
||||
masterProcess *os.Process
|
||||
volumeProcess *os.Process
|
||||
filerProcess *os.Process
|
||||
@@ -27,6 +29,9 @@ type FuseTestFramework struct {
|
||||
masterAddr string
|
||||
volumeAddr string
|
||||
filerAddr string
|
||||
masterPort int
|
||||
volumePort int
|
||||
filerPort int
|
||||
weedBinary string
|
||||
isSetup bool
|
||||
}
|
||||
@@ -57,7 +62,9 @@ func DefaultTestConfig() *TestConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// NewFuseTestFramework creates a new FUSE testing framework
|
||||
// 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()
|
||||
@@ -66,27 +73,50 @@ 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{
|
||||
t: t,
|
||||
tempDir: tempDir,
|
||||
mountPoint: filepath.Join(tempDir, "mount"),
|
||||
dataDir: filepath.Join(tempDir, "data"),
|
||||
masterAddr: "127.0.0.1:19333",
|
||||
volumeAddr: "127.0.0.1:18080",
|
||||
filerAddr: "127.0.0.1:18888",
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Setup starts SeaweedFS cluster and mounts FUSE filesystem
|
||||
func (f *FuseTestFramework) Setup(config *TestConfig) error {
|
||||
if f.isSetup {
|
||||
return fmt.Errorf("framework already setup")
|
||||
}
|
||||
|
||||
// Create directories
|
||||
dirs := []string{f.mountPoint, f.dataDir}
|
||||
// Create all required directories upfront
|
||||
dirs := []string{
|
||||
f.mountPoint,
|
||||
f.logDir,
|
||||
filepath.Join(f.dataDir, "master"),
|
||||
filepath.Join(f.dataDir, "volume"),
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s: %v", dir, err)
|
||||
@@ -100,6 +130,7 @@ func (f *FuseTestFramework) Setup(config *TestConfig) error {
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -110,6 +141,7 @@ func (f *FuseTestFramework) Setup(config *TestConfig) error {
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -120,6 +152,7 @@ func (f *FuseTestFramework) Setup(config *TestConfig) error {
|
||||
|
||||
// Wait for filer to be ready
|
||||
if err := f.waitForService(f.filerAddr, 30*time.Second); err != nil {
|
||||
f.dumpLog("filer")
|
||||
return fmt.Errorf("filer not ready: %v", err)
|
||||
}
|
||||
|
||||
@@ -130,6 +163,7 @@ func (f *FuseTestFramework) Setup(config *TestConfig) error {
|
||||
|
||||
// Wait for mount to be ready
|
||||
if err := f.waitForMount(30 * time.Second); err != nil {
|
||||
f.dumpLog("mount")
|
||||
return fmt.Errorf("FUSE mount not ready: %v", err)
|
||||
}
|
||||
|
||||
@@ -168,26 +202,61 @@ func (f *FuseTestFramework) GetFilerAddr() string {
|
||||
return f.filerAddr
|
||||
}
|
||||
|
||||
// startProcess is a helper that starts a weed sub-command with output captured
|
||||
// to a log file in f.logDir.
|
||||
func (f *FuseTestFramework) startProcess(name string, args []string) (*os.Process, error) {
|
||||
logFile, err := os.Create(filepath.Join(f.logDir, name+".log"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create log file: %v", err)
|
||||
}
|
||||
cmd := exec.Command(f.weedBinary, args...)
|
||||
cmd.Dir = f.tempDir
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return nil, err
|
||||
}
|
||||
// Close the file handle — the child process inherited it.
|
||||
logFile.Close()
|
||||
return cmd.Process, nil
|
||||
}
|
||||
|
||||
// dumpLog prints the last lines of a process log file to the test output
|
||||
// for debugging when a service fails to start.
|
||||
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:]
|
||||
}
|
||||
f.t.Logf("[%s log tail]\n%s", name, 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.
|
||||
args := []string{
|
||||
"master",
|
||||
"-ip=127.0.0.1",
|
||||
"-port=19333",
|
||||
"-port=" + strconv.Itoa(f.masterPort),
|
||||
"-mdir=" + filepath.Join(f.dataDir, "master"),
|
||||
"-raftBootstrap",
|
||||
"-peers=none", // Faster startup when no multiple masters needed
|
||||
}
|
||||
if config.EnableDebug {
|
||||
args = append(args, "-v=4")
|
||||
}
|
||||
|
||||
cmd := exec.Command(f.weedBinary, args...)
|
||||
cmd.Dir = f.tempDir
|
||||
if err := cmd.Start(); err != nil {
|
||||
proc, err := f.startProcess("master", args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.masterProcess = cmd.Process
|
||||
f.masterProcess = proc
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -195,9 +264,9 @@ func (f *FuseTestFramework) startMaster(config *TestConfig) error {
|
||||
func (f *FuseTestFramework) startVolumeServers(config *TestConfig) error {
|
||||
args := []string{
|
||||
"volume",
|
||||
"-master=" + f.masterAddr,
|
||||
"-master=127.0.0.1:" + strconv.Itoa(f.masterPort),
|
||||
"-ip=127.0.0.1",
|
||||
"-port=18080",
|
||||
"-port=" + strconv.Itoa(f.volumePort),
|
||||
"-dir=" + filepath.Join(f.dataDir, "volume"),
|
||||
fmt.Sprintf("-max=%d", config.NumVolumes),
|
||||
}
|
||||
@@ -205,12 +274,11 @@ func (f *FuseTestFramework) startVolumeServers(config *TestConfig) error {
|
||||
args = append(args, "-v=4")
|
||||
}
|
||||
|
||||
cmd := exec.Command(f.weedBinary, args...)
|
||||
cmd.Dir = f.tempDir
|
||||
if err := cmd.Start(); err != nil {
|
||||
proc, err := f.startProcess("volume", args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.volumeProcess = cmd.Process
|
||||
f.volumeProcess = proc
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -218,20 +286,19 @@ func (f *FuseTestFramework) startVolumeServers(config *TestConfig) error {
|
||||
func (f *FuseTestFramework) startFiler(config *TestConfig) error {
|
||||
args := []string{
|
||||
"filer",
|
||||
"-master=" + f.masterAddr,
|
||||
"-master=127.0.0.1:" + strconv.Itoa(f.masterPort),
|
||||
"-ip=127.0.0.1",
|
||||
"-port=18888",
|
||||
"-port=" + strconv.Itoa(f.filerPort),
|
||||
}
|
||||
if config.EnableDebug {
|
||||
args = append(args, "-v=4")
|
||||
}
|
||||
|
||||
cmd := exec.Command(f.weedBinary, args...)
|
||||
cmd.Dir = f.tempDir
|
||||
if err := cmd.Start(); err != nil {
|
||||
proc, err := f.startProcess("filer", args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.filerProcess = cmd.Process
|
||||
f.filerProcess = proc
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -239,10 +306,11 @@ func (f *FuseTestFramework) startFiler(config *TestConfig) error {
|
||||
func (f *FuseTestFramework) mountFuse(config *TestConfig) error {
|
||||
args := []string{
|
||||
"mount",
|
||||
"-filer=" + f.filerAddr,
|
||||
"-filer=127.0.0.1:" + strconv.Itoa(f.filerPort),
|
||||
"-dir=" + f.mountPoint,
|
||||
"-filer.path=/",
|
||||
"-dirAutoCreate",
|
||||
"-allowOthers=false",
|
||||
}
|
||||
|
||||
if config.Collection != "" {
|
||||
@@ -255,7 +323,7 @@ func (f *FuseTestFramework) mountFuse(config *TestConfig) error {
|
||||
args = append(args, fmt.Sprintf("-chunkSizeLimitMB=%d", config.ChunkSizeMB))
|
||||
}
|
||||
if config.CacheSizeMB > 0 {
|
||||
args = append(args, fmt.Sprintf("-cacheSizeMB=%d", config.CacheSizeMB))
|
||||
args = append(args, fmt.Sprintf("-cacheCapacityMB=%d", config.CacheSizeMB))
|
||||
}
|
||||
if config.EnableDebug {
|
||||
args = append(args, "-v=4")
|
||||
@@ -263,12 +331,11 @@ func (f *FuseTestFramework) mountFuse(config *TestConfig) error {
|
||||
|
||||
args = append(args, config.MountOptions...)
|
||||
|
||||
cmd := exec.Command(f.weedBinary, args...)
|
||||
cmd.Dir = f.tempDir
|
||||
if err := cmd.Start(); err != nil {
|
||||
proc, err := f.startProcess("mount", args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.mountProcess = cmd.Process
|
||||
f.mountProcess = proc
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -281,7 +348,8 @@ func (f *FuseTestFramework) unmountFuse() error {
|
||||
}
|
||||
|
||||
// Also try system unmount as backup
|
||||
exec.Command("umount", f.mountPoint).Run()
|
||||
exec.Command("fusermount3", "-u", f.mountPoint).Run()
|
||||
exec.Command("fusermount", "-u", f.mountPoint).Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -315,27 +383,31 @@ func (f *FuseTestFramework) waitForMount(timeout time.Duration) error {
|
||||
return fmt.Errorf("mount point not ready within timeout")
|
||||
}
|
||||
|
||||
// findWeedBinary locates the weed binary
|
||||
// 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 {
|
||||
// Try different possible locations
|
||||
candidates := []string{
|
||||
"./weed",
|
||||
"../weed",
|
||||
"../../weed",
|
||||
"weed", // in PATH
|
||||
// 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",
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if _, err := exec.LookPath(candidate); err == nil {
|
||||
return candidate
|
||||
}
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
|
||||
abs, _ := filepath.Abs(candidate)
|
||||
return abs
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
// Default fallback — will fail with a clear "not found" at exec time.
|
||||
return "weed"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,825 @@
|
||||
package fuse_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// writebackConfig returns a TestConfig with writebackCache enabled
|
||||
func writebackConfig() *TestConfig {
|
||||
return &TestConfig{
|
||||
Collection: "",
|
||||
Replication: "000",
|
||||
ChunkSizeMB: 2,
|
||||
CacheSizeMB: 100,
|
||||
NumVolumes: 3,
|
||||
EnableDebug: false,
|
||||
MountOptions: []string{
|
||||
"-writebackCache",
|
||||
},
|
||||
SkipCleanup: false,
|
||||
}
|
||||
}
|
||||
|
||||
// waitForFileContent polls until a file has the expected content or timeout expires.
|
||||
// This is needed because writebackCache defers data upload to background goroutines,
|
||||
// so there is a brief window after close() where the file may not yet be readable.
|
||||
func waitForFileContent(t *testing.T, path string, expected []byte, timeout time.Duration) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
var lastErr error
|
||||
for time.Now().Before(deadline) {
|
||||
actual, err := os.ReadFile(path)
|
||||
if err == nil && bytes.Equal(expected, actual) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
} else {
|
||||
lastErr = fmt.Errorf("content mismatch: got %d bytes, want %d bytes", len(actual), len(expected))
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("file %s did not have expected content within %v: %v", path, timeout, lastErr)
|
||||
}
|
||||
|
||||
// waitForFileSize polls until a file reports the expected size or timeout expires.
|
||||
func waitForFileSize(t *testing.T, path string, expectedSize int64, timeout time.Duration) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
info, err := os.Stat(path)
|
||||
if err == nil && info.Size() == expectedSize {
|
||||
return
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("file %s did not reach expected size %d within %v", path, expectedSize, timeout)
|
||||
}
|
||||
|
||||
// TestWritebackCacheBasicOperations tests fundamental file I/O with writebackCache enabled
|
||||
func TestWritebackCacheBasicOperations(t *testing.T) {
|
||||
config := writebackConfig()
|
||||
framework := NewFuseTestFramework(t, config)
|
||||
defer framework.Cleanup()
|
||||
|
||||
require.NoError(t, framework.Setup(config))
|
||||
|
||||
t.Run("WriteAndReadBack", func(t *testing.T) {
|
||||
testWritebackWriteAndReadBack(t, framework)
|
||||
})
|
||||
|
||||
t.Run("MultipleFilesSequential", func(t *testing.T) {
|
||||
testWritebackMultipleFilesSequential(t, framework)
|
||||
})
|
||||
|
||||
t.Run("LargeFile", func(t *testing.T) {
|
||||
testWritebackLargeFile(t, framework)
|
||||
})
|
||||
|
||||
t.Run("EmptyFile", func(t *testing.T) {
|
||||
testWritebackEmptyFile(t, framework)
|
||||
})
|
||||
|
||||
t.Run("OverwriteExistingFile", func(t *testing.T) {
|
||||
testWritebackOverwriteFile(t, framework)
|
||||
})
|
||||
}
|
||||
|
||||
// testWritebackWriteAndReadBack writes a file and verifies it can be read back
|
||||
// after the async flush completes.
|
||||
func testWritebackWriteAndReadBack(t *testing.T, framework *FuseTestFramework) {
|
||||
filename := "writeback_basic.txt"
|
||||
content := []byte("Hello from writebackCache test!")
|
||||
mountPath := filepath.Join(framework.GetMountPoint(), filename)
|
||||
|
||||
// Write file — close() returns immediately with async flush
|
||||
require.NoError(t, os.WriteFile(mountPath, content, 0644))
|
||||
|
||||
// Wait for async flush to complete and verify content
|
||||
waitForFileContent(t, mountPath, content, 30*time.Second)
|
||||
}
|
||||
|
||||
// testWritebackMultipleFilesSequential writes multiple files sequentially
|
||||
// and verifies all are readable after async flushes complete.
|
||||
func testWritebackMultipleFilesSequential(t *testing.T, framework *FuseTestFramework) {
|
||||
dir := "writeback_sequential"
|
||||
framework.CreateTestDir(dir)
|
||||
|
||||
numFiles := 50
|
||||
files := make(map[string][]byte, numFiles)
|
||||
|
||||
// Write files sequentially — each close() returns immediately
|
||||
for i := 0; i < numFiles; i++ {
|
||||
filename := fmt.Sprintf("file_%03d.txt", i)
|
||||
content := []byte(fmt.Sprintf("Sequential file %d content: %s", i, time.Now().Format(time.RFC3339Nano)))
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, filename)
|
||||
require.NoError(t, os.WriteFile(path, content, 0644))
|
||||
files[filename] = content
|
||||
}
|
||||
|
||||
// Verify all files after a brief wait for async flushes
|
||||
for filename, expectedContent := range files {
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, filename)
|
||||
waitForFileContent(t, path, expectedContent, 30*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// testWritebackLargeFile writes a large file (multi-chunk) with writebackCache
|
||||
func testWritebackLargeFile(t *testing.T, framework *FuseTestFramework) {
|
||||
filename := "writeback_large.bin"
|
||||
mountPath := filepath.Join(framework.GetMountPoint(), filename)
|
||||
|
||||
// 8MB file (spans multiple 2MB chunks)
|
||||
content := make([]byte, 8*1024*1024)
|
||||
_, err := rand.Read(content)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, os.WriteFile(mountPath, content, 0644))
|
||||
|
||||
// Wait for file to be fully flushed
|
||||
waitForFileContent(t, mountPath, content, 60*time.Second)
|
||||
}
|
||||
|
||||
// testWritebackEmptyFile creates an empty file with writebackCache
|
||||
func testWritebackEmptyFile(t *testing.T, framework *FuseTestFramework) {
|
||||
filename := "writeback_empty.txt"
|
||||
mountPath := filepath.Join(framework.GetMountPoint(), filename)
|
||||
|
||||
// Create empty file
|
||||
f, err := os.Create(mountPath)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
// Should exist and be empty
|
||||
info, err := os.Stat(mountPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(0), info.Size())
|
||||
}
|
||||
|
||||
// testWritebackOverwriteFile tests overwriting an existing file
|
||||
func testWritebackOverwriteFile(t *testing.T, framework *FuseTestFramework) {
|
||||
filename := "writeback_overwrite.txt"
|
||||
mountPath := filepath.Join(framework.GetMountPoint(), filename)
|
||||
|
||||
// First write
|
||||
content1 := []byte("First version of the file")
|
||||
require.NoError(t, os.WriteFile(mountPath, content1, 0644))
|
||||
waitForFileContent(t, mountPath, content1, 30*time.Second)
|
||||
|
||||
// Overwrite with different content
|
||||
content2 := []byte("Second version — overwritten content that is longer than the first")
|
||||
require.NoError(t, os.WriteFile(mountPath, content2, 0644))
|
||||
waitForFileContent(t, mountPath, content2, 30*time.Second)
|
||||
}
|
||||
|
||||
// TestWritebackCacheFsync tests that fsync still forces synchronous flush
|
||||
// even when writebackCache is enabled
|
||||
func TestWritebackCacheFsync(t *testing.T) {
|
||||
config := writebackConfig()
|
||||
framework := NewFuseTestFramework(t, config)
|
||||
defer framework.Cleanup()
|
||||
|
||||
require.NoError(t, framework.Setup(config))
|
||||
|
||||
t.Run("FsyncForcesFlush", func(t *testing.T) {
|
||||
testFsyncForcesFlush(t, framework)
|
||||
})
|
||||
|
||||
t.Run("FsyncThenRead", func(t *testing.T) {
|
||||
testFsyncThenRead(t, framework)
|
||||
})
|
||||
}
|
||||
|
||||
// testFsyncForcesFlush verifies that calling fsync before close ensures
|
||||
// data is immediately available for reading, bypassing the async path.
|
||||
func testFsyncForcesFlush(t *testing.T, framework *FuseTestFramework) {
|
||||
filename := "writeback_fsync.txt"
|
||||
mountPath := filepath.Join(framework.GetMountPoint(), filename)
|
||||
|
||||
content := []byte("Data that must be flushed synchronously via fsync")
|
||||
|
||||
// Open, write, fsync, close
|
||||
f, err := os.OpenFile(mountPath, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = f.Write(content)
|
||||
require.NoError(t, err)
|
||||
|
||||
// fsync forces synchronous data+metadata flush
|
||||
require.NoError(t, f.Sync())
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
// Data should be immediately available — no wait needed
|
||||
actual, err := os.ReadFile(mountPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, content, actual)
|
||||
}
|
||||
|
||||
// testFsyncThenRead verifies that after fsync, a freshly opened read
|
||||
// returns the correct data without any delay.
|
||||
func testFsyncThenRead(t *testing.T, framework *FuseTestFramework) {
|
||||
filename := "writeback_fsync_read.txt"
|
||||
mountPath := filepath.Join(framework.GetMountPoint(), filename)
|
||||
|
||||
content := make([]byte, 64*1024) // 64KB
|
||||
_, err := rand.Read(content)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Write with explicit fsync
|
||||
f, err := os.OpenFile(mountPath, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
require.NoError(t, err)
|
||||
_, err = f.Write(content)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Sync())
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
// Immediate read should succeed
|
||||
actual, err := os.ReadFile(mountPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, content, actual)
|
||||
}
|
||||
|
||||
// TestWritebackCacheConcurrentSmallFiles is the primary test for issue #8718:
|
||||
// many small files written concurrently should all be eventually readable.
|
||||
func TestWritebackCacheConcurrentSmallFiles(t *testing.T) {
|
||||
config := writebackConfig()
|
||||
framework := NewFuseTestFramework(t, config)
|
||||
defer framework.Cleanup()
|
||||
|
||||
require.NoError(t, framework.Setup(config))
|
||||
|
||||
t.Run("ConcurrentSmallFiles", func(t *testing.T) {
|
||||
testWritebackConcurrentSmallFiles(t, framework)
|
||||
})
|
||||
|
||||
t.Run("ConcurrentSmallFilesMultiDir", func(t *testing.T) {
|
||||
testWritebackConcurrentSmallFilesMultiDir(t, framework)
|
||||
})
|
||||
|
||||
t.Run("RapidCreateCloseSequence", func(t *testing.T) {
|
||||
testWritebackRapidCreateClose(t, framework)
|
||||
})
|
||||
}
|
||||
|
||||
// testWritebackConcurrentSmallFiles simulates the rsync workload from #8718:
|
||||
// multiple workers creating many small files in parallel.
|
||||
func testWritebackConcurrentSmallFiles(t *testing.T, framework *FuseTestFramework) {
|
||||
dir := "writeback_concurrent_small"
|
||||
framework.CreateTestDir(dir)
|
||||
|
||||
numWorkers := 8
|
||||
filesPerWorker := 20
|
||||
totalFiles := numWorkers * filesPerWorker
|
||||
|
||||
type fileRecord struct {
|
||||
path string
|
||||
content []byte
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
var writeErrors []error
|
||||
records := make([]fileRecord, 0, totalFiles)
|
||||
|
||||
// Phase 1: Write files concurrently (simulating rsync workers)
|
||||
var wg sync.WaitGroup
|
||||
for w := 0; w < numWorkers; w++ {
|
||||
wg.Add(1)
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for f := 0; f < filesPerWorker; f++ {
|
||||
filename := fmt.Sprintf("w%02d_f%03d.dat", workerID, f)
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, filename)
|
||||
|
||||
// Vary sizes: 100B to 100KB
|
||||
size := 100 + (workerID*filesPerWorker+f)*500
|
||||
if size > 100*1024 {
|
||||
size = 100*1024
|
||||
}
|
||||
content := make([]byte, size)
|
||||
if _, err := rand.Read(content); err != nil {
|
||||
mu.Lock()
|
||||
writeErrors = append(writeErrors, fmt.Errorf("worker %d file %d rand: %v", workerID, f, err))
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, content, 0644); err != nil {
|
||||
mu.Lock()
|
||||
writeErrors = append(writeErrors, fmt.Errorf("worker %d file %d: %v", workerID, f, err))
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
records = append(records, fileRecord{path: path, content: content})
|
||||
mu.Unlock()
|
||||
}
|
||||
}(w)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
require.Empty(t, writeErrors, "write errors: %v", writeErrors)
|
||||
assert.Equal(t, totalFiles, len(records))
|
||||
|
||||
// Phase 2: Wait for async flushes and verify all files
|
||||
for _, rec := range records {
|
||||
waitForFileContent(t, rec.path, rec.content, 60*time.Second)
|
||||
}
|
||||
|
||||
// Phase 3: Verify directory listing has correct count
|
||||
entries, err := os.ReadDir(filepath.Join(framework.GetMountPoint(), dir))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, totalFiles, len(entries))
|
||||
}
|
||||
|
||||
// testWritebackConcurrentSmallFilesMultiDir tests concurrent writes across
|
||||
// multiple directories — a common pattern for parallel copy tools.
|
||||
func testWritebackConcurrentSmallFilesMultiDir(t *testing.T, framework *FuseTestFramework) {
|
||||
baseDir := "writeback_multidir"
|
||||
framework.CreateTestDir(baseDir)
|
||||
|
||||
numDirs := 4
|
||||
filesPerDir := 25
|
||||
|
||||
type fileRecord struct {
|
||||
path string
|
||||
content []byte
|
||||
}
|
||||
var mu sync.Mutex
|
||||
var records []fileRecord
|
||||
var writeErrors []error
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for d := 0; d < numDirs; d++ {
|
||||
subDir := filepath.Join(baseDir, fmt.Sprintf("dir_%02d", d))
|
||||
framework.CreateTestDir(subDir)
|
||||
|
||||
wg.Add(1)
|
||||
go func(dirID int, dirPath string) {
|
||||
defer wg.Done()
|
||||
|
||||
for f := 0; f < filesPerDir; f++ {
|
||||
filename := fmt.Sprintf("file_%03d.txt", f)
|
||||
path := filepath.Join(framework.GetMountPoint(), dirPath, filename)
|
||||
content := []byte(fmt.Sprintf("dir=%d file=%d data=%s", dirID, f, time.Now().Format(time.RFC3339Nano)))
|
||||
|
||||
if err := os.WriteFile(path, content, 0644); err != nil {
|
||||
mu.Lock()
|
||||
writeErrors = append(writeErrors, fmt.Errorf("dir %d file %d: %v", dirID, f, err))
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
records = append(records, fileRecord{path: path, content: content})
|
||||
mu.Unlock()
|
||||
}
|
||||
}(d, subDir)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
require.Empty(t, writeErrors, "write errors: %v", writeErrors)
|
||||
|
||||
// Verify all files
|
||||
for _, rec := range records {
|
||||
waitForFileContent(t, rec.path, rec.content, 60*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// testWritebackRapidCreateClose rapidly creates and closes files to stress
|
||||
// the async flush goroutine pool.
|
||||
func testWritebackRapidCreateClose(t *testing.T, framework *FuseTestFramework) {
|
||||
dir := "writeback_rapid"
|
||||
framework.CreateTestDir(dir)
|
||||
|
||||
numFiles := 200
|
||||
type fileRecord struct {
|
||||
path string
|
||||
content []byte
|
||||
}
|
||||
records := make([]fileRecord, numFiles)
|
||||
|
||||
// Rapidly create files without pausing
|
||||
for i := 0; i < numFiles; i++ {
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, fmt.Sprintf("rapid_%04d.bin", i))
|
||||
content := []byte(fmt.Sprintf("rapid-file-%d", i))
|
||||
require.NoError(t, os.WriteFile(path, content, 0644))
|
||||
records[i] = fileRecord{path: path, content: content}
|
||||
}
|
||||
|
||||
// Verify all files eventually appear with correct content
|
||||
for _, rec := range records {
|
||||
waitForFileContent(t, rec.path, rec.content, 60*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWritebackCacheDataIntegrity tests that data integrity is preserved
|
||||
// across various write patterns with writebackCache enabled.
|
||||
func TestWritebackCacheDataIntegrity(t *testing.T) {
|
||||
config := writebackConfig()
|
||||
framework := NewFuseTestFramework(t, config)
|
||||
defer framework.Cleanup()
|
||||
|
||||
require.NoError(t, framework.Setup(config))
|
||||
|
||||
t.Run("AppendAfterClose", func(t *testing.T) {
|
||||
testWritebackAppendAfterClose(t, framework)
|
||||
})
|
||||
|
||||
t.Run("PartialWrites", func(t *testing.T) {
|
||||
testWritebackPartialWrites(t, framework)
|
||||
})
|
||||
|
||||
t.Run("FileSizeCorrectness", func(t *testing.T) {
|
||||
testWritebackFileSizeCorrectness(t, framework)
|
||||
})
|
||||
|
||||
t.Run("BinaryData", func(t *testing.T) {
|
||||
testWritebackBinaryData(t, framework)
|
||||
})
|
||||
}
|
||||
|
||||
// testWritebackAppendAfterClose writes a file, closes it (triggering async flush),
|
||||
// waits for flush, then reopens and appends more data.
|
||||
func testWritebackAppendAfterClose(t *testing.T, framework *FuseTestFramework) {
|
||||
filename := "writeback_append.txt"
|
||||
mountPath := filepath.Join(framework.GetMountPoint(), filename)
|
||||
|
||||
// First write
|
||||
part1 := []byte("First part of the data.\n")
|
||||
require.NoError(t, os.WriteFile(mountPath, part1, 0644))
|
||||
|
||||
// Wait for first async flush
|
||||
waitForFileContent(t, mountPath, part1, 30*time.Second)
|
||||
|
||||
// Append more data
|
||||
part2 := []byte("Second part appended.\n")
|
||||
f, err := os.OpenFile(mountPath, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
require.NoError(t, err)
|
||||
_, err = f.Write(part2)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
// Verify combined content
|
||||
expected := append(part1, part2...)
|
||||
waitForFileContent(t, mountPath, expected, 30*time.Second)
|
||||
}
|
||||
|
||||
// testWritebackPartialWrites tests writing to specific offsets within a file
|
||||
func testWritebackPartialWrites(t *testing.T, framework *FuseTestFramework) {
|
||||
filename := "writeback_partial.bin"
|
||||
mountPath := filepath.Join(framework.GetMountPoint(), filename)
|
||||
|
||||
// Create file with initial content
|
||||
initial := bytes.Repeat([]byte("A"), 4096)
|
||||
require.NoError(t, os.WriteFile(mountPath, initial, 0644))
|
||||
waitForFileContent(t, mountPath, initial, 30*time.Second)
|
||||
|
||||
// Open and write at specific offset
|
||||
f, err := os.OpenFile(mountPath, os.O_WRONLY, 0644)
|
||||
require.NoError(t, err)
|
||||
patch := []byte("PATCHED")
|
||||
_, err = f.WriteAt(patch, 100)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
// Build expected content
|
||||
expected := make([]byte, 4096)
|
||||
copy(expected, initial)
|
||||
copy(expected[100:], patch)
|
||||
|
||||
waitForFileContent(t, mountPath, expected, 30*time.Second)
|
||||
}
|
||||
|
||||
// testWritebackFileSizeCorrectness verifies that file sizes are correct
|
||||
// after async flush completes.
|
||||
func testWritebackFileSizeCorrectness(t *testing.T, framework *FuseTestFramework) {
|
||||
sizes := []int{0, 1, 100, 4096, 65536, 1024 * 1024}
|
||||
|
||||
for _, size := range sizes {
|
||||
filename := fmt.Sprintf("writeback_size_%d.bin", size)
|
||||
mountPath := filepath.Join(framework.GetMountPoint(), filename)
|
||||
|
||||
content := make([]byte, size)
|
||||
if size > 0 {
|
||||
_, err := rand.Read(content)
|
||||
require.NoError(t, err, "rand.Read failed for size %d", size)
|
||||
}
|
||||
|
||||
require.NoError(t, os.WriteFile(mountPath, content, 0644), "failed to write file of size %d", size)
|
||||
|
||||
if size > 0 {
|
||||
waitForFileSize(t, mountPath, int64(size), 30*time.Second)
|
||||
waitForFileContent(t, mountPath, content, 30*time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testWritebackBinaryData verifies that arbitrary binary data (including null bytes)
|
||||
// is preserved correctly through the async flush path.
|
||||
func testWritebackBinaryData(t *testing.T, framework *FuseTestFramework) {
|
||||
filename := "writeback_binary.bin"
|
||||
mountPath := filepath.Join(framework.GetMountPoint(), filename)
|
||||
|
||||
// Generate data with all byte values including nulls
|
||||
content := make([]byte, 256*100)
|
||||
for i := range content {
|
||||
content[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
require.NoError(t, os.WriteFile(mountPath, content, 0644))
|
||||
waitForFileContent(t, mountPath, content, 30*time.Second)
|
||||
}
|
||||
|
||||
// TestWritebackCachePerformance measures whether writebackCache actually
|
||||
// improves throughput for small file workloads compared to synchronous flush.
|
||||
func TestWritebackCachePerformance(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping performance test in short mode")
|
||||
}
|
||||
|
||||
numFiles := 200
|
||||
fileSize := 4096 // 4KB files
|
||||
|
||||
// Generate test data upfront
|
||||
testData := make([][]byte, numFiles)
|
||||
for i := range testData {
|
||||
testData[i] = make([]byte, fileSize)
|
||||
rand.Read(testData[i])
|
||||
}
|
||||
|
||||
// Benchmark with writebackCache enabled
|
||||
t.Run("WithWritebackCache", func(t *testing.T) {
|
||||
config := writebackConfig()
|
||||
framework := NewFuseTestFramework(t, config)
|
||||
defer framework.Cleanup()
|
||||
require.NoError(t, framework.Setup(config))
|
||||
|
||||
dir := "perf_writeback"
|
||||
framework.CreateTestDir(dir)
|
||||
|
||||
start := time.Now()
|
||||
for i := 0; i < numFiles; i++ {
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, fmt.Sprintf("file_%04d.bin", i))
|
||||
require.NoError(t, os.WriteFile(path, testData[i], 0644))
|
||||
}
|
||||
writebackDuration := time.Since(start)
|
||||
|
||||
// Wait for all files to be flushed
|
||||
for i := 0; i < numFiles; i++ {
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, fmt.Sprintf("file_%04d.bin", i))
|
||||
waitForFileContent(t, path, testData[i], 60*time.Second)
|
||||
}
|
||||
|
||||
t.Logf("writebackCache: wrote %d files in %v (%.0f files/sec)",
|
||||
numFiles, writebackDuration, float64(numFiles)/writebackDuration.Seconds())
|
||||
})
|
||||
|
||||
// Benchmark without writebackCache (synchronous flush)
|
||||
t.Run("WithoutWritebackCache", func(t *testing.T) {
|
||||
config := DefaultTestConfig()
|
||||
framework := NewFuseTestFramework(t, config)
|
||||
defer framework.Cleanup()
|
||||
require.NoError(t, framework.Setup(config))
|
||||
|
||||
dir := "perf_sync"
|
||||
framework.CreateTestDir(dir)
|
||||
|
||||
start := time.Now()
|
||||
for i := 0; i < numFiles; i++ {
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, fmt.Sprintf("file_%04d.bin", i))
|
||||
require.NoError(t, os.WriteFile(path, testData[i], 0644))
|
||||
}
|
||||
syncDuration := time.Since(start)
|
||||
|
||||
t.Logf("synchronous: wrote %d files in %v (%.0f files/sec)",
|
||||
numFiles, syncDuration, float64(numFiles)/syncDuration.Seconds())
|
||||
})
|
||||
}
|
||||
|
||||
// TestWritebackCacheConcurrentMixedOps tests a mix of operations happening
|
||||
// concurrently with writebackCache: creates, reads, overwrites, and deletes.
|
||||
func TestWritebackCacheConcurrentMixedOps(t *testing.T) {
|
||||
config := writebackConfig()
|
||||
framework := NewFuseTestFramework(t, config)
|
||||
defer framework.Cleanup()
|
||||
|
||||
require.NoError(t, framework.Setup(config))
|
||||
|
||||
dir := "writeback_mixed"
|
||||
framework.CreateTestDir(dir)
|
||||
|
||||
numFiles := 50
|
||||
var mu sync.Mutex
|
||||
var errors []error
|
||||
var completedWrites int64
|
||||
|
||||
addError := func(err error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
// Phase 1: Create initial files and wait for async flushes
|
||||
initialContents := make(map[int][]byte, numFiles)
|
||||
for i := 0; i < numFiles; i++ {
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, fmt.Sprintf("file_%03d.txt", i))
|
||||
content := []byte(fmt.Sprintf("initial content %d", i))
|
||||
require.NoError(t, os.WriteFile(path, content, 0644))
|
||||
initialContents[i] = content
|
||||
}
|
||||
|
||||
// Poll until initial files are flushed (instead of fixed sleep)
|
||||
for i := 0; i < numFiles; i++ {
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, fmt.Sprintf("file_%03d.txt", i))
|
||||
waitForFileContent(t, path, initialContents[i], 30*time.Second)
|
||||
}
|
||||
|
||||
// Phase 2: Concurrent mixed operations
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Writers: overwrite existing files
|
||||
for i := 0; i < 4; i++ {
|
||||
wg.Add(1)
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < numFiles; j++ {
|
||||
if j%4 != workerID {
|
||||
continue // each worker handles a subset
|
||||
}
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, fmt.Sprintf("file_%03d.txt", j))
|
||||
content := []byte(fmt.Sprintf("overwritten by worker %d at %s", workerID, time.Now().Format(time.RFC3339Nano)))
|
||||
if err := os.WriteFile(path, content, 0644); err != nil {
|
||||
addError(fmt.Errorf("writer %d file %d: %v", workerID, j, err))
|
||||
return
|
||||
}
|
||||
atomic.AddInt64(&completedWrites, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Readers: read files (may see old or new content, but should not error)
|
||||
for i := 0; i < 4; i++ {
|
||||
wg.Add(1)
|
||||
go func(readerID int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < numFiles; j++ {
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, fmt.Sprintf("file_%03d.txt", j))
|
||||
_, err := os.ReadFile(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
addError(fmt.Errorf("reader %d file %d: %v", readerID, j, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// New file creators
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 20; i++ {
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, fmt.Sprintf("new_file_%03d.txt", i))
|
||||
content := []byte(fmt.Sprintf("new file %d", i))
|
||||
if err := os.WriteFile(path, content, 0644); err != nil {
|
||||
addError(fmt.Errorf("creator file %d: %v", i, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
require.Empty(t, errors, "mixed operation errors: %v", errors)
|
||||
assert.True(t, atomic.LoadInt64(&completedWrites) > 0, "should have completed some writes")
|
||||
|
||||
// Verify new files exist after async flushes complete (poll instead of fixed sleep)
|
||||
for i := 0; i < 20; i++ {
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, fmt.Sprintf("new_file_%03d.txt", i))
|
||||
expected := []byte(fmt.Sprintf("new file %d", i))
|
||||
waitForFileContent(t, path, expected, 30*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWritebackCacheStressSmallFiles is a focused stress test for the
|
||||
// async flush path with many small files — the core scenario from #8718.
|
||||
func TestWritebackCacheStressSmallFiles(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping stress test in short mode")
|
||||
}
|
||||
|
||||
config := writebackConfig()
|
||||
framework := NewFuseTestFramework(t, config)
|
||||
defer framework.Cleanup()
|
||||
|
||||
require.NoError(t, framework.Setup(config))
|
||||
|
||||
dir := "writeback_stress"
|
||||
framework.CreateTestDir(dir)
|
||||
|
||||
numWorkers := 16
|
||||
filesPerWorker := 100
|
||||
totalFiles := numWorkers * filesPerWorker
|
||||
|
||||
type fileRecord struct {
|
||||
path string
|
||||
content []byte
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
var writeErrors []error
|
||||
records := make([]fileRecord, 0, totalFiles)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Simulate rsync-like workload: many workers each writing small files
|
||||
var wg sync.WaitGroup
|
||||
for w := 0; w < numWorkers; w++ {
|
||||
wg.Add(1)
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
for f := 0; f < filesPerWorker; f++ {
|
||||
filename := fmt.Sprintf("w%02d/f%04d.dat", workerID, f)
|
||||
path := filepath.Join(framework.GetMountPoint(), dir, filename)
|
||||
|
||||
// Ensure subdirectory exists
|
||||
if f == 0 {
|
||||
subDir := filepath.Join(framework.GetMountPoint(), dir, fmt.Sprintf("w%02d", workerID))
|
||||
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||
mu.Lock()
|
||||
writeErrors = append(writeErrors, fmt.Errorf("worker %d mkdir: %v", workerID, err))
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Small file: 1KB-10KB (typical for rsync of config/source files)
|
||||
size := 1024 + (f%10)*1024
|
||||
content := make([]byte, size)
|
||||
if _, err := rand.Read(content); err != nil {
|
||||
mu.Lock()
|
||||
writeErrors = append(writeErrors, fmt.Errorf("worker %d file %d rand: %v", workerID, f, err))
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, content, 0644); err != nil {
|
||||
mu.Lock()
|
||||
writeErrors = append(writeErrors, fmt.Errorf("worker %d file %d: %v", workerID, f, err))
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
records = append(records, fileRecord{path: path, content: content})
|
||||
mu.Unlock()
|
||||
}
|
||||
}(w)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
writeDuration := time.Since(start)
|
||||
t.Logf("wrote %d files in %v (%.0f files/sec)",
|
||||
totalFiles, writeDuration, float64(totalFiles)/writeDuration.Seconds())
|
||||
|
||||
require.Empty(t, writeErrors, "write errors: %v", writeErrors)
|
||||
assert.Equal(t, totalFiles, len(records))
|
||||
|
||||
// Verify all files are eventually readable with correct content
|
||||
var verifyErrors []error
|
||||
for _, rec := range records {
|
||||
deadline := time.Now().Add(120 * time.Second)
|
||||
var lastErr error
|
||||
for time.Now().Before(deadline) {
|
||||
actual, err := os.ReadFile(rec.path)
|
||||
if err == nil && bytes.Equal(rec.content, actual) {
|
||||
lastErr = nil
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
} else {
|
||||
lastErr = fmt.Errorf("content mismatch for %s: got %d bytes, want %d", rec.path, len(actual), len(rec.content))
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
if lastErr != nil {
|
||||
verifyErrors = append(verifyErrors, lastErr)
|
||||
}
|
||||
}
|
||||
require.Empty(t, verifyErrors, "verification errors after stress test: %v", verifyErrors)
|
||||
|
||||
t.Logf("all %d files verified successfully", totalFiles)
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package example
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestS3ListObjectsEmptyDirectoryMarkers reproduces GitHub issue #8698:
|
||||
// S3 API ListObjects does not include empty directory markers (zero-byte
|
||||
// objects with keys ending in "/") created via PutObject.
|
||||
//
|
||||
// AWS S3 includes these markers as regular keys in Contents. SeaweedFS
|
||||
// was filtering them out during recursive directory listing.
|
||||
func TestS3ListObjectsEmptyDirectoryMarkers(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
cluster, err := startMiniCluster(t)
|
||||
require.NoError(t, err)
|
||||
defer cluster.Stop()
|
||||
|
||||
bucketName := createTestBucket(t, cluster, "test-empty-dirs-")
|
||||
|
||||
// Create a regular file at Empty/manifest.yaml
|
||||
_, err = cluster.s3Client.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String("Empty/manifest.yaml"),
|
||||
Body: bytes.NewReader([]byte("name: test\nversion: 1.0\n")),
|
||||
})
|
||||
require.NoError(t, err, "failed to create regular file")
|
||||
|
||||
// Create an empty directory marker via PutObject (key ending in "/", zero bytes).
|
||||
// This is how `aws s3api put-object --key "Empty/empty/"` works.
|
||||
_, err = cluster.s3Client.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String("Empty/empty/"),
|
||||
Body: bytes.NewReader([]byte{}),
|
||||
})
|
||||
require.NoError(t, err, "failed to create empty directory marker")
|
||||
|
||||
// Verify the directory marker exists via HeadObject
|
||||
_, err = cluster.s3Client.HeadObject(&s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String("Empty/empty/"),
|
||||
})
|
||||
require.NoError(t, err, "directory marker should exist via HeadObject")
|
||||
|
||||
// Test 1: ListObjectsV2 with prefix (no delimiter) — the exact scenario from the issue.
|
||||
// AWS S3 returns both "Empty/empty/" and "Empty/manifest.yaml" in Contents.
|
||||
t.Run("ListV2_WithPrefix_NoDelimiter", func(t *testing.T) {
|
||||
resp, err := cluster.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucketName),
|
||||
Prefix: aws.String("Empty"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := collectKeys(resp.Contents)
|
||||
sort.Strings(keys)
|
||||
|
||||
assert.Equal(t, []string{"Empty/empty/", "Empty/manifest.yaml"}, keys,
|
||||
"both the directory marker and regular file should be listed")
|
||||
})
|
||||
|
||||
// Test 2: ListObjectsV1 with prefix (no delimiter).
|
||||
t.Run("ListV1_WithPrefix_NoDelimiter", func(t *testing.T) {
|
||||
resp, err := cluster.s3Client.ListObjects(&s3.ListObjectsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Prefix: aws.String("Empty"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := collectKeysV1(resp.Contents)
|
||||
sort.Strings(keys)
|
||||
|
||||
assert.Equal(t, []string{"Empty/empty/", "Empty/manifest.yaml"}, keys,
|
||||
"both the directory marker and regular file should be listed")
|
||||
})
|
||||
|
||||
// Test 3: ListObjectsV2 without prefix and without delimiter.
|
||||
// All objects in the bucket should appear, including directory markers.
|
||||
t.Run("ListV2_NoPrefix_NoDelimiter", func(t *testing.T) {
|
||||
resp, err := cluster.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := collectKeys(resp.Contents)
|
||||
|
||||
assert.Contains(t, keys, "Empty/empty/", "directory marker should appear in listing")
|
||||
assert.Contains(t, keys, "Empty/manifest.yaml", "regular file should appear in listing")
|
||||
})
|
||||
|
||||
// Test 4: ListObjectsV2 with prefix "Empty/" and delimiter "/".
|
||||
// The directory marker "Empty/empty/" should appear as a CommonPrefix,
|
||||
// and "Empty/manifest.yaml" should appear in Contents.
|
||||
t.Run("ListV2_WithPrefix_WithDelimiter", func(t *testing.T) {
|
||||
resp, err := cluster.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucketName),
|
||||
Prefix: aws.String("Empty/"),
|
||||
Delimiter: aws.String("/"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := collectKeys(resp.Contents)
|
||||
prefixes := collectPrefixes(resp.CommonPrefixes)
|
||||
|
||||
assert.Contains(t, keys, "Empty/manifest.yaml", "regular file should be in Contents")
|
||||
assert.Contains(t, prefixes, "Empty/empty/", "directory marker should appear as CommonPrefix")
|
||||
})
|
||||
|
||||
// Test 5: Multiple empty directory markers at different nesting levels.
|
||||
t.Run("ListV2_NestedEmptyDirs", func(t *testing.T) {
|
||||
nestedBucket := createTestBucket(t, cluster, "test-nested-dirs-")
|
||||
|
||||
// Create nested structure:
|
||||
// dir/sub1/ (empty dir marker)
|
||||
// dir/sub2/deep/ (empty dir marker at deeper level)
|
||||
// dir/file.txt (regular file)
|
||||
_, err := cluster.s3Client.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(nestedBucket),
|
||||
Key: aws.String("dir/sub1/"),
|
||||
Body: bytes.NewReader([]byte{}),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = cluster.s3Client.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(nestedBucket),
|
||||
Key: aws.String("dir/sub2/deep/"),
|
||||
Body: bytes.NewReader([]byte{}),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = cluster.s3Client.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(nestedBucket),
|
||||
Key: aws.String("dir/file.txt"),
|
||||
Body: bytes.NewReader([]byte("content")),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := cluster.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(nestedBucket),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := collectKeys(resp.Contents)
|
||||
sort.Strings(keys)
|
||||
|
||||
assert.Equal(t, []string{"dir/file.txt", "dir/sub1/", "dir/sub2/deep/"}, keys,
|
||||
"all objects including nested empty directory markers should be listed")
|
||||
})
|
||||
|
||||
// Test 6: Directory marker with a child object, paginated with MaxKeys=1.
|
||||
// This exercises the emit-marker-then-recurse path under truncation:
|
||||
// the marker is emitted, maxKeys drops to 0, and the child must be
|
||||
// picked up on a subsequent page.
|
||||
t.Run("ListV2_MarkerWithChild_Paginated", func(t *testing.T) {
|
||||
siblingBucket := createTestBucket(t, cluster, "test-sibling-dirs-")
|
||||
|
||||
// Create:
|
||||
// docs/ (directory key object)
|
||||
// docs/readme.txt (child file inside the marker)
|
||||
// readme.txt (sibling file at top level)
|
||||
_, err := cluster.s3Client.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(siblingBucket),
|
||||
Key: aws.String("docs/"),
|
||||
Body: bytes.NewReader([]byte{}),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = cluster.s3Client.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(siblingBucket),
|
||||
Key: aws.String("docs/readme.txt"),
|
||||
Body: bytes.NewReader([]byte("inside docs")),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = cluster.s3Client.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(siblingBucket),
|
||||
Key: aws.String("readme.txt"),
|
||||
Body: bytes.NewReader([]byte("hello")),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Unpaginated: all three keys should appear.
|
||||
resp, err := cluster.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(siblingBucket),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := collectKeys(resp.Contents)
|
||||
sort.Strings(keys)
|
||||
|
||||
assert.Equal(t, []string{"docs/", "docs/readme.txt", "readme.txt"}, keys,
|
||||
"directory marker, its child, and sibling file should all be listed")
|
||||
|
||||
// Paginated with MaxKeys=1: collect all keys across pages.
|
||||
var allKeys []string
|
||||
var token *string
|
||||
for {
|
||||
pageResp, pageErr := cluster.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(siblingBucket),
|
||||
MaxKeys: aws.Int64(1),
|
||||
ContinuationToken: token,
|
||||
})
|
||||
require.NoError(t, pageErr)
|
||||
allKeys = append(allKeys, collectKeys(pageResp.Contents)...)
|
||||
if !aws.BoolValue(pageResp.IsTruncated) {
|
||||
break
|
||||
}
|
||||
token = pageResp.NextContinuationToken
|
||||
}
|
||||
sort.Strings(allKeys)
|
||||
|
||||
assert.Equal(t, []string{"docs/", "docs/readme.txt", "readme.txt"}, allKeys,
|
||||
"paginated listing should return all keys including marker and its child")
|
||||
})
|
||||
|
||||
t.Run("ListExplicitDirectoryMarkerWithOctetStreamContentType", func(t *testing.T) {
|
||||
bucketWithContentType := createTestBucket(t, cluster, "test-explicit-dir-")
|
||||
objectKey := "test-content/empty/"
|
||||
|
||||
_, err := cluster.s3Client.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketWithContentType),
|
||||
Key: aws.String(objectKey),
|
||||
Body: bytes.NewReader([]byte{}),
|
||||
ContentType: aws.String("application/octet-stream"),
|
||||
})
|
||||
require.NoError(t, err, "failed to create explicit directory marker with octet-stream content type")
|
||||
|
||||
headResp, err := cluster.s3Client.HeadObject(&s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucketWithContentType),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
require.NoError(t, err, "directory marker should exist via HeadObject")
|
||||
assert.Equal(t, "application/octet-stream", aws.StringValue(headResp.ContentType))
|
||||
|
||||
respV1, err := cluster.s3Client.ListObjects(&s3.ListObjectsInput{
|
||||
Bucket: aws.String(bucketWithContentType),
|
||||
Prefix: aws.String("test-content"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{objectKey}, collectKeysV1(respV1.Contents),
|
||||
"explicit empty directory markers with octet-stream content type should be listed")
|
||||
|
||||
respV2, err := cluster.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucketWithContentType),
|
||||
Prefix: aws.String("test-content"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{objectKey}, collectKeys(respV2.Contents),
|
||||
"explicit empty directory markers with octet-stream content type should be listed")
|
||||
})
|
||||
}
|
||||
|
||||
func collectKeys(contents []*s3.Object) []string {
|
||||
keys := make([]string, 0, len(contents))
|
||||
for _, obj := range contents {
|
||||
keys = append(keys, aws.StringValue(obj.Key))
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func collectKeysV1(contents []*s3.Object) []string {
|
||||
return collectKeys(contents)
|
||||
}
|
||||
|
||||
func collectPrefixes(prefixes []*s3.CommonPrefix) []string {
|
||||
result := make([]string, 0, len(prefixes))
|
||||
for _, p := range prefixes {
|
||||
result = append(result, aws.StringValue(p.Prefix))
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -50,6 +50,12 @@ print("WRITE_COUNT=" + str(count))
|
||||
keys := listObjectKeysByPrefix(t, env, "test", "issue-8285/")
|
||||
var temporaryKeys []string
|
||||
for _, key := range keys {
|
||||
// Skip directory markers (keys ending in "/") — these are 0-byte
|
||||
// metadata objects, not data artifacts. They are verified separately
|
||||
// via HeadObject with a timeout below.
|
||||
if strings.HasSuffix(key, "/") {
|
||||
continue
|
||||
}
|
||||
if hasTemporaryPathSegment(key) {
|
||||
temporaryKeys = append(temporaryKeys, key)
|
||||
}
|
||||
|
||||
@@ -1284,6 +1284,23 @@ func (s *AdminServer) RunPluginDetectionWithReport(
|
||||
return s.plugin.RunDetectionWithReport(ctx, jobType, clusterContext, maxResults)
|
||||
}
|
||||
|
||||
// DispatchPluginProposals dispatches a batch of proposals using the same
|
||||
// capacity-aware dispatch logic as the scheduler loop (executor reservation with
|
||||
// backoff, per-job retry on transient errors). The plugin lock must already be
|
||||
// held by the caller.
|
||||
func (s *AdminServer) DispatchPluginProposals(
|
||||
ctx context.Context,
|
||||
jobType string,
|
||||
proposals []*plugin_pb.JobProposal,
|
||||
clusterContext *plugin_pb.ClusterContext,
|
||||
) (successCount, errorCount, canceledCount int, err error) {
|
||||
if s.plugin == nil {
|
||||
return 0, 0, 0, fmt.Errorf("plugin is not enabled")
|
||||
}
|
||||
sc, ec, cc := s.plugin.DispatchProposals(ctx, jobType, proposals, clusterContext)
|
||||
return sc, ec, cc, nil
|
||||
}
|
||||
|
||||
// ExecutePluginJob dispatches one job to a capable worker and waits for completion.
|
||||
func (s *AdminServer) ExecutePluginJob(
|
||||
ctx context.Context,
|
||||
|
||||
@@ -526,40 +526,10 @@ func (s *AdminServer) RunPluginJobTypeAPI(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
type executionResult struct {
|
||||
JobID string `json:"job_id"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Completion map[string]interface{} `json:"completion,omitempty"`
|
||||
}
|
||||
|
||||
results := make([]executionResult, 0, len(filteredProposals))
|
||||
successCount := 0
|
||||
errorCount := 0
|
||||
|
||||
for index, proposal := range filteredProposals {
|
||||
job := buildJobSpecFromProposal(jobType, proposal, index)
|
||||
completed, execErr := s.ExecutePluginJob(ctx, job, clusterContext, req.Attempt)
|
||||
|
||||
result := executionResult{
|
||||
JobID: job.JobId,
|
||||
Success: execErr == nil,
|
||||
}
|
||||
|
||||
if completed != nil {
|
||||
if payload, marshalErr := protoMessageToMap(completed); marshalErr == nil {
|
||||
result.Completion = payload
|
||||
}
|
||||
}
|
||||
|
||||
if execErr != nil {
|
||||
result.Error = execErr.Error()
|
||||
errorCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
successCount, errorCount, canceledCount, dispatchErr := s.DispatchPluginProposals(ctx, jobType, filteredProposals, clusterContext)
|
||||
if dispatchErr != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, dispatchErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
@@ -567,10 +537,10 @@ func (s *AdminServer) RunPluginJobTypeAPI(w http.ResponseWriter, r *http.Request
|
||||
"detected_count": detectedCount,
|
||||
"ready_to_execute_count": len(filteredProposals),
|
||||
"skipped_active_count": skippedActiveCount,
|
||||
"executed_count": len(results),
|
||||
"executed_count": successCount + errorCount + canceledCount,
|
||||
"success_count": successCount,
|
||||
"error_count": errorCount,
|
||||
"execution_results": results,
|
||||
"canceled_count": canceledCount,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -153,13 +153,6 @@ func (s *AdminServer) DeleteObjectStoreUser(username string) error {
|
||||
return fmt.Errorf("credential manager not available")
|
||||
}
|
||||
|
||||
// Prevent deletion of the anonymous identity — it is a system identity
|
||||
// used for unauthenticated S3 access. Removing it would break anonymous
|
||||
// request handling in the IAM layer.
|
||||
if username == "anonymous" {
|
||||
return fmt.Errorf("cannot delete the system identity 'anonymous'")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Delete user using credential manager
|
||||
|
||||
@@ -446,6 +446,30 @@ func (s *ConfigStore) LoadJobDetail(jobID string) (*TrackedJob, error) {
|
||||
return &clone, nil
|
||||
}
|
||||
|
||||
// DeleteJobDetail removes the persisted detail snapshot for a job.
|
||||
// It is a no-op when the file does not exist.
|
||||
func (s *ConfigStore) DeleteJobDetail(jobID string) error {
|
||||
jobID, err := sanitizeJobID(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if !s.configured {
|
||||
delete(s.memJobDetails, jobID)
|
||||
return nil
|
||||
}
|
||||
|
||||
path := filepath.Join(s.baseDir, jobsDirName, jobDetailsDirName, jobDetailFileName(jobID))
|
||||
err = os.Remove(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("delete job detail: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ConfigStore) SaveActivities(activities []JobActivity) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
@@ -330,3 +330,50 @@ func TestConfigStoreJobDetailRoundTrip(t *testing.T) {
|
||||
t.Fatalf("expected result output values")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigStoreDeleteJobDetail(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
store, err := NewConfigStore(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("NewConfigStore: %v", err)
|
||||
}
|
||||
|
||||
input := TrackedJob{
|
||||
JobID: "job-to-delete",
|
||||
JobType: "vacuum",
|
||||
Summary: "will be deleted",
|
||||
}
|
||||
|
||||
if err := store.SaveJobDetail(input); err != nil {
|
||||
t.Fatalf("SaveJobDetail: %v", err)
|
||||
}
|
||||
|
||||
// Verify it was persisted.
|
||||
got, err := store.LoadJobDetail(input.JobID)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadJobDetail before delete: %v", err)
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatalf("expected saved job detail, got nil")
|
||||
}
|
||||
|
||||
// Delete it.
|
||||
if err := store.DeleteJobDetail(input.JobID); err != nil {
|
||||
t.Fatalf("DeleteJobDetail: %v", err)
|
||||
}
|
||||
|
||||
// Verify it is gone.
|
||||
got, err = store.LoadJobDetail(input.JobID)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadJobDetail after delete: %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("expected nil after delete, got %+v", got)
|
||||
}
|
||||
|
||||
// Deleting again should be a no-op, not an error.
|
||||
if err := store.DeleteJobDetail(input.JobID); err != nil {
|
||||
t.Fatalf("second DeleteJobDetail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,12 @@ func (r *Plugin) loadPersistedMonitorState() error {
|
||||
}
|
||||
|
||||
if len(trackedJobs) > 0 {
|
||||
// Collect jobs that still carry inline rich details (old format) so we
|
||||
// can migrate them to dedicated per-job detail files asynchronously.
|
||||
// Writing many files synchronously during startup can be extremely slow
|
||||
// on distributed storage (e.g. Longhorn), so we defer those writes.
|
||||
var jobsToMigrate []TrackedJob
|
||||
|
||||
r.jobsMu.Lock()
|
||||
for i := range trackedJobs {
|
||||
job := trackedJobs[i]
|
||||
@@ -78,10 +84,9 @@ func (r *Plugin) loadPersistedMonitorState() error {
|
||||
}
|
||||
// Backward compatibility: migrate older inline detail payloads
|
||||
// out of tracked_jobs.json into dedicated per-job detail files.
|
||||
// Collect for async migration instead of writing synchronously.
|
||||
if hasTrackedJobRichDetails(job) {
|
||||
if err := r.store.SaveJobDetail(job); err != nil {
|
||||
glog.Warningf("Plugin failed to migrate detail snapshot for job %s: %v", job.JobID, err)
|
||||
}
|
||||
jobsToMigrate = append(jobsToMigrate, job)
|
||||
}
|
||||
stripTrackedJobDetailFields(&job)
|
||||
jobCopy := job
|
||||
@@ -89,6 +94,10 @@ func (r *Plugin) loadPersistedMonitorState() error {
|
||||
}
|
||||
r.pruneTrackedJobsLocked()
|
||||
r.jobsMu.Unlock()
|
||||
|
||||
if len(jobsToMigrate) > 0 {
|
||||
go r.migrateInlineJobDetails(jobsToMigrate)
|
||||
}
|
||||
}
|
||||
|
||||
if len(activities) > maxActivityRecords {
|
||||
@@ -103,6 +112,23 @@ func (r *Plugin) loadPersistedMonitorState() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateInlineJobDetails writes inline detail payloads from old-format
|
||||
// tracked_jobs.json entries to dedicated per-job detail files. It is called
|
||||
// asynchronously during startup to avoid blocking plugin initialisation.
|
||||
func (r *Plugin) migrateInlineJobDetails(jobs []TrackedJob) {
|
||||
migrated := 0
|
||||
for _, job := range jobs {
|
||||
if err := r.store.SaveJobDetail(job); err != nil {
|
||||
glog.Warningf("Plugin failed to migrate detail snapshot for job %s: %v", job.JobID, err)
|
||||
} else {
|
||||
migrated++
|
||||
}
|
||||
}
|
||||
if migrated > 0 {
|
||||
glog.V(1).Infof("Plugin migrated %d inline job details to dedicated files", migrated)
|
||||
}
|
||||
}
|
||||
|
||||
// ExpireJob marks an active job as failed so it no longer blocks scheduling.
|
||||
func (r *Plugin) ExpireJob(jobID, reason string) (*TrackedJob, bool, error) {
|
||||
normalizedJobID := strings.TrimSpace(jobID)
|
||||
@@ -1108,7 +1134,11 @@ func (r *Plugin) pruneTrackedJobsLocked() {
|
||||
}
|
||||
|
||||
for i := 0; i < toDelete; i++ {
|
||||
delete(r.jobs, terminalJobs[i].jobID)
|
||||
jobID := terminalJobs[i].jobID
|
||||
delete(r.jobs, jobID)
|
||||
if err := r.store.DeleteJobDetail(jobID); err != nil {
|
||||
glog.V(2).Infof("Plugin failed to delete job detail for pruned job %s: %v", jobID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1287,6 +1287,35 @@ func isWaitingTrackedJobState(state string) bool {
|
||||
return normalized == "pending" || normalized == "job_state_pending"
|
||||
}
|
||||
|
||||
// DispatchProposals dispatches a batch of proposals using the same capacity-aware
|
||||
// dispatch logic as the scheduler loop: concurrent execution, executor reservation
|
||||
// with backoff, and per-job retry on transient errors. The scheduler policy is
|
||||
// loaded from the persisted job type config; if the job type has no config or is
|
||||
// disabled a sensible default policy is used so manual runs always work.
|
||||
func (r *Plugin) DispatchProposals(
|
||||
ctx context.Context,
|
||||
jobType string,
|
||||
proposals []*plugin_pb.JobProposal,
|
||||
clusterContext *plugin_pb.ClusterContext,
|
||||
) (successCount, errorCount, canceledCount int) {
|
||||
if len(proposals) == 0 {
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
policy, enabled, err := r.loadSchedulerPolicy(jobType)
|
||||
if err != nil || !enabled {
|
||||
policy = schedulerPolicy{
|
||||
ExecutionConcurrency: defaultScheduledExecutionConcurrency,
|
||||
PerWorkerConcurrency: defaultScheduledPerWorkerConcurrency,
|
||||
ExecutionTimeout: defaultScheduledExecutionTimeout,
|
||||
RetryBackoff: defaultScheduledRetryBackoff,
|
||||
ExecutorReserveBackoff: 200 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
return r.dispatchScheduledProposals(ctx, jobType, proposals, clusterContext, policy)
|
||||
}
|
||||
|
||||
func (r *Plugin) filterScheduledProposals(proposals []*plugin_pb.JobProposal) []*plugin_pb.JobProposal {
|
||||
filtered := make([]*plugin_pb.JobProposal, 0, len(proposals))
|
||||
seenInRun := make(map[string]struct{}, len(proposals))
|
||||
|
||||
@@ -210,6 +210,24 @@ func (option *RemoteGatewayOptions) makeBucketedEventProcessor(filerSource *sour
|
||||
if isMultipartUploadFile(message.NewParentPath, message.NewEntry.Name) {
|
||||
return nil
|
||||
}
|
||||
// Propagate delete markers as deletions on the remote.
|
||||
// Delete markers are zero-content version entries, so they
|
||||
// would be filtered out by the HasData check below.
|
||||
if isDeleteMarker(message.NewEntry) {
|
||||
if newParent, newName, ok := rewriteVersionedSourcePath(message.NewParentPath, message.NewEntry.Name); ok {
|
||||
bucket, remoteStorageMountLocation, remoteStorage, ok := option.detectBucketInfo(newParent)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
client, err := remote_storage.GetRemoteStorage(remoteStorage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dest := toRemoteStorageLocation(bucket, util.NewFullPath(newParent, newName), remoteStorageMountLocation)
|
||||
return syncDeleteMarker(client, option, message, dest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !filer.HasData(message.NewEntry) {
|
||||
return nil
|
||||
}
|
||||
@@ -226,7 +244,16 @@ func (option *RemoteGatewayOptions) makeBucketedEventProcessor(filerSource *sour
|
||||
glog.V(2).Infof("skipping creating: %+v", resp)
|
||||
return nil
|
||||
}
|
||||
dest := toRemoteStorageLocation(bucket, util.NewFullPath(message.NewParentPath, message.NewEntry.Name), remoteStorageMountLocation)
|
||||
// Rewrite internal versioning paths to the original S3 key
|
||||
// to prevent double-versioning when central also has versioning enabled
|
||||
parentPath, entryName := message.NewParentPath, message.NewEntry.Name
|
||||
isRewrittenVersion := false
|
||||
if newParent, newName, ok := rewriteVersionedSourcePath(parentPath, entryName); ok {
|
||||
glog.V(0).Infof("rewrite versioned path %s/%s -> %s/%s", parentPath, entryName, newParent, newName)
|
||||
parentPath, entryName = newParent, newName
|
||||
isRewrittenVersion = true
|
||||
}
|
||||
dest := toRemoteStorageLocation(bucket, util.NewFullPath(parentPath, entryName), remoteStorageMountLocation)
|
||||
if message.NewEntry.IsDirectory {
|
||||
glog.V(0).Infof("mkdir %s", remote_storage.FormatLocation(dest))
|
||||
return client.WriteDirectory(dest, message.NewEntry)
|
||||
@@ -236,12 +263,26 @@ func (option *RemoteGatewayOptions) makeBucketedEventProcessor(filerSource *sour
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
// Skip updateLocalEntry for versioned rewrites: the logical
|
||||
// object (e.g. file.xml) has no filer entry in versioned
|
||||
// buckets, and stamping the internal v_* entry with a
|
||||
// RemoteEntry for the logical key is semantically wrong.
|
||||
// Replay is safe because S3 PutObject is idempotent.
|
||||
if isRewrittenVersion {
|
||||
return nil
|
||||
}
|
||||
return updateLocalEntry(option, message.NewParentPath, message.NewEntry, remoteEntry)
|
||||
}
|
||||
if filer_pb.IsDelete(resp) {
|
||||
if resp.Directory == option.bucketsDir {
|
||||
return handleDeleteBucket(message.OldEntry)
|
||||
}
|
||||
// Skip deletion of internal version files; individual version
|
||||
// deletes should not propagate to the remote object
|
||||
if isVersionedPath(resp.Directory, message.OldEntry.Name, message.OldEntry.IsDirectory) {
|
||||
glog.V(2).Infof("skipping delete of internal version path: %s/%s", resp.Directory, message.OldEntry.Name)
|
||||
return nil
|
||||
}
|
||||
bucket, remoteStorageMountLocation, remoteStorage, ok := option.detectBucketInfo(resp.Directory)
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -276,6 +317,11 @@ func (option *RemoteGatewayOptions) makeBucketedEventProcessor(filerSource *sour
|
||||
if isMultipartUploadFile(message.NewParentPath, message.NewEntry.Name) {
|
||||
return nil
|
||||
}
|
||||
// Skip updates to internal version paths
|
||||
if isVersionedPath(message.NewParentPath, message.NewEntry.Name, message.NewEntry.IsDirectory) {
|
||||
glog.V(2).Infof("skipping update of internal version path: %s/%s", message.NewParentPath, message.NewEntry.Name)
|
||||
return nil
|
||||
}
|
||||
oldBucket, oldRemoteStorageMountLocation, oldRemoteStorage, oldOk := option.detectBucketInfo(resp.Directory)
|
||||
newBucket, newRemoteStorageMountLocation, newRemoteStorage, newOk := option.detectBucketInfo(message.NewParentPath)
|
||||
if oldOk && newOk {
|
||||
|
||||
@@ -138,6 +138,16 @@ func (option *RemoteSyncOptions) makeEventProcessor(remoteStorage *remote_pb.Rem
|
||||
if isMultipartUploadFile(message.NewParentPath, message.NewEntry.Name) {
|
||||
return nil
|
||||
}
|
||||
// Propagate delete markers as deletions on the remote.
|
||||
// Delete markers are zero-content version entries, so they
|
||||
// would be filtered out by the HasData check below.
|
||||
if isDeleteMarker(message.NewEntry) {
|
||||
if newParent, newName, ok := rewriteVersionedSourcePath(message.NewParentPath, message.NewEntry.Name); ok {
|
||||
dest := toRemoteStorageLocation(util.FullPath(mountedDir), util.NewFullPath(newParent, newName), remoteStorageMountLocation)
|
||||
return syncDeleteMarker(client, option, message, dest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !filer.HasData(message.NewEntry) {
|
||||
return nil
|
||||
}
|
||||
@@ -146,7 +156,16 @@ func (option *RemoteSyncOptions) makeEventProcessor(remoteStorage *remote_pb.Rem
|
||||
glog.V(2).Infof("skipping creating: %+v", resp)
|
||||
return nil
|
||||
}
|
||||
dest := toRemoteStorageLocation(util.FullPath(mountedDir), util.NewFullPath(message.NewParentPath, message.NewEntry.Name), remoteStorageMountLocation)
|
||||
// Rewrite internal versioning paths to the original S3 key
|
||||
// to prevent double-versioning when central also has versioning enabled
|
||||
parentPath, entryName := message.NewParentPath, message.NewEntry.Name
|
||||
isRewrittenVersion := false
|
||||
if newParent, newName, ok := rewriteVersionedSourcePath(parentPath, entryName); ok {
|
||||
glog.V(0).Infof("rewrite versioned path %s/%s -> %s/%s", parentPath, entryName, newParent, newName)
|
||||
parentPath, entryName = newParent, newName
|
||||
isRewrittenVersion = true
|
||||
}
|
||||
dest := toRemoteStorageLocation(util.FullPath(mountedDir), util.NewFullPath(parentPath, entryName), remoteStorageMountLocation)
|
||||
if message.NewEntry.IsDirectory {
|
||||
glog.V(0).Infof("mkdir %s", remote_storage.FormatLocation(dest))
|
||||
return client.WriteDirectory(dest, message.NewEntry)
|
||||
@@ -156,9 +175,23 @@ func (option *RemoteSyncOptions) makeEventProcessor(remoteStorage *remote_pb.Rem
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
// Skip updateLocalEntry for versioned rewrites: the logical
|
||||
// object (e.g. file.xml) has no filer entry in versioned
|
||||
// buckets, and stamping the internal v_* entry with a
|
||||
// RemoteEntry for the logical key is semantically wrong.
|
||||
// Replay is safe because S3 PutObject is idempotent.
|
||||
if isRewrittenVersion {
|
||||
return nil
|
||||
}
|
||||
return updateLocalEntry(option, message.NewParentPath, message.NewEntry, remoteEntry)
|
||||
}
|
||||
if filer_pb.IsDelete(resp) {
|
||||
// Skip deletion of internal version files; individual version
|
||||
// deletes should not propagate to the remote object
|
||||
if isVersionedPath(resp.Directory, message.OldEntry.Name, message.OldEntry.IsDirectory) {
|
||||
glog.V(2).Infof("skipping delete of internal version path: %s/%s", resp.Directory, message.OldEntry.Name)
|
||||
return nil
|
||||
}
|
||||
glog.V(2).Infof("delete: %+v", resp)
|
||||
dest := toRemoteStorageLocation(util.FullPath(mountedDir), util.NewFullPath(resp.Directory, message.OldEntry.Name), remoteStorageMountLocation)
|
||||
if message.OldEntry.IsDirectory {
|
||||
@@ -172,6 +205,11 @@ func (option *RemoteSyncOptions) makeEventProcessor(remoteStorage *remote_pb.Rem
|
||||
if isMultipartUploadFile(message.NewParentPath, message.NewEntry.Name) {
|
||||
return nil
|
||||
}
|
||||
// Skip updates to internal version paths
|
||||
if isVersionedPath(message.NewParentPath, message.NewEntry.Name, message.NewEntry.IsDirectory) {
|
||||
glog.V(2).Infof("skipping update of internal version path: %s/%s", message.NewParentPath, message.NewEntry.Name)
|
||||
return nil
|
||||
}
|
||||
oldDest := toRemoteStorageLocation(util.FullPath(mountedDir), util.NewFullPath(resp.Directory, message.OldEntry.Name), remoteStorageMountLocation)
|
||||
dest := toRemoteStorageLocation(util.FullPath(mountedDir), util.NewFullPath(message.NewParentPath, message.NewEntry.Name), remoteStorageMountLocation)
|
||||
if !shouldSendToRemote(message.NewEntry) {
|
||||
@@ -292,3 +330,88 @@ func isMultipartUploadDir(dir string) bool {
|
||||
return strings.HasPrefix(dir, "/buckets/") &&
|
||||
strings.Contains(dir, "/"+s3_constants.MultipartUploadsFolder+"/")
|
||||
}
|
||||
|
||||
// isDeleteMarker returns true if the entry is an S3 delete marker
|
||||
// (a zero-content version entry with ExtDeleteMarkerKey set to "true").
|
||||
func isDeleteMarker(entry *filer_pb.Entry) bool {
|
||||
if entry == nil || entry.Extended == nil {
|
||||
return false
|
||||
}
|
||||
return string(entry.Extended[s3_constants.ExtDeleteMarkerKey]) == "true"
|
||||
}
|
||||
|
||||
// syncDeleteMarker propagates a delete marker to the remote storage and
|
||||
// persists a local sync marker so that replaying the same event is a no-op.
|
||||
func syncDeleteMarker(
|
||||
client remote_storage.RemoteStorageClient,
|
||||
filerClient filer_pb.FilerClient,
|
||||
message *filer_pb.EventNotification,
|
||||
dest *remote_pb.RemoteStorageLocation,
|
||||
) error {
|
||||
glog.V(0).Infof("delete (marker) %s", remote_storage.FormatLocation(dest))
|
||||
if err := client.DeleteFile(dest); err != nil {
|
||||
return err
|
||||
}
|
||||
return updateLocalEntry(filerClient, message.NewParentPath, message.NewEntry, &filer_pb.RemoteEntry{
|
||||
StorageName: dest.Name,
|
||||
RemoteMtime: message.NewEntry.Attributes.GetMtime(),
|
||||
})
|
||||
}
|
||||
|
||||
// isVersionedPath returns true if the dir/name refers to an internal
|
||||
// versioning path (.versions directory or a version file inside it).
|
||||
// These paths are SeaweedFS-internal and must not be synced to remote
|
||||
// storage as-is, because the remote S3 endpoint may apply its own
|
||||
// versioning, leading to double-versioned paths.
|
||||
//
|
||||
// For directories: matches only when the entry name ends with the
|
||||
// VersionsFolder suffix (e.g. "file.xml.versions").
|
||||
// For files: matches only when the parent directory ends with
|
||||
// VersionsFolder and the file name has the "v_" prefix used by
|
||||
// the internal version file naming convention.
|
||||
func isVersionedPath(dir string, name string, isDir bool) bool {
|
||||
if !strings.HasPrefix(dir, "/buckets/") {
|
||||
return false
|
||||
}
|
||||
if isDir {
|
||||
return strings.HasSuffix(name, s3_constants.VersionsFolder)
|
||||
}
|
||||
return strings.HasSuffix(dir, s3_constants.VersionsFolder) && strings.HasPrefix(name, "v_")
|
||||
}
|
||||
|
||||
// rewriteVersionedSourcePath rewrites an internal versioning path to the
|
||||
// original S3 object key. When a file is uploaded to a versioned bucket,
|
||||
// SeaweedFS stores it internally as:
|
||||
//
|
||||
// /buckets/{bucket}/{key}.versions/v_{versionId}
|
||||
//
|
||||
// This function strips the ".versions/v_{versionId}" suffix and returns
|
||||
// the original parent directory and object name, so the remote destination
|
||||
// points to the logical S3 key rather than the internal version storage path.
|
||||
//
|
||||
// Returns (newDir, newName, true) if the path was rewritten, or
|
||||
// (dir, name, false) if the path is not a versioned path.
|
||||
func rewriteVersionedSourcePath(dir string, name string) (string, string, bool) {
|
||||
if !strings.HasPrefix(dir, "/buckets/") {
|
||||
return dir, name, false
|
||||
}
|
||||
if !strings.HasSuffix(dir, s3_constants.VersionsFolder) {
|
||||
return dir, name, false
|
||||
}
|
||||
if !strings.HasPrefix(name, "v_") {
|
||||
return dir, name, false
|
||||
}
|
||||
// dir = "/buckets/bucket/path/to/file.xml.versions"
|
||||
// name = "v_abc123"
|
||||
// Original object: dir without ".versions" suffix → "/buckets/bucket/path/to/file.xml"
|
||||
originalObjectPath := dir[:len(dir)-len(s3_constants.VersionsFolder)]
|
||||
lastSlash := strings.LastIndex(originalObjectPath, "/")
|
||||
if lastSlash < 0 {
|
||||
return dir, name, false
|
||||
}
|
||||
newDir := originalObjectPath[:lastSlash]
|
||||
if lastSlash == 0 {
|
||||
newDir = "/"
|
||||
}
|
||||
return newDir, originalObjectPath[lastSlash+1:], true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/remote_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
// TestVersionedFilePathRewrittenForRemote verifies that the fix for
|
||||
// https://github.com/seaweedfs/seaweedfs/discussions/8481#discussioncomment-16209342
|
||||
// works correctly: internal .versions/v_{id} paths are rewritten to the
|
||||
// original S3 object key before syncing to the remote.
|
||||
func TestVersionedFilePathRewrittenForRemote(t *testing.T) {
|
||||
bucketsDir := "/buckets"
|
||||
bucketName := "devicetransaction"
|
||||
bucket := util.FullPath(bucketsDir).Child(bucketName)
|
||||
|
||||
objectPath := "9e149757-2363-11f1-bfa6-11c8ff31b539/transactionlog-2026-03-19-16-30-00.xml"
|
||||
versionId := "6761c63812bd9b64704acf08a3ba5800"
|
||||
versionFileName := "v_" + versionId
|
||||
|
||||
// The filer event for the version file creation
|
||||
versionedParentPath := string(bucket) + "/" + objectPath + s3_constants.VersionsFolder
|
||||
|
||||
// The CREATE event that remote_gateway receives
|
||||
event := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: versionedParentPath,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewParentPath: versionedParentPath,
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: versionFileName,
|
||||
Content: []byte("test content"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Verify preconditions
|
||||
if !filer_pb.IsCreate(event) {
|
||||
t.Fatal("expected create event")
|
||||
}
|
||||
if isMultipartUploadFile(event.EventNotification.NewParentPath, event.EventNotification.NewEntry.Name) {
|
||||
t.Fatal("should not be detected as multipart upload")
|
||||
}
|
||||
if !filer.HasData(event.EventNotification.NewEntry) {
|
||||
t.Fatal("version file should have data")
|
||||
}
|
||||
if !shouldSendToRemote(event.EventNotification.NewEntry) {
|
||||
t.Fatal("version file should be eligible for remote sync")
|
||||
}
|
||||
|
||||
// Apply the versioned path rewriting (as remote_gateway now does)
|
||||
parentPath, entryName := event.EventNotification.NewParentPath, event.EventNotification.NewEntry.Name
|
||||
if newParent, newName, ok := rewriteVersionedSourcePath(parentPath, entryName); ok {
|
||||
parentPath, entryName = newParent, newName
|
||||
}
|
||||
|
||||
// Compute the remote destination with the rewritten path
|
||||
remoteStorageMountLocation := &remote_pb.RemoteStorageLocation{
|
||||
Name: "central",
|
||||
Bucket: bucketName,
|
||||
Path: "/",
|
||||
}
|
||||
sourcePath := util.NewFullPath(parentPath, entryName)
|
||||
dest := toRemoteStorageLocation(bucket, sourcePath, remoteStorageMountLocation)
|
||||
|
||||
// Verify the destination does NOT contain internal .versions structure
|
||||
if strings.Contains(dest.Path, s3_constants.VersionsFolder) {
|
||||
t.Errorf("remote destination path still contains .versions: %s", dest.Path)
|
||||
}
|
||||
|
||||
expectedPath := "/" + objectPath
|
||||
if dest.Path != expectedPath {
|
||||
t.Errorf("remote destination path = %q, want %q", dest.Path, expectedPath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVersionsDirectoryFilteredByHasData verifies that the .versions
|
||||
// directory creation event is correctly filtered out (no data), so only
|
||||
// the version file event needs path rewriting.
|
||||
func TestVersionsDirectoryFilteredByHasData(t *testing.T) {
|
||||
bucket := "/buckets/devicetransaction"
|
||||
objectPath := "9e149757-2363-11f1-bfa6-11c8ff31b539/transactionlog-2026-03-19-16-30-00.xml"
|
||||
|
||||
dirEvent := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: bucket + "/9e149757-2363-11f1-bfa6-11c8ff31b539",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewParentPath: bucket + "/9e149757-2363-11f1-bfa6-11c8ff31b539",
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: objectPath[strings.LastIndex(objectPath, "/")+1:] + s3_constants.VersionsFolder,
|
||||
IsDirectory: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if filer.HasData(dirEvent.EventNotification.NewEntry) {
|
||||
t.Error(".versions directory should not have data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsVersionedPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
label string
|
||||
dir string
|
||||
name string
|
||||
isDir bool
|
||||
expected bool
|
||||
}{
|
||||
// Version file inside .versions directory (file, v_ prefix)
|
||||
{
|
||||
label: "version file in .versions dir",
|
||||
dir: "/buckets/mybucket/path/to/file.xml" + s3_constants.VersionsFolder,
|
||||
name: "v_6761c63812bd9b64704acf08a3ba5800",
|
||||
isDir: false,
|
||||
expected: true,
|
||||
},
|
||||
// Regular file (not versioned)
|
||||
{
|
||||
label: "regular file",
|
||||
dir: "/buckets/mybucket/path/to",
|
||||
name: "file.xml",
|
||||
isDir: false,
|
||||
expected: false,
|
||||
},
|
||||
// .versions directory entry itself
|
||||
{
|
||||
label: ".versions directory entry",
|
||||
dir: "/buckets/mybucket/path/to",
|
||||
name: "file.xml" + s3_constants.VersionsFolder,
|
||||
isDir: true,
|
||||
expected: true,
|
||||
},
|
||||
// Non-version file inside .versions dir (no v_ prefix) — not internal
|
||||
{
|
||||
label: "non-version file in .versions dir",
|
||||
dir: "/buckets/mybucket/file.xml" + s3_constants.VersionsFolder,
|
||||
name: "some_other_file",
|
||||
isDir: false,
|
||||
expected: false,
|
||||
},
|
||||
// User-created directory whose name ends with .versions — not
|
||||
// treated as versioned when isDir=false (file inside it)
|
||||
{
|
||||
label: "file in user dir ending with .versions but no v_ prefix",
|
||||
dir: "/buckets/mybucket/my" + s3_constants.VersionsFolder,
|
||||
name: "data.txt",
|
||||
isDir: false,
|
||||
expected: false,
|
||||
},
|
||||
// Regular directory (not .versions)
|
||||
{
|
||||
label: "regular directory",
|
||||
dir: "/buckets/mybucket/path/to",
|
||||
name: "subdir",
|
||||
isDir: true,
|
||||
expected: false,
|
||||
},
|
||||
// Entry whose name ends with .versions but is a file, not a dir
|
||||
{
|
||||
label: "file named like .versions dir",
|
||||
dir: "/buckets/mybucket/path/to",
|
||||
name: "file.xml" + s3_constants.VersionsFolder,
|
||||
isDir: false,
|
||||
expected: false,
|
||||
},
|
||||
// Non-bucket mount: .versions dir should not match
|
||||
{
|
||||
label: ".versions dir outside /buckets/",
|
||||
dir: "/mnt/remote/path/to",
|
||||
name: "file.xml" + s3_constants.VersionsFolder,
|
||||
isDir: true,
|
||||
expected: false,
|
||||
},
|
||||
// Non-bucket mount: v_ file should not match
|
||||
{
|
||||
label: "v_ file outside /buckets/",
|
||||
dir: "/data/archive/file.xml" + s3_constants.VersionsFolder,
|
||||
name: "v_abc123",
|
||||
isDir: false,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.label, func(t *testing.T) {
|
||||
got := isVersionedPath(tt.dir, tt.name, tt.isDir)
|
||||
if got != tt.expected {
|
||||
t.Errorf("isVersionedPath(%q, %q, %v) = %v, want %v",
|
||||
tt.dir, tt.name, tt.isDir, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteMarkerDetectedBeforeHasDataFilter verifies that a delete marker
|
||||
// (zero-content version entry with ExtDeleteMarkerKey="true") is detected
|
||||
// and can be propagated as a deletion, rather than being silently dropped
|
||||
// by the HasData() check.
|
||||
func TestDeleteMarkerDetectedBeforeHasDataFilter(t *testing.T) {
|
||||
bucketsDir := "/buckets"
|
||||
bucketName := "devicetransaction"
|
||||
bucket := util.FullPath(bucketsDir).Child(bucketName)
|
||||
|
||||
objectPath := "docs/report.pdf"
|
||||
versionId := "aabb112233445566"
|
||||
versionFileName := "v_" + versionId
|
||||
versionedParentPath := string(bucket) + "/" + objectPath + s3_constants.VersionsFolder
|
||||
|
||||
// A delete marker CREATE event: has ExtDeleteMarkerKey but no content
|
||||
deleteMarkerEntry := &filer_pb.Entry{
|
||||
Name: versionFileName,
|
||||
Extended: map[string][]byte{
|
||||
s3_constants.ExtDeleteMarkerKey: []byte("true"),
|
||||
s3_constants.ExtVersionIdKey: []byte(versionId),
|
||||
},
|
||||
// Content and Chunks are nil → HasData() returns false
|
||||
}
|
||||
|
||||
// Preconditions
|
||||
if filer.HasData(deleteMarkerEntry) {
|
||||
t.Fatal("delete marker should have no data")
|
||||
}
|
||||
if !isDeleteMarker(deleteMarkerEntry) {
|
||||
t.Fatal("should be detected as a delete marker")
|
||||
}
|
||||
|
||||
// The versioned path should be rewritable to the original key
|
||||
newParent, newName, ok := rewriteVersionedSourcePath(versionedParentPath, deleteMarkerEntry.Name)
|
||||
if !ok {
|
||||
t.Fatal("delete marker path should be rewritable")
|
||||
}
|
||||
|
||||
// Verify the rewritten path points to the original object
|
||||
remoteStorageMountLocation := &remote_pb.RemoteStorageLocation{
|
||||
Name: "central",
|
||||
Bucket: bucketName,
|
||||
Path: "/",
|
||||
}
|
||||
dest := toRemoteStorageLocation(bucket, util.NewFullPath(newParent, newName), remoteStorageMountLocation)
|
||||
|
||||
expectedPath := "/" + objectPath
|
||||
if dest.Path != expectedPath {
|
||||
t.Errorf("delete marker destination = %q, want %q", dest.Path, expectedPath)
|
||||
}
|
||||
if strings.Contains(dest.Path, s3_constants.VersionsFolder) {
|
||||
t.Errorf("delete marker destination should not contain .versions: %s", dest.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteVersionedSourcePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dir string
|
||||
entryName string
|
||||
wantDir string
|
||||
wantName string
|
||||
wantChanged bool
|
||||
}{
|
||||
{
|
||||
name: "version file in .versions dir",
|
||||
dir: "/buckets/bucket/path/to/file.xml" + s3_constants.VersionsFolder,
|
||||
entryName: "v_6761c63812bd9b64704acf08a3ba5800",
|
||||
wantDir: "/buckets/bucket/path/to",
|
||||
wantName: "file.xml",
|
||||
wantChanged: true,
|
||||
},
|
||||
{
|
||||
name: "regular file",
|
||||
dir: "/buckets/bucket/path/to",
|
||||
entryName: "file.xml",
|
||||
wantDir: "/buckets/bucket/path/to",
|
||||
wantName: "file.xml",
|
||||
wantChanged: false,
|
||||
},
|
||||
{
|
||||
name: "version file at bucket root",
|
||||
dir: "/buckets/bucket/report.pdf" + s3_constants.VersionsFolder,
|
||||
entryName: "v_abc123",
|
||||
wantDir: "/buckets/bucket",
|
||||
wantName: "report.pdf",
|
||||
wantChanged: true,
|
||||
},
|
||||
{
|
||||
name: "non-bucket path not rewritten",
|
||||
dir: "/file.xml" + s3_constants.VersionsFolder,
|
||||
entryName: "v_abc123",
|
||||
wantDir: "/file.xml" + s3_constants.VersionsFolder,
|
||||
wantName: "v_abc123",
|
||||
wantChanged: false,
|
||||
},
|
||||
{
|
||||
name: "non-bucket mount not rewritten",
|
||||
dir: "/mnt/remote/file.xml" + s3_constants.VersionsFolder,
|
||||
entryName: "v_abc123",
|
||||
wantDir: "/mnt/remote/file.xml" + s3_constants.VersionsFolder,
|
||||
wantName: "v_abc123",
|
||||
wantChanged: false,
|
||||
},
|
||||
{
|
||||
name: "non-version file in .versions dir",
|
||||
dir: "/buckets/bucket/file.xml" + s3_constants.VersionsFolder,
|
||||
entryName: "some_other_file",
|
||||
wantDir: "/buckets/bucket/file.xml" + s3_constants.VersionsFolder,
|
||||
wantName: "some_other_file",
|
||||
wantChanged: false,
|
||||
},
|
||||
{
|
||||
name: "dir not ending in .versions",
|
||||
dir: "/buckets/bucket/path/to",
|
||||
entryName: "v_abc123",
|
||||
wantDir: "/buckets/bucket/path/to",
|
||||
wantName: "v_abc123",
|
||||
wantChanged: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotDir, gotName, gotChanged := rewriteVersionedSourcePath(tt.dir, tt.entryName)
|
||||
if gotDir != tt.wantDir || gotName != tt.wantName || gotChanged != tt.wantChanged {
|
||||
t.Errorf("rewriteVersionedSourcePath(%q, %q) = (%q, %q, %v), want (%q, %q, %v)",
|
||||
tt.dir, tt.entryName, gotDir, gotName, gotChanged, tt.wantDir, tt.wantName, tt.wantChanged)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -349,6 +349,7 @@ func RunMount(option *MountOptions, umask os.FileMode) bool {
|
||||
RdmaMaxConcurrent: *option.rdmaMaxConcurrent,
|
||||
RdmaTimeoutMs: *option.rdmaTimeoutMs,
|
||||
DirIdleEvictSec: *option.dirIdleEvictSec,
|
||||
WritebackCache: option.writebackCache != nil && *option.writebackCache,
|
||||
})
|
||||
|
||||
// create mount root
|
||||
@@ -396,6 +397,10 @@ func RunMount(option *MountOptions, umask os.FileMode) bool {
|
||||
|
||||
server.Serve()
|
||||
|
||||
// Wait for any pending background flushes (writebackCache async mode)
|
||||
// before clearing caches, to prevent data loss during clean unmount.
|
||||
seaweedFileSystem.WaitForAsyncFlush()
|
||||
|
||||
seaweedFileSystem.ClearCacheDir()
|
||||
|
||||
return true
|
||||
|
||||
@@ -128,9 +128,7 @@ func (fsw *FilerStoreWrapper) InsertEntry(ctx context.Context, entry *Entry) err
|
||||
}()
|
||||
|
||||
filer_pb.BeforeEntrySerialization(entry.GetChunks())
|
||||
if entry.Mime == "application/octet-stream" {
|
||||
entry.Mime = ""
|
||||
}
|
||||
normalizeEntryMimeForStore(entry)
|
||||
|
||||
if err := fsw.handleUpdateToHardLinks(ctx, entry); err != nil {
|
||||
return err
|
||||
@@ -150,9 +148,7 @@ func (fsw *FilerStoreWrapper) UpdateEntry(ctx context.Context, entry *Entry) err
|
||||
}()
|
||||
|
||||
filer_pb.BeforeEntrySerialization(entry.GetChunks())
|
||||
if entry.Mime == "application/octet-stream" {
|
||||
entry.Mime = ""
|
||||
}
|
||||
normalizeEntryMimeForStore(entry)
|
||||
|
||||
if err := fsw.handleUpdateToHardLinks(ctx, entry); err != nil {
|
||||
return err
|
||||
@@ -162,6 +158,15 @@ func (fsw *FilerStoreWrapper) UpdateEntry(ctx context.Context, entry *Entry) err
|
||||
return actualStore.UpdateEntry(ctx, entry)
|
||||
}
|
||||
|
||||
func normalizeEntryMimeForStore(entry *Entry) {
|
||||
if entry == nil {
|
||||
return
|
||||
}
|
||||
if !entry.IsDirectory() && entry.Mime == "application/octet-stream" {
|
||||
entry.Mime = ""
|
||||
}
|
||||
}
|
||||
|
||||
func (fsw *FilerStoreWrapper) FindEntry(ctx context.Context, fp util.FullPath) (entry *Entry, err error) {
|
||||
ctx = context.WithoutCancel(ctx)
|
||||
actualStore := fsw.getActualStore(fp)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package filer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFilerStoreWrapperMimeNormalization(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mode os.FileMode
|
||||
wantMime string
|
||||
}{
|
||||
{
|
||||
name: "files strip octet-stream",
|
||||
mode: 0o660,
|
||||
wantMime: "",
|
||||
},
|
||||
{
|
||||
name: "directories keep octet-stream",
|
||||
mode: os.ModeDir | 0o770,
|
||||
wantMime: "application/octet-stream",
|
||||
},
|
||||
}
|
||||
|
||||
operations := []struct {
|
||||
name string
|
||||
run func(*FilerStoreWrapper, context.Context, *Entry) error
|
||||
}{
|
||||
{
|
||||
name: "insert",
|
||||
run: func(fsw *FilerStoreWrapper, ctx context.Context, entry *Entry) error {
|
||||
return fsw.InsertEntry(ctx, entry)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update",
|
||||
run: func(fsw *FilerStoreWrapper, ctx context.Context, entry *Entry) error {
|
||||
return fsw.UpdateEntry(ctx, entry)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
for _, op := range operations {
|
||||
t.Run(tt.name+"/"+op.name, func(t *testing.T) {
|
||||
store := newStubFilerStore()
|
||||
wrapper := NewFilerStoreWrapper(store)
|
||||
entry := &Entry{
|
||||
FullPath: util.FullPath("/buckets/test/object"),
|
||||
Attr: Attr{
|
||||
Mode: tt.mode,
|
||||
Mime: "application/octet-stream",
|
||||
},
|
||||
}
|
||||
|
||||
err := op.run(wrapper, context.Background(), entry)
|
||||
require.NoError(t, err)
|
||||
|
||||
storedEntry, findErr := store.FindEntry(context.Background(), entry.FullPath)
|
||||
require.NoError(t, findErr)
|
||||
assert.Equal(t, tt.wantMime, storedEntry.Mime)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -630,7 +630,21 @@ func (buf *buffer) someDigits(i, d int) int {
|
||||
return copy(buf.tmp[i:], buf.tmp[j:])
|
||||
}
|
||||
|
||||
// logJSON is a helper that formats and outputs a JSON log line.
|
||||
// Used by println, printDepth, and printf to avoid code duplication.
|
||||
// depth is incremented by 1 to account for this extra call frame.
|
||||
func (l *loggingT) logJSON(s severity, depth int, msg string) {
|
||||
buf, file, line := l.formatJSON(s, depth+1)
|
||||
buf.WriteString(jsonEscapeString(strings.TrimRight(msg, "\n")))
|
||||
finishJSON(buf)
|
||||
l.output(s, buf, file, line, false)
|
||||
}
|
||||
|
||||
func (l *loggingT) println(s severity, args ...interface{}) {
|
||||
if IsJSONMode() {
|
||||
l.logJSON(s, 0, fmt.Sprintln(args...))
|
||||
return
|
||||
}
|
||||
buf, file, line := l.header(s, 0)
|
||||
fmt.Fprintln(buf, args...)
|
||||
l.output(s, buf, file, line, false)
|
||||
@@ -641,6 +655,10 @@ func (l *loggingT) print(s severity, args ...interface{}) {
|
||||
}
|
||||
|
||||
func (l *loggingT) printDepth(s severity, depth int, args ...interface{}) {
|
||||
if IsJSONMode() {
|
||||
l.logJSON(s, depth, fmt.Sprint(args...))
|
||||
return
|
||||
}
|
||||
buf, file, line := l.header(s, depth)
|
||||
fmt.Fprint(buf, args...)
|
||||
if buf.Bytes()[buf.Len()-1] != '\n' {
|
||||
@@ -650,6 +668,10 @@ func (l *loggingT) printDepth(s severity, depth int, args ...interface{}) {
|
||||
}
|
||||
|
||||
func (l *loggingT) printf(s severity, format string, args ...interface{}) {
|
||||
if IsJSONMode() {
|
||||
l.logJSON(s, 0, fmt.Sprintf(format, args...))
|
||||
return
|
||||
}
|
||||
buf, file, line := l.header(s, 0)
|
||||
fmt.Fprintf(buf, format, args...)
|
||||
if buf.Bytes()[buf.Len()-1] != '\n' {
|
||||
@@ -662,6 +684,36 @@ func (l *loggingT) printf(s severity, format string, args ...interface{}) {
|
||||
// alsoLogToStderr is true, the log message always appears on standard error; it
|
||||
// will also appear in the log file unless --logtostderr is set.
|
||||
func (l *loggingT) printWithFileLine(s severity, file string, line int, alsoToStderr bool, args ...interface{}) {
|
||||
if IsJSONMode() {
|
||||
buf := l.getBuffer()
|
||||
now := timeNow()
|
||||
buf.WriteString(`{"ts":"`)
|
||||
buf.WriteString(now.UTC().Format(time.RFC3339Nano))
|
||||
buf.WriteString(`","level":"`)
|
||||
switch {
|
||||
case s == infoLog:
|
||||
buf.WriteString("INFO")
|
||||
case s == warningLog:
|
||||
buf.WriteString("WARNING")
|
||||
case s == errorLog:
|
||||
buf.WriteString("ERROR")
|
||||
case s >= fatalLog:
|
||||
buf.WriteString("FATAL")
|
||||
}
|
||||
buf.WriteString(`","file":"`)
|
||||
buf.WriteString(jsonEscapeString(file))
|
||||
buf.WriteString(`","line":`)
|
||||
if line < 0 {
|
||||
line = 0
|
||||
}
|
||||
buf.WriteString(itoa(line))
|
||||
buf.WriteString(`,"msg":"`)
|
||||
msg := fmt.Sprint(args...)
|
||||
buf.WriteString(jsonEscapeString(strings.TrimRight(msg, "\n")))
|
||||
finishJSON(buf)
|
||||
l.output(s, buf, file, line, alsoToStderr)
|
||||
return
|
||||
}
|
||||
buf := l.formatHeader(s, file, line)
|
||||
fmt.Fprint(buf, args...)
|
||||
if buf.Bytes()[buf.Len()-1] != '\n' {
|
||||
@@ -852,10 +904,16 @@ func (sb *syncBuffer) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
// rotateFile closes the syncBuffer's file and starts a new one.
|
||||
// If compression is enabled, the old file is gzipped in the background.
|
||||
func (sb *syncBuffer) rotateFile(now time.Time) error {
|
||||
if sb.file != nil {
|
||||
oldName := sb.file.Name()
|
||||
sb.Flush()
|
||||
sb.file.Close()
|
||||
// Compress the rotated file in background (non-blocking)
|
||||
if IsCompressRotated() && oldName != "" {
|
||||
go compressFile(oldName)
|
||||
}
|
||||
}
|
||||
var err error
|
||||
sb.file, _, err = create(severityName[sb.sev], now)
|
||||
@@ -867,6 +925,11 @@ func (sb *syncBuffer) rotateFile(now time.Time) error {
|
||||
|
||||
sb.Writer = bufio.NewWriterSize(sb.file, bufferSize)
|
||||
|
||||
// Skip text header in JSON mode to keep files as valid NDJSON.
|
||||
if IsJSONMode() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write header.
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "Log file created at: %s\n", now.Format("2006/01/02 15:04:05"))
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package glog
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// CompressRotated controls whether rotated log files are gzip-compressed.
|
||||
// 0 = disabled (default), 1 = enabled.
|
||||
var compressRotated int32
|
||||
|
||||
// SetCompressRotated enables or disables gzip compression of rotated log files.
|
||||
// When enabled, after a log file is rotated the old file is compressed in a
|
||||
// background goroutine (non-blocking to the logging path).
|
||||
func SetCompressRotated(enabled bool) {
|
||||
if enabled {
|
||||
atomic.StoreInt32(&compressRotated, 1)
|
||||
} else {
|
||||
atomic.StoreInt32(&compressRotated, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// IsCompressRotated returns whether compression of rotated files is active.
|
||||
func IsCompressRotated() bool {
|
||||
return atomic.LoadInt32(&compressRotated) == 1
|
||||
}
|
||||
|
||||
// compressFile compresses the given file with gzip and removes the original.
|
||||
// This runs in a background goroutine to avoid blocking the log write path.
|
||||
// If any step fails the original file is kept intact.
|
||||
func compressFile(path string) {
|
||||
// Skip if already compressed
|
||||
if strings.HasSuffix(path, ".gz") {
|
||||
return
|
||||
}
|
||||
|
||||
src, err := os.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
dstPath := path + ".gz"
|
||||
tmpPath := dstPath + ".tmp"
|
||||
dst, err := os.Create(tmpPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var success bool
|
||||
defer func() {
|
||||
dst.Close()
|
||||
if !success {
|
||||
os.Remove(tmpPath)
|
||||
}
|
||||
}()
|
||||
|
||||
gz, err := gzip.NewWriterLevel(dst, gzip.BestSpeed)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = io.Copy(gz, src); err != nil {
|
||||
gz.Close()
|
||||
return
|
||||
}
|
||||
|
||||
if err = gz.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = dst.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.Rename(tmpPath, dstPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
success = true
|
||||
|
||||
// Only remove original after successful compression
|
||||
os.Remove(path)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package glog
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompressRotated_Toggle(t *testing.T) {
|
||||
if IsCompressRotated() {
|
||||
t.Error("compression should be off by default")
|
||||
}
|
||||
|
||||
SetCompressRotated(true)
|
||||
if !IsCompressRotated() {
|
||||
t.Error("compression should be on after SetCompressRotated(true)")
|
||||
}
|
||||
|
||||
SetCompressRotated(false)
|
||||
if IsCompressRotated() {
|
||||
t.Error("compression should be off after SetCompressRotated(false)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompressFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.log")
|
||||
|
||||
content := "line 1\nline 2\nline 3\n"
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
compressFile(path)
|
||||
|
||||
// Original should be deleted
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Error("original file should be deleted after compression")
|
||||
}
|
||||
|
||||
// .gz should exist
|
||||
gzPath := path + ".gz"
|
||||
if _, err := os.Stat(gzPath); err != nil {
|
||||
t.Fatalf(".gz file should exist: %v", err)
|
||||
}
|
||||
|
||||
// Verify .gz content
|
||||
f, err := os.Open(gzPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open .gz: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create gzip reader: %v", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
decompressed, err := io.ReadAll(gz)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decompress: %v", err)
|
||||
}
|
||||
|
||||
if string(decompressed) != content {
|
||||
t.Errorf("decompressed content mismatch: got %q, want %q", string(decompressed), content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompressFile_AlreadyGz(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.log.gz")
|
||||
|
||||
if err := os.WriteFile(path, []byte("fake gz"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Should skip .gz files
|
||||
compressFile(path)
|
||||
|
||||
// File should still exist unchanged
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != "fake gz" {
|
||||
t.Error("should not modify .gz files")
|
||||
}
|
||||
|
||||
// No .gz.gz should exist
|
||||
if _, err := os.Stat(path + ".gz"); !os.IsNotExist(err) {
|
||||
t.Error(".gz.gz should not be created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompressFile_NonExistent(t *testing.T) {
|
||||
// Should not panic on missing file
|
||||
missing := filepath.Join(t.TempDir(), "does-not-exist.log")
|
||||
compressFile(missing)
|
||||
}
|
||||
|
||||
func TestCompressFile_EmptyFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "empty.log")
|
||||
|
||||
if err := os.WriteFile(path, []byte{}, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
compressFile(path)
|
||||
|
||||
// .gz should exist even for empty files
|
||||
gzPath := path + ".gz"
|
||||
if _, err := os.Stat(gzPath); err != nil {
|
||||
t.Errorf(".gz should exist for empty file: %v", err)
|
||||
}
|
||||
|
||||
// Original should be removed
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Error("original empty file should be deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompressFile_LargeContent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "large.log")
|
||||
|
||||
// Write 1MB of repetitive content (compresses well)
|
||||
content := strings.Repeat("I0319 12:00:00.000000 12345 server.go:42] repeated log line\n", 15000)
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
origSize := int64(len(content))
|
||||
compressFile(path)
|
||||
|
||||
gzPath := path + ".gz"
|
||||
info, err := os.Stat(gzPath)
|
||||
if err != nil {
|
||||
t.Fatalf(".gz missing: %v", err)
|
||||
}
|
||||
|
||||
// Compressed should be significantly smaller
|
||||
ratio := float64(info.Size()) / float64(origSize) * 100
|
||||
if ratio > 20 {
|
||||
t.Errorf("compression ratio too high: %.1f%% (expected < 20%% for repetitive logs)", ratio)
|
||||
}
|
||||
}
|
||||
+30
-8
@@ -64,6 +64,14 @@ var logMaxFiles = flag.Int("log_max_files", 5, "Maximum number of log files to k
|
||||
// The default is 168 hours (7 days). Set to 0 to disable time-based rotation.
|
||||
var logRotateHours = flag.Int("log_rotate_hours", 168, "Rotate log files after this many hours (default: 168 = 7 days, 0 = disabled)")
|
||||
|
||||
// logJSON enables JSON-formatted log output (one JSON object per line).
|
||||
// Useful for integration with ELK, Loki, Datadog, and other log aggregation systems.
|
||||
var logJSON = flag.Bool("log_json", false, "Output logs in JSON format instead of glog text format")
|
||||
|
||||
// logCompress enables gzip compression of rotated log files.
|
||||
// Compressed files get a .gz suffix. Compression runs in the background.
|
||||
var logCompress = flag.Bool("log_compress", false, "Gzip-compress rotated log files to save disk space")
|
||||
|
||||
func createLogDirs() {
|
||||
// Apply flag values now that flags have been parsed.
|
||||
if *logMaxSizeMB > 0 {
|
||||
@@ -73,6 +81,10 @@ func createLogDirs() {
|
||||
MaxFileCount = *logMaxFiles
|
||||
}
|
||||
|
||||
if *logCompress {
|
||||
SetCompressRotated(true)
|
||||
}
|
||||
|
||||
if *logDir != "" {
|
||||
logDirs = append(logDirs, *logDir)
|
||||
} else {
|
||||
@@ -160,21 +172,31 @@ func create(tag string, t time.Time) (f *os.File, filename string, err error) {
|
||||
var lastErr error
|
||||
for _, dir := range logDirs {
|
||||
|
||||
// remove old logs
|
||||
// remove old logs (including .gz compressed rotated files)
|
||||
// Deduplicate .log/.log.gz pairs so concurrent compression
|
||||
// doesn't cause double-counting against MaxFileCount.
|
||||
entries, _ := os.ReadDir(dir)
|
||||
var previousLogs []string
|
||||
previousLogs := make(map[string][]string) // bare name -> actual file names
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), logPrefix) {
|
||||
previousLogs = append(previousLogs, entry.Name())
|
||||
name := entry.Name()
|
||||
bare := strings.TrimSuffix(name, ".gz")
|
||||
if strings.HasPrefix(bare, logPrefix) {
|
||||
previousLogs[bare] = append(previousLogs[bare], name)
|
||||
}
|
||||
}
|
||||
if len(previousLogs) >= MaxFileCount {
|
||||
sort.Strings(previousLogs)
|
||||
for i, entry := range previousLogs {
|
||||
if i > len(previousLogs)-MaxFileCount {
|
||||
var keys []string
|
||||
for bare := range previousLogs {
|
||||
keys = append(keys, bare)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for i, bare := range keys {
|
||||
if i > len(keys)-MaxFileCount {
|
||||
break
|
||||
}
|
||||
os.Remove(filepath.Join(dir, entry))
|
||||
for _, name := range previousLogs[bare] {
|
||||
os.Remove(filepath.Join(dir, name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package glog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// JSONMode controls whether log output is in JSON format.
|
||||
// 0 = classic glog text (default), 1 = JSON lines.
|
||||
// Safe for concurrent access.
|
||||
var jsonMode int32
|
||||
|
||||
// jsonFlagOnce ensures the --log_json flag is applied on first use,
|
||||
// even when -logtostderr prevents createLogDirs() from running.
|
||||
var jsonFlagOnce sync.Once
|
||||
|
||||
// SetJSONMode enables or disables JSON-formatted log output.
|
||||
// When enabled, each log line is a single JSON object:
|
||||
//
|
||||
// {"ts":"2006-01-02T15:04:05.000000Z","level":"INFO","file":"server.go","line":42,"msg":"..."}
|
||||
//
|
||||
// This is useful for log aggregation systems (ELK, Loki, Datadog, etc).
|
||||
func SetJSONMode(enabled bool) {
|
||||
// Prevent lazy flag initialization from overriding this explicit call.
|
||||
jsonFlagOnce.Do(func() {})
|
||||
if enabled {
|
||||
atomic.StoreInt32(&jsonMode, 1)
|
||||
} else {
|
||||
atomic.StoreInt32(&jsonMode, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// IsJSONMode returns whether JSON mode is currently active.
|
||||
// On first call, it applies the --log_json flag value if set.
|
||||
func IsJSONMode() bool {
|
||||
jsonFlagOnce.Do(func() {
|
||||
if logJSON != nil && *logJSON {
|
||||
atomic.StoreInt32(&jsonMode, 1)
|
||||
}
|
||||
})
|
||||
return atomic.LoadInt32(&jsonMode) == 1
|
||||
}
|
||||
|
||||
// formatJSON builds a JSON log line without using encoding/json
|
||||
// to avoid allocations and keep it as fast as the text path.
|
||||
// Output: {"ts":"...","level":"...","file":"...","line":N,"msg":"..."}\n
|
||||
func (l *loggingT) formatJSON(s severity, depth int) (*buffer, string, int) {
|
||||
_, file, line, ok := runtime.Caller(3 + depth)
|
||||
if !ok {
|
||||
file = "???"
|
||||
line = 1
|
||||
} else {
|
||||
slash := strings.LastIndex(file, "/")
|
||||
if slash >= 0 {
|
||||
file = file[slash+1:]
|
||||
}
|
||||
}
|
||||
|
||||
buf := l.getBuffer()
|
||||
now := timeNow()
|
||||
|
||||
buf.WriteString(`{"ts":"`)
|
||||
buf.WriteString(now.UTC().Format(time.RFC3339Nano))
|
||||
buf.WriteString(`","level":"`)
|
||||
|
||||
switch {
|
||||
case s == infoLog:
|
||||
buf.WriteString("INFO")
|
||||
case s == warningLog:
|
||||
buf.WriteString("WARNING")
|
||||
case s == errorLog:
|
||||
buf.WriteString("ERROR")
|
||||
case s >= fatalLog:
|
||||
buf.WriteString("FATAL")
|
||||
}
|
||||
|
||||
buf.WriteString(`","file":"`)
|
||||
buf.WriteString(jsonEscapeString(file))
|
||||
buf.WriteString(`","line":`)
|
||||
// Write line number without fmt.Sprintf
|
||||
buf.WriteString(itoa(line))
|
||||
buf.WriteString(`,"msg":"`)
|
||||
|
||||
return buf, file, line
|
||||
}
|
||||
|
||||
// finishJSON closes the JSON object and adds a newline.
|
||||
func finishJSON(buf *buffer) {
|
||||
buf.WriteString("\"}\n")
|
||||
}
|
||||
|
||||
// jsonEscapeString escapes a string for safe inclusion in JSON (RFC 8259).
|
||||
// Handles: \, ", \n, \r, \t, control characters, and invalid UTF-8 sequences.
|
||||
func jsonEscapeString(s string) string {
|
||||
// Fast path: no special chars and valid UTF-8
|
||||
needsEscape := false
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c == '"' || c == '\\' || c < 0x20 || c > 0x7e {
|
||||
needsEscape = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !needsEscape {
|
||||
return s
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(len(s) + 10)
|
||||
for i := 0; i < len(s); {
|
||||
c := s[i]
|
||||
switch {
|
||||
case c == '"':
|
||||
b.WriteString(`\"`)
|
||||
i++
|
||||
case c == '\\':
|
||||
b.WriteString(`\\`)
|
||||
i++
|
||||
case c == '\n':
|
||||
b.WriteString(`\n`)
|
||||
i++
|
||||
case c == '\r':
|
||||
b.WriteString(`\r`)
|
||||
i++
|
||||
case c == '\t':
|
||||
b.WriteString(`\t`)
|
||||
i++
|
||||
case c < 0x20:
|
||||
fmt.Fprintf(&b, `\u%04x`, c)
|
||||
i++
|
||||
case c < utf8.RuneSelf:
|
||||
b.WriteByte(c)
|
||||
i++
|
||||
default:
|
||||
r, size := utf8.DecodeRuneInString(s[i:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
b.WriteString(`\ufffd`)
|
||||
i++
|
||||
} else {
|
||||
b.WriteString(s[i : i+size])
|
||||
i += size
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// itoa converts an integer to a string.
|
||||
func itoa(i int) string {
|
||||
return strconv.Itoa(i)
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package glog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestJSONMode_Toggle(t *testing.T) {
|
||||
// Default is off
|
||||
if IsJSONMode() {
|
||||
t.Error("JSON mode should be off by default")
|
||||
}
|
||||
|
||||
SetJSONMode(true)
|
||||
if !IsJSONMode() {
|
||||
t.Error("JSON mode should be on after SetJSONMode(true)")
|
||||
}
|
||||
|
||||
SetJSONMode(false)
|
||||
if IsJSONMode() {
|
||||
t.Error("JSON mode should be off after SetJSONMode(false)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONMode_Output(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
defer SetJSONMode(false)
|
||||
|
||||
SetJSONMode(true)
|
||||
|
||||
defer func(previous func() time.Time) { timeNow = previous }(timeNow)
|
||||
timeNow = func() time.Time {
|
||||
return time.Date(2026, 3, 19, 15, 30, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
Info("hello json")
|
||||
|
||||
output := contents(infoLog)
|
||||
if !strings.HasPrefix(output, "{") {
|
||||
t.Fatalf("JSON output should start with '{', got: %q", output)
|
||||
}
|
||||
|
||||
// Parse as JSON
|
||||
var parsed map[string]interface{}
|
||||
// Trim trailing newline
|
||||
line := strings.TrimSpace(output)
|
||||
if err := json.Unmarshal([]byte(line), &parsed); err != nil {
|
||||
t.Fatalf("output is not valid JSON: %v\noutput: %q", err, line)
|
||||
}
|
||||
|
||||
// Check required fields
|
||||
if _, ok := parsed["ts"]; !ok {
|
||||
t.Error("JSON missing 'ts' field")
|
||||
}
|
||||
if level, ok := parsed["level"]; !ok || level != "INFO" {
|
||||
t.Errorf("expected level=INFO, got %v", level)
|
||||
}
|
||||
if _, ok := parsed["file"]; !ok {
|
||||
t.Error("JSON missing 'file' field")
|
||||
}
|
||||
if _, ok := parsed["line"]; !ok {
|
||||
t.Error("JSON missing 'line' field")
|
||||
}
|
||||
if msg, ok := parsed["msg"]; !ok || msg != "hello json" {
|
||||
t.Errorf("expected msg='hello json', got %v", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONMode_AllLevels(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
defer SetJSONMode(false)
|
||||
SetJSONMode(true)
|
||||
|
||||
Info("info msg")
|
||||
Warning("warn msg")
|
||||
Error("error msg")
|
||||
|
||||
for _, sev := range []severity{infoLog, warningLog, errorLog} {
|
||||
output := contents(sev)
|
||||
if output == "" {
|
||||
continue
|
||||
}
|
||||
// Each line should be valid JSON
|
||||
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var parsed map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(line), &parsed); err != nil {
|
||||
t.Errorf("severity %d: invalid JSON: %v\nline: %q", sev, err, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONMode_Infof(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
defer SetJSONMode(false)
|
||||
SetJSONMode(true)
|
||||
|
||||
Infof("count=%d name=%s", 42, "test")
|
||||
|
||||
output := strings.TrimSpace(contents(infoLog))
|
||||
var parsed map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(output), &parsed); err != nil {
|
||||
t.Fatalf("Infof JSON invalid: %v\noutput: %q", err, output)
|
||||
}
|
||||
msg := parsed["msg"].(string)
|
||||
if !strings.Contains(msg, "count=42") || !strings.Contains(msg, "name=test") {
|
||||
t.Errorf("Infof message wrong: %q", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONEscapeString(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"hello", "hello"},
|
||||
{`say "hi"`, `say \"hi\"`},
|
||||
{"line1\nline2", `line1\nline2`},
|
||||
{"tab\there", `tab\there`},
|
||||
{`back\slash`, `back\\slash`},
|
||||
{"ctrl\x00char", `ctrl\u0000char`},
|
||||
{"", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := jsonEscapeString(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("jsonEscapeString(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestItoa(t *testing.T) {
|
||||
tests := []struct {
|
||||
input int
|
||||
want string
|
||||
}{
|
||||
{0, "0"},
|
||||
{5, "5"},
|
||||
{9, "9"},
|
||||
{10, "10"},
|
||||
{42, "42"},
|
||||
{12345, "12345"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := itoa(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("itoa(%d) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONMode_TextFallback(t *testing.T) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
|
||||
// With JSON mode OFF, output should NOT be JSON
|
||||
SetJSONMode(false)
|
||||
Info("text mode")
|
||||
|
||||
output := contents(infoLog)
|
||||
if strings.HasPrefix(output, "{") {
|
||||
t.Error("text mode should not produce JSON output")
|
||||
}
|
||||
if !strings.Contains(output, "text mode") {
|
||||
t.Error("text mode output missing message")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJSONMode(b *testing.B) {
|
||||
setFlags()
|
||||
defer logging.swap(logging.newBuffers())
|
||||
defer SetJSONMode(false)
|
||||
SetJSONMode(true)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Info("benchmark json message")
|
||||
}
|
||||
}
|
||||
@@ -24,10 +24,15 @@ type FileHandle struct {
|
||||
wfs *WFS
|
||||
|
||||
// cache file has been written to
|
||||
dirtyMetadata bool
|
||||
dirtyPages *PageWriter
|
||||
reader *filer.ChunkReadAt
|
||||
contentType string
|
||||
dirtyMetadata bool
|
||||
dirtyPages *PageWriter
|
||||
reader *filer.ChunkReadAt
|
||||
contentType string
|
||||
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)
|
||||
|
||||
isDeleted bool
|
||||
|
||||
|
||||
@@ -55,39 +55,72 @@ func (i *FileHandleToInode) AcquireFileHandle(wfs *WFS, inode uint64, entry *fil
|
||||
return fh
|
||||
}
|
||||
|
||||
func (i *FileHandleToInode) ReleaseByInode(inode uint64) {
|
||||
func (i *FileHandleToInode) ReleaseByInode(inode uint64) *FileHandle {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
fh, found := i.inode2fh[inode]
|
||||
if found {
|
||||
fh.counter--
|
||||
if fh.counter <= 0 {
|
||||
delete(i.inode2fh, inode)
|
||||
delete(i.fh2inode, fh.fh)
|
||||
fh.ReleaseHandle()
|
||||
}
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
// If the counter is already <= 0, a prior Release already started the
|
||||
// drain. Return nil to prevent double-processing (e.g. Forget after Release).
|
||||
if fh.counter <= 0 {
|
||||
return nil
|
||||
}
|
||||
fh.counter--
|
||||
if fh.counter <= 0 {
|
||||
if fh.asyncFlushPending {
|
||||
// Handle stays in fhMap so rename/unlink can find it during drain.
|
||||
return fh
|
||||
}
|
||||
delete(i.inode2fh, inode)
|
||||
delete(i.fh2inode, fh.fh)
|
||||
return fh
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *FileHandleToInode) ReleaseByHandle(fh FileHandleId) {
|
||||
func (i *FileHandleToInode) ReleaseByHandle(fh FileHandleId) *FileHandle {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
inode, found := i.fh2inode[fh]
|
||||
if !found {
|
||||
return // Handle already released or invalid
|
||||
return nil
|
||||
}
|
||||
|
||||
fhHandle, fhFound := i.inode2fh[inode]
|
||||
if !fhFound {
|
||||
delete(i.fh2inode, fh)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the counter is already <= 0, a prior Release already started the
|
||||
// drain. Return nil to prevent double-processing.
|
||||
if fhHandle.counter <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fhHandle.counter--
|
||||
if fhHandle.counter <= 0 {
|
||||
if fhHandle.asyncFlushPending {
|
||||
// Handle stays in fhMap so rename/unlink can still find it
|
||||
// via FindFileHandle during the background drain.
|
||||
return fhHandle
|
||||
}
|
||||
delete(i.inode2fh, inode)
|
||||
delete(i.fh2inode, fhHandle.fh)
|
||||
fhHandle.ReleaseHandle()
|
||||
return fhHandle
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveFileHandle removes a handle from both maps. Called after an async
|
||||
// drain completes to clean up the handle that was intentionally kept in the
|
||||
// maps during the flush.
|
||||
func (i *FileHandleToInode) RemoveFileHandle(fh FileHandleId, inode uint64) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
delete(i.inode2fh, inode)
|
||||
delete(i.fh2inode, fh)
|
||||
}
|
||||
|
||||
@@ -65,10 +65,6 @@ func (mc *MemChunk) ReadDataAt(p []byte, off int64, tsNs int64) (maxStop int64)
|
||||
if logicStart < logicStop {
|
||||
copy(p[logicStart-off:logicStop-off], mc.buf[logicStart-memChunkBaseOffset:logicStop-memChunkBaseOffset])
|
||||
maxStop = max(maxStop, logicStop)
|
||||
|
||||
if t.TsNs >= tsNs {
|
||||
println("read new data1", t.TsNs-tsNs, "ns")
|
||||
}
|
||||
}
|
||||
}
|
||||
mc.activityScore.MarkRead()
|
||||
|
||||
@@ -151,10 +151,6 @@ func (sc *SwapFileChunk) ReadDataAt(p []byte, off int64, tsNs int64) (maxStop in
|
||||
break
|
||||
}
|
||||
maxStop = max(maxStop, logicStop)
|
||||
|
||||
if t.TsNs > tsNs {
|
||||
println("read new data2", t.TsNs-tsNs, "ns")
|
||||
}
|
||||
}
|
||||
}
|
||||
//sc.memChunk.ReadDataAt(memCopy, off, tsNs)
|
||||
|
||||
+51
-8
@@ -78,6 +78,10 @@ type Option struct {
|
||||
// Directory cache refresh/eviction controls
|
||||
DirIdleEvictSec int
|
||||
|
||||
// WritebackCache enables async flush on close for improved small file write performance.
|
||||
// When true, Flush() returns immediately and data upload + metadata flush happen in background.
|
||||
WritebackCache bool
|
||||
|
||||
uniqueCacheDirForRead string
|
||||
uniqueCacheDirForWrite string
|
||||
}
|
||||
@@ -110,6 +114,16 @@ type WFS struct {
|
||||
dirHotWindow time.Duration
|
||||
dirHotThreshold int
|
||||
dirIdleEvict time.Duration
|
||||
|
||||
// asyncFlushWg tracks pending background flush goroutines for writebackCache mode.
|
||||
// Must be waited on before unmount cleanup to prevent data loss.
|
||||
asyncFlushWg sync.WaitGroup
|
||||
|
||||
// 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{}
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -151,13 +165,14 @@ func NewSeaweedFileSystem(option *Option) *WFS {
|
||||
}
|
||||
|
||||
wfs := &WFS{
|
||||
RawFileSystem: fuse.NewDefaultRawFileSystem(),
|
||||
option: option,
|
||||
signature: util.RandomInt32(),
|
||||
inodeToPath: NewInodeToPath(util.FullPath(option.FilerMountRootPath), option.CacheMetaTTlSec),
|
||||
fhMap: NewFileHandleToInode(),
|
||||
dhMap: NewDirectoryHandleToInode(),
|
||||
filerClient: filerClient, // nil for proxy mode, initialized for direct access
|
||||
RawFileSystem: fuse.NewDefaultRawFileSystem(),
|
||||
option: option,
|
||||
signature: util.RandomInt32(),
|
||||
inodeToPath: NewInodeToPath(util.FullPath(option.FilerMountRootPath), option.CacheMetaTTlSec),
|
||||
fhMap: NewFileHandleToInode(),
|
||||
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,
|
||||
@@ -204,8 +219,32 @@ func NewSeaweedFileSystem(option *Option) *WFS {
|
||||
}
|
||||
})
|
||||
grace.OnInterrupt(func() {
|
||||
// grace calls os.Exit(0) after all hooks, so WaitForAsyncFlush
|
||||
// after server.Serve() would never execute. Drain here first.
|
||||
//
|
||||
// Use a timeout to avoid hanging on Ctrl-C if the filer is
|
||||
// unreachable (metadata retry can take up to 7 seconds).
|
||||
// If the timeout expires, skip the write-cache removal so that
|
||||
// still-running goroutines can finish reading swap files.
|
||||
asyncDrained := true
|
||||
if wfs.option.WritebackCache {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wfs.asyncFlushWg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
glog.V(0).Infof("all async flushes completed before shutdown")
|
||||
case <-time.After(30 * time.Second):
|
||||
glog.Warningf("timed out waiting for async flushes — swap files preserved for in-flight uploads")
|
||||
asyncDrained = false
|
||||
}
|
||||
}
|
||||
wfs.metaCache.Shutdown()
|
||||
os.RemoveAll(option.getUniqueCacheDirForWrite())
|
||||
if asyncDrained {
|
||||
os.RemoveAll(option.getUniqueCacheDirForWrite())
|
||||
}
|
||||
os.RemoveAll(option.getUniqueCacheDirForRead())
|
||||
if wfs.rdmaClient != nil {
|
||||
wfs.rdmaClient.Close()
|
||||
@@ -240,6 +279,10 @@ func NewSeaweedFileSystem(option *Option) *WFS {
|
||||
}
|
||||
|
||||
func (wfs *WFS) StartBackgroundTasks() error {
|
||||
if wfs.option.WritebackCache {
|
||||
glog.V(0).Infof("writebackCache enabled: async flush on close() for improved small file performance")
|
||||
}
|
||||
|
||||
follower, err := wfs.subscribeFilerConfEvents()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
const asyncFlushMetadataRetries = 3
|
||||
|
||||
// completeAsyncFlush is called in a background goroutine when a file handle
|
||||
// with pending async flush work is released. It performs the deferred data
|
||||
// upload and metadata flush that was skipped in doFlush() for writebackCache mode.
|
||||
//
|
||||
// This enables close() to return immediately for small file workloads (e.g., rsync),
|
||||
// while the actual I/O happens concurrently in the background.
|
||||
//
|
||||
// The caller (submitAsyncFlush) owns asyncFlushWg and the per-inode done channel.
|
||||
func (wfs *WFS) completeAsyncFlush(fh *FileHandle) {
|
||||
// Phase 1: Flush dirty pages — seals writable chunks, uploads to volume servers, and waits.
|
||||
// The underlying UploadWithRetry already retries transient HTTP/gRPC errors internally,
|
||||
// so a failure here indicates a persistent issue; the chunk data has been freed.
|
||||
if err := fh.dirtyPages.FlushData(); err != nil {
|
||||
glog.Errorf("completeAsyncFlush inode %d: data flush failed: %v", fh.inode, err)
|
||||
// Data is lost at this point (chunks freed after internal retry exhaustion).
|
||||
// Proceed to cleanup to avoid resource leaks and unmount hangs.
|
||||
} else if fh.dirtyMetadata {
|
||||
// Phase 2: Flush metadata unless the file was explicitly unlinked.
|
||||
//
|
||||
// isDeleted is set by the Unlink handler when it finds a draining
|
||||
// handle. In that case the filer entry is already gone and
|
||||
// flushing would recreate it. The uploaded chunks become orphans
|
||||
// and are cleaned up by volume.fsck.
|
||||
if fh.isDeleted {
|
||||
glog.V(3).Infof("completeAsyncFlush inode %d: file was unlinked, skipping metadata flush", fh.inode)
|
||||
} else {
|
||||
// Resolve the current path for metadata flush.
|
||||
//
|
||||
// Try GetPath first — it reflects any rename that happened
|
||||
// after close(). If the inode mapping is gone (Forget
|
||||
// dropped it after the kernel's lookup count hit zero), fall
|
||||
// back to the dir/name saved at doFlush time. Rename also
|
||||
// updates the saved path, so the fallback is always current.
|
||||
//
|
||||
// Forget does NOT mean the file was deleted — it only means
|
||||
// the kernel evicted its cache entry.
|
||||
dir, name := fh.asyncFlushDir, fh.asyncFlushName
|
||||
fileFullPath := util.FullPath(dir).Child(name)
|
||||
|
||||
if resolvedPath, status := wfs.inodeToPath.GetPath(fh.inode); status == fuse.OK {
|
||||
dir, name = resolvedPath.DirAndName()
|
||||
fileFullPath = resolvedPath
|
||||
}
|
||||
|
||||
wfs.flushMetadataWithRetry(fh, dir, name, fileFullPath)
|
||||
}
|
||||
}
|
||||
|
||||
glog.V(3).Infof("completeAsyncFlush done inode %d fh %d", fh.inode, fh.fh)
|
||||
|
||||
// Phase 3: Destroy the upload pipeline and free resources.
|
||||
fh.ReleaseHandle()
|
||||
}
|
||||
|
||||
// flushMetadataWithRetry attempts to flush file metadata to the filer, retrying
|
||||
// with exponential backoff on transient errors. The chunk data is already on the
|
||||
// volume servers at this point; only the filer metadata reference needs persisting.
|
||||
func (wfs *WFS) flushMetadataWithRetry(fh *FileHandle, dir, name string, fileFullPath util.FullPath) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// WaitForAsyncFlush waits for all pending background flush goroutines to complete.
|
||||
// Called before unmount cleanup to ensure no data is lost.
|
||||
func (wfs *WFS) WaitForAsyncFlush() {
|
||||
wfs.asyncFlushWg.Wait()
|
||||
}
|
||||
@@ -161,6 +161,15 @@ 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
|
||||
|
||||
@@ -59,7 +59,7 @@ func (wfs *WFS) Flush(cancel <-chan struct{}, in *fuse.FlushIn) fuse.Status {
|
||||
return fuse.OK
|
||||
}
|
||||
|
||||
return wfs.doFlush(fh, in.Uid, in.Gid)
|
||||
return wfs.doFlush(fh, in.Uid, in.Gid, true)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,11 +88,12 @@ func (wfs *WFS) Fsync(cancel <-chan struct{}, in *fuse.FsyncIn) (code fuse.Statu
|
||||
return fuse.ENOENT
|
||||
}
|
||||
|
||||
return wfs.doFlush(fh, in.Uid, in.Gid)
|
||||
// Fsync is an explicit sync request — always flush synchronously
|
||||
return wfs.doFlush(fh, in.Uid, in.Gid, false)
|
||||
|
||||
}
|
||||
|
||||
func (wfs *WFS) doFlush(fh *FileHandle, uid, gid uint32) fuse.Status {
|
||||
func (wfs *WFS) doFlush(fh *FileHandle, uid, gid uint32, allowAsync bool) fuse.Status {
|
||||
|
||||
// flush works at fh level
|
||||
fileFullPath := fh.FullPath()
|
||||
@@ -100,6 +101,26 @@ func (wfs *WFS) doFlush(fh *FileHandle, uid, gid uint32) fuse.Status {
|
||||
// send the data to the OS
|
||||
glog.V(4).Infof("doFlush %s fh %d", fileFullPath, fh.fh)
|
||||
|
||||
// When writebackCache is enabled and this is a close()-triggered Flush (not fsync),
|
||||
// defer the expensive data upload + metadata flush to a background goroutine.
|
||||
// This allows the calling process (e.g., rsync) to proceed to the next file immediately.
|
||||
// POSIX does not require close() to wait for delayed I/O to complete.
|
||||
if allowAsync && wfs.option.WritebackCache && fh.dirtyMetadata {
|
||||
if wfs.IsOverQuotaWithUncommitted() {
|
||||
return fuse.Status(syscall.ENOSPC)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// Synchronous flush path (normal mode, fsync, or no dirty data)
|
||||
fh.asyncFlushPending = false
|
||||
|
||||
// Check quota including uncommitted writes for real-time enforcement
|
||||
isOverQuota := wfs.IsOverQuotaWithUncommitted()
|
||||
if !isOverQuota {
|
||||
@@ -117,6 +138,23 @@ func (wfs *WFS) doFlush(fh *FileHandle, uid, gid uint32) fuse.Status {
|
||||
return fuse.Status(syscall.ENOSPC)
|
||||
}
|
||||
|
||||
if err := wfs.flushMetadataToFiler(fh, dir, name, uid, gid); err != nil {
|
||||
glog.Errorf("%v fh %d flush: %v", fileFullPath, fh.fh, err)
|
||||
return fuse.EIO
|
||||
}
|
||||
|
||||
if IsDebugFileReadWrite {
|
||||
fh.mirrorFile.Sync()
|
||||
}
|
||||
|
||||
return fuse.OK
|
||||
}
|
||||
|
||||
// flushMetadataToFiler sends the file's chunk references and attributes to the filer.
|
||||
// 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()
|
||||
|
||||
fhActiveLock := fh.wfs.fhLockTable.AcquireLock("doFlush", fh.fh, util.ExclusiveLock)
|
||||
defer fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock)
|
||||
|
||||
@@ -144,9 +182,6 @@ func (wfs *WFS) doFlush(fh *FileHandle, uid, gid uint32) fuse.Status {
|
||||
}
|
||||
|
||||
glog.V(4).Infof("%s set chunks: %v", fileFullPath, len(entry.GetChunks()))
|
||||
//for i, chunk := range entry.GetChunks() {
|
||||
// glog.V(4).Infof("%s chunks %d: %v [%d,%d)", fileFullPath, i, chunk.GetFileIdString(), chunk.Offset, chunk.Offset+int64(chunk.Size))
|
||||
//}
|
||||
|
||||
manifestChunks, nonManifestChunks := filer.SeparateManifestChunks(entry.GetChunks())
|
||||
|
||||
@@ -183,14 +218,5 @@ func (wfs *WFS) doFlush(fh *FileHandle, uid, gid uint32) fuse.Status {
|
||||
fh.dirtyMetadata = false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf("%v fh %d flush: %v", fileFullPath, fh.fh, err)
|
||||
return fuse.EIO
|
||||
}
|
||||
|
||||
if IsDebugFileReadWrite {
|
||||
fh.mirrorFile.Sync()
|
||||
}
|
||||
|
||||
return fuse.OK
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,11 +2,18 @@ package mount
|
||||
|
||||
import (
|
||||
"github.com/seaweedfs/go-fuse/v2/fuse"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
func (wfs *WFS) AcquireHandle(inode uint64, flags, uid, gid uint32) (fileHandle *FileHandle, status fuse.Status) {
|
||||
// If there is an in-flight async flush for this inode, wait for it to
|
||||
// complete before reopening. Otherwise the new handle would be built
|
||||
// from pre-close filer metadata and its next flush could overwrite the
|
||||
// data that was just written asynchronously.
|
||||
wfs.waitForPendingAsyncFlush(inode)
|
||||
|
||||
var entry *filer_pb.Entry
|
||||
var path util.FullPath
|
||||
path, _, entry, status = wfs.maybeReadEntry(inode)
|
||||
@@ -20,8 +27,59 @@ func (wfs *WFS) AcquireHandle(inode uint64, flags, uid, gid uint32) (fileHandle
|
||||
return
|
||||
}
|
||||
|
||||
// ReleaseHandle is called from FUSE Release. For handles with a pending
|
||||
// async flush, the map removal and the pendingAsyncFlush registration are
|
||||
// done under a single lock hold so that a concurrent AcquireHandle cannot
|
||||
// slip through the gap between the two (P1-1 TOCTOU fix).
|
||||
//
|
||||
// The handle intentionally stays in fhMap during the drain so that rename
|
||||
// and unlink can still find it via FindFileHandle (P1-2 fix). It is
|
||||
// removed from fhMap only after the drain completes (RemoveFileHandle).
|
||||
func (wfs *WFS) ReleaseHandle(handleId FileHandleId) {
|
||||
wfs.fhMap.ReleaseByHandle(handleId)
|
||||
// Hold pendingAsyncFlushMu across the counter decrement and the
|
||||
// pending-flush registration. Lock ordering: pendingAsyncFlushMu → fhMap.
|
||||
wfs.pendingAsyncFlushMu.Lock()
|
||||
fhToRelease := wfs.fhMap.ReleaseByHandle(handleId)
|
||||
if fhToRelease != nil && fhToRelease.asyncFlushPending {
|
||||
done := make(chan struct{})
|
||||
wfs.pendingAsyncFlush[fhToRelease.inode] = done
|
||||
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)
|
||||
}()
|
||||
return
|
||||
}
|
||||
wfs.pendingAsyncFlushMu.Unlock()
|
||||
|
||||
if fhToRelease != nil {
|
||||
fhToRelease.ReleaseHandle()
|
||||
}
|
||||
}
|
||||
|
||||
// waitForPendingAsyncFlush blocks until any in-flight async flush for
|
||||
// the given inode completes. Called from AcquireHandle before building
|
||||
// new handle state, so the filer metadata reflects the flushed data.
|
||||
func (wfs *WFS) waitForPendingAsyncFlush(inode uint64) {
|
||||
wfs.pendingAsyncFlushMu.Lock()
|
||||
done, found := wfs.pendingAsyncFlush[inode]
|
||||
wfs.pendingAsyncFlushMu.Unlock()
|
||||
|
||||
if found {
|
||||
glog.V(3).Infof("waitForPendingAsyncFlush: waiting for inode %d", inode)
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func (wfs *WFS) GetHandle(handleId FileHandleId) *FileHandle {
|
||||
|
||||
@@ -66,5 +66,10 @@ func (wfs *WFS) Forget(nodeid, nlookup uint64) {
|
||||
wfs.inodeToPath.Forget(nodeid, nlookup, func(dir util.FullPath) {
|
||||
wfs.metaCache.DeleteFolderChildren(context.Background(), dir)
|
||||
})
|
||||
wfs.fhMap.ReleaseByInode(nodeid)
|
||||
// ReleaseByInode returns nil if the handle is already draining (counter
|
||||
// was already <= 0 from a prior Release). Only non-async handles that
|
||||
// reach counter 0 here need cleanup.
|
||||
if fhToRelease := wfs.fhMap.ReleaseByInode(nodeid); fhToRelease != nil {
|
||||
fhToRelease.ReleaseHandle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +253,12 @@ 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
|
||||
// after Forget uses the post-rename location, not the old one.
|
||||
if fh.asyncFlushPending {
|
||||
fh.asyncFlushDir = string(newParent)
|
||||
fh.asyncFlushName = newName
|
||||
}
|
||||
}
|
||||
// invalidate attr and data
|
||||
// wfs.fuseServer.InodeNotify(sourceInode, 0, -1)
|
||||
|
||||
@@ -250,24 +250,6 @@ func NewIdentityAccessManagementWithStore(option *S3ApiServerOption, filerClient
|
||||
iam.stopChan = make(chan struct{})
|
||||
iam.grpcDialOption = option.GrpcDialOption
|
||||
|
||||
// Initialize default anonymous identity
|
||||
// This ensures consistent behavior for anonymous access:
|
||||
// 1. In simple auth mode (no IAM integration):
|
||||
// - lookupAnonymous returns this identity
|
||||
// - VerifyActionPermission checks actions (which are empty) -> Denies access
|
||||
// - This preserves the secure-by-default behavior for simple auth
|
||||
// 2. In advanced IAM mode (with Policy Engine):
|
||||
// - lookupAnonymous returns this identity
|
||||
// - VerifyActionPermission proceeds to Policy Engine
|
||||
// - Policy Engine evaluates against policies (DefaultEffect=Allow if no config)
|
||||
// - This enables the flexible "Open by Default" for zero-config startup
|
||||
iam.identityAnonymous = &Identity{
|
||||
Name: "anonymous",
|
||||
Account: &AccountAnonymous,
|
||||
Actions: []Action{},
|
||||
IsStatic: true,
|
||||
}
|
||||
|
||||
// First, try to load configurations from file or filer
|
||||
startConfigFile := option.Config
|
||||
if startConfigFile == "" {
|
||||
@@ -657,16 +639,6 @@ func (iam *IdentityAccessManagement) ReplaceS3ApiConfiguration(config *iam_pb.S3
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure anonymous identity exists
|
||||
if identityAnonymous == nil {
|
||||
identityAnonymous = &Identity{
|
||||
Name: "anonymous",
|
||||
Account: accounts[AccountAnonymous.Id],
|
||||
Actions: []Action{},
|
||||
IsStatic: true,
|
||||
}
|
||||
}
|
||||
|
||||
// atomically switch
|
||||
iam.identities = identities
|
||||
iam.identityAnonymous = identityAnonymous
|
||||
@@ -921,6 +893,35 @@ func (iam *IdentityAccessManagement) MergeS3ApiConfiguration(config *iam_pb.S3Ap
|
||||
glog.V(3).Infof("Loaded service account %s for dynamic parent %s (expiration: %d)", sa.Id, sa.ParentUser, sa.Expiration)
|
||||
}
|
||||
|
||||
// If the anonymous identity was carried over from the previous state but is
|
||||
// no longer present in the credential-manager snapshot, clear it so that
|
||||
// deleted anonymous users do not persist across merges.
|
||||
if identityAnonymous != nil && !identityAnonymous.IsStatic {
|
||||
stillPresent := false
|
||||
for _, ident := range config.Identities {
|
||||
if ident.Name == s3_constants.AccountAnonymousId {
|
||||
stillPresent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !stillPresent {
|
||||
// Remove from identities slice and maps
|
||||
for i, ident := range identities {
|
||||
if ident == identityAnonymous {
|
||||
identities = append(identities[:i], identities[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
delete(nameToIdentity, identityAnonymous.Name)
|
||||
for _, cred := range identityAnonymous.Credentials {
|
||||
if accessKeyIdent[cred.AccessKey] == identityAnonymous {
|
||||
delete(accessKeyIdent, cred.AccessKey)
|
||||
}
|
||||
}
|
||||
identityAnonymous = nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, policy := range config.Policies {
|
||||
policies[policy.Name] = policy
|
||||
}
|
||||
@@ -1131,11 +1132,11 @@ func (iam *IdentityAccessManagement) LookupByAccessKey(accessKey string) (identi
|
||||
return iam.lookupByAccessKey(accessKey)
|
||||
}
|
||||
|
||||
// LookupAnonymous returns the anonymous identity if it exists
|
||||
// LookupAnonymous returns the anonymous identity if it exists and is not disabled.
|
||||
func (iam *IdentityAccessManagement) LookupAnonymous() (identity *Identity, found bool) {
|
||||
iam.m.RLock()
|
||||
defer iam.m.RUnlock()
|
||||
if iam.identityAnonymous != nil {
|
||||
if iam.identityAnonymous != nil && !iam.identityAnonymous.Disabled {
|
||||
return iam.identityAnonymous, true
|
||||
}
|
||||
return nil, false
|
||||
@@ -1450,8 +1451,10 @@ func (iam *IdentityAccessManagement) AuthSignatureOnly(r *http.Request) (*Identi
|
||||
return identity, s3err.ErrNotImplemented
|
||||
}
|
||||
case authTypeAnonymous:
|
||||
// Anonymous users can be authenticated, but authorization is handled separately
|
||||
return iam.identityAnonymous, s3err.ErrNone
|
||||
if ident, found := iam.LookupAnonymous(); found {
|
||||
return ident, s3err.ErrNone
|
||||
}
|
||||
return nil, s3err.ErrAccessDenied
|
||||
default:
|
||||
return identity, s3err.ErrNotImplemented
|
||||
}
|
||||
|
||||
@@ -4,36 +4,21 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoadIAMManagerFromConfig_OptionalConfig(t *testing.T) {
|
||||
// Mock dependencies
|
||||
filerAddressProvider := func() string { return "localhost:8888" }
|
||||
getFilerSigningKey := func() string { return "test-signing-key" }
|
||||
|
||||
// Test Case 1: Empty config path should load defaults
|
||||
iamManager, err := loadIAMManagerFromConfig("", filerAddressProvider, getFilerSigningKey)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, iamManager)
|
||||
|
||||
// Verify STS Service is initialized with defaults
|
||||
stsService := iamManager.GetSTSService()
|
||||
assert.NotNil(t, stsService)
|
||||
|
||||
// Verify defaults are applied
|
||||
// Since we can't easily access the internal config of stsService,
|
||||
// we rely on the fact that initialization succeeded without error.
|
||||
// We can also verify that the policy engine uses memory store by default.
|
||||
|
||||
// Verify Policy Engine is initialized with defaults (Memory store, Deny effect)
|
||||
// Again, internal state might be hard to access directly, but successful init implies defaults worked.
|
||||
func TestLoadIAMManagerWithNoConfig(t *testing.T) {
|
||||
// Verify that IAM can be initialized without any config
|
||||
option := &S3ApiServerOption{
|
||||
Config: "",
|
||||
}
|
||||
iamManager := NewIdentityAccessManagementWithStore(option, nil, "memory")
|
||||
assert.NotNil(t, iamManager)
|
||||
// Internal state might be hard to access directly, but successful init implies defaults worked.
|
||||
}
|
||||
|
||||
func TestLoadIAMManagerFromConfig_EmptyConfigWithFallbackKey(t *testing.T) {
|
||||
// Mock dependencies where getFilerSigningKey returns empty, forcing fallback logic
|
||||
// Initialize IAM with empty config (should trigger defaults)
|
||||
// We pass empty string for config file path
|
||||
// Initialize IAM with empty config — no anonymous identity is configured,
|
||||
// so LookupAnonymous should return not-found.
|
||||
option := &S3ApiServerOption{
|
||||
Config: "",
|
||||
IamConfig: "",
|
||||
@@ -41,10 +26,6 @@ func TestLoadIAMManagerFromConfig_EmptyConfigWithFallbackKey(t *testing.T) {
|
||||
}
|
||||
iamManager := NewIdentityAccessManagementWithStore(option, nil, "memory")
|
||||
|
||||
// Verify identityAnonymous is initialized
|
||||
// This confirms the fix for anonymous access in zero-config mode
|
||||
anonIdentity, found := iamManager.LookupAnonymous()
|
||||
assert.True(t, found, "Anonymous identity should be found by default")
|
||||
assert.NotNil(t, anonIdentity, "Anonymous identity should not be nil")
|
||||
assert.Equal(t, "anonymous", anonIdentity.Name)
|
||||
_, found := iamManager.LookupAnonymous()
|
||||
assert.False(t, found, "Anonymous identity should not be found when not explicitly configured")
|
||||
}
|
||||
|
||||
@@ -677,15 +677,17 @@ func (s3a *S3ApiServer) doListFilerEntries(client filer_pb.SeaweedFilerClient, d
|
||||
}
|
||||
|
||||
if delimiter != "/" || cursor.prefixEndsOnDelimiter {
|
||||
// When delimiter is empty (recursive mode), recurse into directories but don't add them to results
|
||||
// Only files and versioned objects should appear in results
|
||||
if cursor.prefixEndsOnDelimiter {
|
||||
cursor.prefixEndsOnDelimiter = false
|
||||
if entry.IsDirectoryKeyObject() {
|
||||
eachEntryFn(dir, entry)
|
||||
}
|
||||
} else if entry.IsDirectoryKeyObject() {
|
||||
// Directory key objects (created via PutObject with trailing "/")
|
||||
// must appear as regular keys in recursive listing mode.
|
||||
eachEntryFn(dir, entry)
|
||||
}
|
||||
// Recurse into subdirectory - don't add the directory itself to results
|
||||
// Recurse into subdirectory to list any children
|
||||
subNextMarker, subErr := s3a.doListFilerEntries(client, dir+"/"+entry.Name, "", cursor, "", delimiter, false, bucket, eachEntryFn)
|
||||
if subErr != nil {
|
||||
err = fmt.Errorf("doListFilerEntries2: %w", subErr)
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/operation"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Commands = append(Commands, &commandVolumeTierCompact{})
|
||||
}
|
||||
|
||||
type commandVolumeTierCompact struct {
|
||||
}
|
||||
|
||||
func (c *commandVolumeTierCompact) Name() string {
|
||||
return "volume.tier.compact"
|
||||
}
|
||||
|
||||
func (c *commandVolumeTierCompact) Help() string {
|
||||
return `compact remote volumes to reclaim space on cloud storage
|
||||
|
||||
volume.tier.compact [-volumeId=<volume_id>]
|
||||
volume.tier.compact [-collection=""] [-garbageThreshold=0.3]
|
||||
|
||||
e.g.:
|
||||
volume.tier.compact -volumeId=7
|
||||
volume.tier.compact -collection="mybucket" -garbageThreshold=0.2
|
||||
|
||||
This command compacts cloud tier volumes by:
|
||||
1. Downloading the .dat file from remote storage to local
|
||||
2. Running compaction to remove deleted data
|
||||
3. Uploading the compacted .dat file back to remote storage
|
||||
|
||||
This reclaims space on remote storage that was used by deleted files.
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
func (c *commandVolumeTierCompact) HasTag(CommandTag) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type remoteVolumeInfo struct {
|
||||
vid needle.VolumeId
|
||||
collection string
|
||||
remoteStorageName string
|
||||
serverAddress pb.ServerAddress
|
||||
serverUrl string
|
||||
}
|
||||
|
||||
func (c *commandVolumeTierCompact) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
|
||||
|
||||
tierCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
|
||||
volumeId := tierCommand.Int("volumeId", 0, "the volume id")
|
||||
collection := tierCommand.String("collection", "", "the collection name (supports regex)")
|
||||
garbageThreshold := tierCommand.Float64("garbageThreshold", 0.3, "compact when garbage ratio exceeds this value")
|
||||
if err = tierCommand.Parse(args); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = commandEnv.confirmIsLocked(args); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vid := needle.VolumeId(*volumeId)
|
||||
|
||||
// collect topology information
|
||||
topologyInfo, _, err := collectTopologyInfo(commandEnv, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find remote volumes
|
||||
var remoteVolumes []remoteVolumeInfo
|
||||
if vid != 0 {
|
||||
rv, found, findErr := findRemoteVolumeInTopology(topologyInfo, vid, *collection)
|
||||
if findErr != nil {
|
||||
return findErr
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("remote volume %d not found", vid)
|
||||
}
|
||||
remoteVolumes = append(remoteVolumes, rv)
|
||||
} else {
|
||||
remoteVolumes, err = collectRemoteVolumesWithInfo(topologyInfo, *collection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(remoteVolumes) == 0 {
|
||||
fmt.Fprintf(writer, "no remote volumes found\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "found %d remote volume(s) to check for compaction\n", len(remoteVolumes))
|
||||
|
||||
var failedCount int
|
||||
for _, rv := range remoteVolumes {
|
||||
if err = doVolumeTierCompact(commandEnv, writer, rv, *garbageThreshold); err != nil {
|
||||
fmt.Fprintf(writer, "error compacting volume %d: %v\n", rv.vid, err)
|
||||
failedCount++
|
||||
}
|
||||
}
|
||||
|
||||
if failedCount > 0 {
|
||||
return fmt.Errorf("%d of %d volume(s) failed to compact", failedCount, len(remoteVolumes))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findRemoteVolumeInTopology(topoInfo *master_pb.TopologyInfo, vid needle.VolumeId, collectionPattern string) (remoteVolumeInfo, bool, error) {
|
||||
// when collectionPattern is provided, compile and use as regex filter
|
||||
var matchesCollection func(string) bool
|
||||
if collectionPattern != "" {
|
||||
collectionRegex, err := compileCollectionPattern(collectionPattern)
|
||||
if err != nil {
|
||||
return remoteVolumeInfo{}, false, fmt.Errorf("invalid collection pattern '%s': %v", collectionPattern, err)
|
||||
}
|
||||
matchesCollection = collectionRegex.MatchString
|
||||
} else {
|
||||
matchesCollection = func(string) bool { return true }
|
||||
}
|
||||
|
||||
var result remoteVolumeInfo
|
||||
found := false
|
||||
eachDataNode(topoInfo, func(dc DataCenterId, rack RackId, dn *master_pb.DataNodeInfo) {
|
||||
if found {
|
||||
return
|
||||
}
|
||||
for _, diskInfo := range dn.DiskInfos {
|
||||
for _, v := range diskInfo.VolumeInfos {
|
||||
if needle.VolumeId(v.Id) == vid && v.RemoteStorageName != "" && v.RemoteStorageKey != "" {
|
||||
if !matchesCollection(v.Collection) {
|
||||
continue
|
||||
}
|
||||
result = remoteVolumeInfo{
|
||||
vid: vid,
|
||||
collection: v.Collection,
|
||||
remoteStorageName: v.RemoteStorageName,
|
||||
serverAddress: pb.NewServerAddressWithGrpcPort(dn.Id, int(dn.GrpcPort)),
|
||||
serverUrl: dn.Id,
|
||||
}
|
||||
found = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return result, found, nil
|
||||
}
|
||||
|
||||
func collectRemoteVolumesWithInfo(topoInfo *master_pb.TopologyInfo, collectionPattern string) ([]remoteVolumeInfo, error) {
|
||||
collectionRegex, err := compileCollectionPattern(collectionPattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid collection pattern '%s': %v", collectionPattern, err)
|
||||
}
|
||||
|
||||
seen := make(map[uint32]bool)
|
||||
var result []remoteVolumeInfo
|
||||
eachDataNode(topoInfo, func(dc DataCenterId, rack RackId, dn *master_pb.DataNodeInfo) {
|
||||
for _, diskInfo := range dn.DiskInfos {
|
||||
for _, v := range diskInfo.VolumeInfos {
|
||||
if v.RemoteStorageName == "" || v.RemoteStorageKey == "" {
|
||||
continue
|
||||
}
|
||||
if !collectionRegex.MatchString(v.Collection) {
|
||||
continue
|
||||
}
|
||||
if seen[v.Id] {
|
||||
continue
|
||||
}
|
||||
seen[v.Id] = true
|
||||
result = append(result, remoteVolumeInfo{
|
||||
vid: needle.VolumeId(v.Id),
|
||||
collection: v.Collection,
|
||||
remoteStorageName: v.RemoteStorageName,
|
||||
serverAddress: pb.NewServerAddressWithGrpcPort(dn.Id, int(dn.GrpcPort)),
|
||||
serverUrl: dn.Id,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func doVolumeTierCompact(commandEnv *CommandEnv, writer io.Writer, rv remoteVolumeInfo, garbageThreshold float64) error {
|
||||
grpcDialOption := commandEnv.option.GrpcDialOption
|
||||
|
||||
// step 1: check garbage level
|
||||
garbageRatio, err := checkVolumeGarbage(grpcDialOption, rv.vid, rv.serverAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check garbage for volume %d: %v", rv.vid, err)
|
||||
}
|
||||
|
||||
if garbageRatio < garbageThreshold {
|
||||
fmt.Fprintf(writer, "volume %d garbage ratio %.4f below threshold %.4f, skipping\n",
|
||||
rv.vid, garbageRatio, garbageThreshold)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "volume %d garbage ratio %.4f, starting compaction...\n", rv.vid, garbageRatio)
|
||||
|
||||
// step 2: download .dat from remote to local
|
||||
// this deletes the remote file and reloads the volume as local
|
||||
fmt.Fprintf(writer, " downloading volume %d from %s to local...\n", rv.vid, rv.remoteStorageName)
|
||||
err = downloadDatFromRemoteTier(grpcDialOption, writer, rv.vid, rv.collection, rv.serverAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download volume %d from remote: %v", rv.vid, err)
|
||||
}
|
||||
|
||||
// step 3: compact the local volume
|
||||
fmt.Fprintf(writer, " compacting volume %d...\n", rv.vid)
|
||||
err = compactVolumeOnServer(grpcDialOption, writer, rv.vid, rv.serverAddress)
|
||||
if err != nil {
|
||||
// compaction failed, but volume is now local without remote reference
|
||||
// upload the uncompacted volume back to restore cloud tier state
|
||||
fmt.Fprintf(writer, " compaction failed: %v\n", err)
|
||||
fmt.Fprintf(writer, " re-uploading volume %d to %s without compaction...\n", rv.vid, rv.remoteStorageName)
|
||||
uploadErr := uploadDatToRemoteTier(grpcDialOption, writer, rv.vid, rv.collection, rv.serverAddress, rv.remoteStorageName, false)
|
||||
if uploadErr != nil {
|
||||
return fmt.Errorf("compaction failed (%v) and re-upload also failed (%v), volume %d remains local",
|
||||
err, uploadErr, rv.vid)
|
||||
}
|
||||
return fmt.Errorf("compaction failed (%v), volume %d re-uploaded to %s without compaction",
|
||||
err, rv.vid, rv.remoteStorageName)
|
||||
}
|
||||
|
||||
// step 4: upload compacted volume back to remote
|
||||
fmt.Fprintf(writer, " uploading compacted volume %d to %s...\n", rv.vid, rv.remoteStorageName)
|
||||
err = uploadDatToRemoteTier(grpcDialOption, writer, rv.vid, rv.collection, rv.serverAddress, rv.remoteStorageName, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("upload compacted volume %d to %s: %v (volume remains local with compacted data)",
|
||||
rv.vid, rv.remoteStorageName, err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "volume %d compacted and uploaded to %s successfully\n", rv.vid, rv.remoteStorageName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkVolumeGarbage(grpcDialOption grpc.DialOption, vid needle.VolumeId, server pb.ServerAddress) (float64, error) {
|
||||
var garbageRatio float64
|
||||
err := operation.WithVolumeServerClient(false, server, grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
||||
resp, err := client.VacuumVolumeCheck(context.Background(), &volume_server_pb.VacuumVolumeCheckRequest{
|
||||
VolumeId: uint32(vid),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
garbageRatio = resp.GarbageRatio
|
||||
return nil
|
||||
})
|
||||
return garbageRatio, err
|
||||
}
|
||||
|
||||
func compactVolumeOnServer(grpcDialOption grpc.DialOption, writer io.Writer, vid needle.VolumeId, server pb.ServerAddress) error {
|
||||
// compact
|
||||
err := operation.WithVolumeServerClient(true, server, grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
||||
stream, err := client.VacuumVolumeCompact(context.Background(), &volume_server_pb.VacuumVolumeCompactRequest{
|
||||
VolumeId: uint32(vid),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
resp, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
if recvErr == io.EOF {
|
||||
break
|
||||
}
|
||||
return recvErr
|
||||
}
|
||||
fmt.Fprintf(writer, " compacted %d bytes\n", resp.ProcessedBytes)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if cleanupErr := vacuumVolumeCleanup(grpcDialOption, vid, server); cleanupErr != nil {
|
||||
fmt.Fprintf(writer, " cleanup after compaction failure also failed: %v\n", cleanupErr)
|
||||
}
|
||||
return fmt.Errorf("compact: %v", err)
|
||||
}
|
||||
|
||||
// commit
|
||||
err = operation.WithVolumeServerClient(false, server, grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
||||
_, err := client.VacuumVolumeCommit(context.Background(), &volume_server_pb.VacuumVolumeCommitRequest{
|
||||
VolumeId: uint32(vid),
|
||||
})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
if cleanupErr := vacuumVolumeCleanup(grpcDialOption, vid, server); cleanupErr != nil {
|
||||
fmt.Fprintf(writer, " cleanup after commit failure also failed: %v\n", cleanupErr)
|
||||
}
|
||||
return fmt.Errorf("commit: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func vacuumVolumeCleanup(grpcDialOption grpc.DialOption, vid needle.VolumeId, server pb.ServerAddress) error {
|
||||
return operation.WithVolumeServerClient(false, server, grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
||||
_, err := client.VacuumVolumeCleanup(context.Background(), &volume_server_pb.VacuumVolumeCleanupRequest{
|
||||
VolumeId: uint32(vid),
|
||||
})
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func buildTestTopologyWithRemoteVolumes() *master_pb.TopologyInfo {
|
||||
return &master_pb.TopologyInfo{
|
||||
Id: "topo",
|
||||
DataCenterInfos: []*master_pb.DataCenterInfo{
|
||||
{
|
||||
Id: "dc1",
|
||||
RackInfos: []*master_pb.RackInfo{
|
||||
{
|
||||
Id: "rack1",
|
||||
DataNodeInfos: []*master_pb.DataNodeInfo{
|
||||
{
|
||||
Id: "server1:8080",
|
||||
GrpcPort: 18080,
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||||
"": {
|
||||
VolumeInfos: []*master_pb.VolumeInformationMessage{
|
||||
{
|
||||
Id: 1,
|
||||
Collection: "col1",
|
||||
RemoteStorageName: "s3.default",
|
||||
RemoteStorageKey: "col1/1.dat",
|
||||
DeleteCount: 100,
|
||||
DeletedByteCount: 1000,
|
||||
Size: 5000,
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Collection: "col1",
|
||||
RemoteStorageName: "",
|
||||
RemoteStorageKey: "",
|
||||
Size: 3000,
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Collection: "col2",
|
||||
RemoteStorageName: "s3.archive",
|
||||
RemoteStorageKey: "col2/3.dat",
|
||||
DeleteCount: 50,
|
||||
DeletedByteCount: 500,
|
||||
Size: 2000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "dc2",
|
||||
RackInfos: []*master_pb.RackInfo{
|
||||
{
|
||||
Id: "rack1",
|
||||
DataNodeInfos: []*master_pb.DataNodeInfo{
|
||||
{
|
||||
Id: "server2:8080",
|
||||
GrpcPort: 18080,
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||||
"": {
|
||||
VolumeInfos: []*master_pb.VolumeInformationMessage{
|
||||
{
|
||||
Id: 4,
|
||||
Collection: "bucket1",
|
||||
RemoteStorageName: "s3.default",
|
||||
RemoteStorageKey: "bucket1/4.dat",
|
||||
Size: 8000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindRemoteVolumeInTopology(t *testing.T) {
|
||||
topo := buildTestTopologyWithRemoteVolumes()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vid needle.VolumeId
|
||||
collection string
|
||||
wantFound bool
|
||||
wantDest string
|
||||
}{
|
||||
{
|
||||
name: "find existing remote volume",
|
||||
vid: 1,
|
||||
wantFound: true,
|
||||
wantDest: "s3.default",
|
||||
},
|
||||
{
|
||||
name: "find remote volume with different backend",
|
||||
vid: 3,
|
||||
wantFound: true,
|
||||
wantDest: "s3.archive",
|
||||
},
|
||||
{
|
||||
name: "find remote volume on another server",
|
||||
vid: 4,
|
||||
wantFound: true,
|
||||
wantDest: "s3.default",
|
||||
},
|
||||
{
|
||||
name: "local volume not found as remote",
|
||||
vid: 2,
|
||||
wantFound: false,
|
||||
},
|
||||
{
|
||||
name: "non-existent volume not found",
|
||||
vid: 999,
|
||||
wantFound: false,
|
||||
},
|
||||
{
|
||||
name: "filter by matching collection exact",
|
||||
vid: 1,
|
||||
collection: "^col1$",
|
||||
wantFound: true,
|
||||
wantDest: "s3.default",
|
||||
},
|
||||
{
|
||||
name: "filter by matching collection regex",
|
||||
vid: 1,
|
||||
collection: "col.*",
|
||||
wantFound: true,
|
||||
wantDest: "s3.default",
|
||||
},
|
||||
{
|
||||
name: "filter by non-matching collection",
|
||||
vid: 1,
|
||||
collection: "^col2$",
|
||||
wantFound: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rv, found, err := findRemoteVolumeInTopology(topo, tt.vid, tt.collection)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantFound, found)
|
||||
if found {
|
||||
assert.Equal(t, tt.vid, rv.vid)
|
||||
assert.Equal(t, tt.wantDest, rv.remoteStorageName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindRemoteVolumeInTopologyInvalidPattern(t *testing.T) {
|
||||
topo := buildTestTopologyWithRemoteVolumes()
|
||||
|
||||
_, _, err := findRemoteVolumeInTopology(topo, 1, "[invalid")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCollectRemoteVolumesWithInfo(t *testing.T) {
|
||||
topo := buildTestTopologyWithRemoteVolumes()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
collectionPattern string
|
||||
wantCount int
|
||||
wantVids []needle.VolumeId
|
||||
}{
|
||||
{
|
||||
name: "empty pattern matches empty collection only",
|
||||
collectionPattern: "",
|
||||
wantCount: 0,
|
||||
},
|
||||
{
|
||||
name: "match all collections",
|
||||
collectionPattern: ".*",
|
||||
wantCount: 3, // volumes 1, 3, 4
|
||||
wantVids: []needle.VolumeId{1, 3, 4},
|
||||
},
|
||||
{
|
||||
name: "match specific collection",
|
||||
collectionPattern: "^col1$",
|
||||
wantCount: 1,
|
||||
wantVids: []needle.VolumeId{1},
|
||||
},
|
||||
{
|
||||
name: "match collection prefix",
|
||||
collectionPattern: "col.*",
|
||||
wantCount: 2, // volumes 1, 3
|
||||
wantVids: []needle.VolumeId{1, 3},
|
||||
},
|
||||
{
|
||||
name: "match bucket collection",
|
||||
collectionPattern: "bucket1",
|
||||
wantCount: 1,
|
||||
wantVids: []needle.VolumeId{4},
|
||||
},
|
||||
{
|
||||
name: "no match",
|
||||
collectionPattern: "^nonexistent$",
|
||||
wantCount: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := collectRemoteVolumesWithInfo(topo, tt.collectionPattern)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantCount, len(result))
|
||||
|
||||
if tt.wantVids != nil {
|
||||
gotVids := make(map[needle.VolumeId]bool)
|
||||
for _, rv := range result {
|
||||
gotVids[rv.vid] = true
|
||||
}
|
||||
for _, vid := range tt.wantVids {
|
||||
assert.True(t, gotVids[vid], "expected volume %d in results", vid)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectRemoteVolumesWithInfoCaptures(t *testing.T) {
|
||||
topo := buildTestTopologyWithRemoteVolumes()
|
||||
|
||||
result, err := collectRemoteVolumesWithInfo(topo, "^col1$")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(result))
|
||||
|
||||
rv := result[0]
|
||||
assert.Equal(t, needle.VolumeId(1), rv.vid)
|
||||
assert.Equal(t, "col1", rv.collection)
|
||||
assert.Equal(t, "s3.default", rv.remoteStorageName)
|
||||
assert.Equal(t, "server1:8080", rv.serverUrl)
|
||||
assert.NotEmpty(t, rv.serverAddress)
|
||||
}
|
||||
|
||||
func TestCollectRemoteVolumesWithInfoInvalidPattern(t *testing.T) {
|
||||
topo := buildTestTopologyWithRemoteVolumes()
|
||||
|
||||
_, err := collectRemoteVolumesWithInfo(topo, "[invalid")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
+14
-2
@@ -52,6 +52,18 @@ func (lt *LockTable[T]) NewActiveLock(intention string, lockType LockType) *Acti
|
||||
return l
|
||||
}
|
||||
|
||||
func isNotFirstWaiter(lock *ActiveLock, entry *LockEntry) bool {
|
||||
return len(entry.waiters) > 0 && lock.ID != entry.waiters[0].ID
|
||||
}
|
||||
|
||||
func shouldWaitForExclusiveLock(lock *ActiveLock, entry *LockEntry) bool {
|
||||
return !lock.isDeleted && (isNotFirstWaiter(lock, entry) || entry.activeExclusiveLockOwnerCount > 0 || entry.activeSharedLockOwnerCount > 0)
|
||||
}
|
||||
|
||||
func shouldWaitForSharedLock(lock *ActiveLock, entry *LockEntry) bool {
|
||||
return !lock.isDeleted && (isNotFirstWaiter(lock, entry) || entry.activeExclusiveLockOwnerCount > 0)
|
||||
}
|
||||
|
||||
func (lt *LockTable[T]) AcquireLock(intention string, key T, lockType LockType) (lock *ActiveLock) {
|
||||
lt.mu.Lock()
|
||||
// Get or create the lock entry for the key
|
||||
@@ -81,11 +93,11 @@ func (lt *LockTable[T]) AcquireLock(intention string, key T, lockType LockType)
|
||||
}
|
||||
entry.waiters = append(entry.waiters, lock)
|
||||
if lockType == ExclusiveLock {
|
||||
for !lock.isDeleted && ((len(entry.waiters) > 0 && lock.ID != entry.waiters[0].ID) || entry.activeExclusiveLockOwnerCount > 0 || entry.activeSharedLockOwnerCount > 0) {
|
||||
for shouldWaitForExclusiveLock(lock, entry) {
|
||||
entry.cond.Wait()
|
||||
}
|
||||
} else {
|
||||
for !lock.isDeleted && ((len(entry.waiters) > 0 && lock.ID != entry.waiters[0].ID) || entry.activeExclusiveLockOwnerCount > 0) {
|
||||
for shouldWaitForSharedLock(lock, entry) {
|
||||
entry.cond.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,3 +40,47 @@ func TestOrderedLock(t *testing.T) {
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestShouldWaitForSharedLock(t *testing.T) {
|
||||
lock := &ActiveLock{ID: 2, lockType: SharedLock}
|
||||
entry := &LockEntry{
|
||||
waiters: []*ActiveLock{{ID: 1}, lock},
|
||||
}
|
||||
|
||||
if !shouldWaitForSharedLock(lock, entry) {
|
||||
t.Fatal("shared lock should wait behind earlier waiters")
|
||||
}
|
||||
|
||||
entry.waiters = []*ActiveLock{lock}
|
||||
entry.activeExclusiveLockOwnerCount = 1
|
||||
if !shouldWaitForSharedLock(lock, entry) {
|
||||
t.Fatal("shared lock should wait while an exclusive owner is active")
|
||||
}
|
||||
|
||||
lock.isDeleted = true
|
||||
if shouldWaitForSharedLock(lock, entry) {
|
||||
t.Fatal("deleted shared lock should stop waiting even if an exclusive owner is active")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldWaitForExclusiveLock(t *testing.T) {
|
||||
lock := &ActiveLock{ID: 2, lockType: ExclusiveLock}
|
||||
entry := &LockEntry{
|
||||
waiters: []*ActiveLock{{ID: 1}, lock},
|
||||
}
|
||||
|
||||
if !shouldWaitForExclusiveLock(lock, entry) {
|
||||
t.Fatal("exclusive lock should wait behind earlier waiters")
|
||||
}
|
||||
|
||||
entry.waiters = []*ActiveLock{lock}
|
||||
entry.activeSharedLockOwnerCount = 1
|
||||
if !shouldWaitForExclusiveLock(lock, entry) {
|
||||
t.Fatal("exclusive lock should wait while shared owners are active")
|
||||
}
|
||||
|
||||
lock.isDeleted = true
|
||||
if shouldWaitForExclusiveLock(lock, entry) {
|
||||
t.Fatal("deleted exclusive lock should stop waiting even if shared owners are active")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func (l *ExclusiveLocker) RequestLock(clientName string) {
|
||||
}
|
||||
return err
|
||||
}); err != nil {
|
||||
println("lock:", err.Error())
|
||||
glog.V(2).Infof("Failed to acquire lock %s: %v", l.lockName, err)
|
||||
time.Sleep(InitLockInterval)
|
||||
} else {
|
||||
break
|
||||
@@ -79,6 +79,7 @@ func (l *ExclusiveLocker) RequestLock(clientName string) {
|
||||
|
||||
l.isLocked.Store(true)
|
||||
l.clientName = clientName
|
||||
glog.V(1).Infof("Acquired lock %s", l.lockName)
|
||||
|
||||
// Each lock has and only has one goroutine
|
||||
if l.renewGoroutineRunning.CompareAndSwap(false, true) {
|
||||
@@ -100,11 +101,11 @@ func (l *ExclusiveLocker) RequestLock(clientName string) {
|
||||
if err == nil {
|
||||
atomic.StoreInt64(&l.token, resp.Token)
|
||||
atomic.StoreInt64(&l.lockTsNs, resp.LockTsNs)
|
||||
// println("ts", l.lockTsNs, "token", l.token)
|
||||
glog.V(2).Infof("Renewed lock %s: ts %d token %d", l.lockName, l.lockTsNs, l.token)
|
||||
}
|
||||
return err
|
||||
}); err != nil {
|
||||
glog.Errorf("failed to renew lock: %v", err)
|
||||
glog.Warningf("Failed to renew lock %s: %v", l.lockName, err)
|
||||
l.isLocked.Store(false)
|
||||
return
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user