mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-06 22:55:51 +00:00
Compare commits
195
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6335c5e94 | ||
|
|
9d3907e36c | ||
|
|
bd953b0f84 | ||
|
|
7944cb4ba2 | ||
|
|
15d3c65e0c | ||
|
|
fea14c01a7 | ||
|
|
f2498e122a | ||
|
|
62481f1673 | ||
|
|
b8f074b7d3 | ||
|
|
cc1ec48151 | ||
|
|
5ece8dd63c | ||
|
|
67f7fad987 | ||
|
|
2b057eccbf | ||
|
|
8fdcf69eb0 | ||
|
|
988fc4f7ba | ||
|
|
11e8c4c288 | ||
|
|
d3cd061c22 | ||
|
|
895d49b55b | ||
|
|
95e0b74fb6 | ||
|
|
0978e7f833 | ||
|
|
38ce95d960 | ||
|
|
9a454bbe2b | ||
|
|
757917f564 | ||
|
|
a033ab76e5 | ||
|
|
5fcf12c080 | ||
|
|
5da137233d | ||
|
|
62d4f9152a | ||
|
|
a901c1a5e2 | ||
|
|
67b19b2db5 | ||
|
|
c54f1fe69c | ||
|
|
818ff69270 | ||
|
|
db89ed198b | ||
|
|
f7b5ca826b | ||
|
|
878d23b95a | ||
|
|
1e0b243aed | ||
|
|
79e2abdfbf | ||
|
|
530be3e373 | ||
|
|
9b3b12c607 | ||
|
|
3abdef3202 | ||
|
|
43fd5b8d82 | ||
|
|
150a69fe11 | ||
|
|
4fec65d949 | ||
|
|
f564918685 | ||
|
|
f9289f0570 | ||
|
|
4303b3aa4c | ||
|
|
a0ee7ba314 | ||
|
|
a976b21010 | ||
|
|
e57f8c4d87 | ||
|
|
67691a1eea | ||
|
|
02353444ac | ||
|
|
68944e83a3 | ||
|
|
00310f6588 | ||
|
|
5c9c424a84 | ||
|
|
5218e68554 | ||
|
|
00cffa028c | ||
|
|
a261f90e18 | ||
|
|
be29f44d87 | ||
|
|
2864bc0fe8 | ||
|
|
ab95d58b7c | ||
|
|
2f641a63d6 | ||
|
|
80fd3635d2 | ||
|
|
7129e1178e | ||
|
|
3c1e8ca7a8 | ||
|
|
0f3ba98e11 | ||
|
|
80a26020d7 | ||
|
|
5389f61cef | ||
|
|
8ad2f29e3e | ||
|
|
c58bd0dfd3 | ||
|
|
f7680cf812 | ||
|
|
4299fdf578 | ||
|
|
3e9fc9e75b | ||
|
|
afce0a3dd3 | ||
|
|
83d44be0f3 | ||
|
|
975cec9228 | ||
|
|
f31a026b2a | ||
|
|
4914c14982 | ||
|
|
2f6c237238 | ||
|
|
df4995b894 | ||
|
|
b750853c42 | ||
|
|
317e756b9a | ||
|
|
5b79f51e3c | ||
|
|
635f69a821 | ||
|
|
11791fad6a | ||
|
|
56d2f05ccd | ||
|
|
8c1be63c92 | ||
|
|
bb9942c646 | ||
|
|
f0afcf904d | ||
|
|
c1ccbcda13 | ||
|
|
94a68fa9b9 | ||
|
|
b3a8701989 | ||
|
|
b9ad62fc16 | ||
|
|
d848b8ed00 | ||
|
|
196c71b613 | ||
|
|
559ec33498 | ||
|
|
d6397fdc50 | ||
|
|
264030c08c | ||
|
|
8099e71934 | ||
|
|
63eb67c9da | ||
|
|
93efc64af6 | ||
|
|
f2b08e47e5 | ||
|
|
5079d926e4 | ||
|
|
a9e1b57dcd | ||
|
|
e2608edda4 | ||
|
|
0f2ecb766f | ||
|
|
6848cdf9e1 | ||
|
|
ca62d4297b | ||
|
|
4bb40732bb | ||
|
|
8ff2e0777e | ||
|
|
ac876eef21 | ||
|
|
ddf009ffe7 | ||
|
|
818f3bb71b | ||
|
|
7643f4f541 | ||
|
|
f1ed270942 | ||
|
|
7dbbdac030 | ||
|
|
6d676eda67 | ||
|
|
d002481037 | ||
|
|
1df8c05bc3 | ||
|
|
44ba070d83 | ||
|
|
062238bb5c | ||
|
|
b77c42ff32 | ||
|
|
26fc90187e | ||
|
|
ef463fe1af | ||
|
|
17ad5a1419 | ||
|
|
b03419ee92 | ||
|
|
f849b7c823 | ||
|
|
f15b980976 | ||
|
|
110b485bae | ||
|
|
06dda12e4b | ||
|
|
a93a1ab2eb | ||
|
|
cd1e738422 | ||
|
|
3dec359d6b | ||
|
|
f6a3286b32 | ||
|
|
01bb3b3053 | ||
|
|
5769057af3 | ||
|
|
4160b92864 | ||
|
|
37bf1cd91d | ||
|
|
a6d72bc272 | ||
|
|
c72eda50a8 | ||
|
|
87ee3b63a2 | ||
|
|
0ca1c19821 | ||
|
|
f40687b34e | ||
|
|
15520f601f | ||
|
|
bdc37a1e86 | ||
|
|
2d2619f0b4 | ||
|
|
ce1e0dc30a | ||
|
|
d4e11a471d | ||
|
|
08d5daf0c1 | ||
|
|
8d34433308 | ||
|
|
4fd67001d9 | ||
|
|
c6a3280595 | ||
|
|
799c495226 | ||
|
|
4ec564469a | ||
|
|
66f1754896 | ||
|
|
994e1f7d64 | ||
|
|
74eeac6b66 | ||
|
|
0eb638f503 | ||
|
|
1a285c1334 | ||
|
|
caf3d157e6 | ||
|
|
3ebc05930d | ||
|
|
0c7beec697 | ||
|
|
a859f0a019 | ||
|
|
def25ca84d | ||
|
|
701e397337 | ||
|
|
4fc9ada2ec | ||
|
|
a73ba3adbb | ||
|
|
71f8128d75 | ||
|
|
01545fc4ff | ||
|
|
1f037e48f9 | ||
|
|
563c729e70 | ||
|
|
55367afded | ||
|
|
a9ecfeef45 | ||
|
|
beaf96a51d | ||
|
|
93d4a6aefd | ||
|
|
166af06a2b | ||
|
|
517f60e875 | ||
|
|
49a680dd64 | ||
|
|
87332eb60b | ||
|
|
e735c12869 | ||
|
|
e4ca0d09e7 | ||
|
|
01433e801d | ||
|
|
1f61097d4d | ||
|
|
0e82b4e351 | ||
|
|
0bd048b76f | ||
|
|
c997e54096 | ||
|
|
2aa6af033d | ||
|
|
02749c1192 | ||
|
|
ac03d3fd78 | ||
|
|
adaf3534fa | ||
|
|
49f20489e4 | ||
|
|
bdec508da9 | ||
|
|
fd33c07843 | ||
|
|
cf38c01978 | ||
|
|
f4bad510c9 | ||
|
|
15d9f6c6fe | ||
|
|
ea179963c0 |
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4.37.9
|
||||
uses: github/codeql-action/init@v4.38.2
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
with:
|
||||
languages: go
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below).
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4.37.9
|
||||
uses: github/codeql-action/autobuild@v4.38.2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -49,4 +49,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4.37.9
|
||||
uses: github/codeql-action/analyze@v4.38.2
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
paths:
|
||||
- 'weed/**'
|
||||
- 'seaweed-volume/**'
|
||||
- 'seaweed-common/**'
|
||||
- 'seaweed-worker/**'
|
||||
- 'docker/**'
|
||||
- 'go.mod'
|
||||
@@ -152,7 +153,7 @@ jobs:
|
||||
org.opencontainers.image.vendor=Chris Lu
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4.3.0
|
||||
uses: docker/setup-qemu-action@v4.4.0
|
||||
|
||||
- name: Create BuildKit config
|
||||
run: |
|
||||
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
echo "seaweedfs_ref=$seaweed" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4.3.0
|
||||
uses: docker/setup-qemu-action@v4.4.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [amd64, arm64, arm, 386]
|
||||
platform: [amd64, arm64, arm, 386, ppc64le, s390x]
|
||||
variant: ${{ fromJSON(needs.setup.outputs.variants) }}
|
||||
|
||||
steps:
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
org.opencontainers.image.vendor=Chris Lu
|
||||
- name: Set up QEMU
|
||||
if: matrix.platform != 'amd64'
|
||||
uses: docker/setup-qemu-action@v4.3.0
|
||||
uses: docker/setup-qemu-action@v4.4.0
|
||||
- name: Create BuildKit config
|
||||
run: |
|
||||
cat > /tmp/buildkitd.toml <<EOF
|
||||
@@ -405,7 +405,7 @@ jobs:
|
||||
output: trivy-results.sarif
|
||||
exit-code: '0'
|
||||
- name: Upload Trivy scan results to GitHub Security
|
||||
uses: github/codeql-action/upload-sarif@v4.37.9
|
||||
uses: github/codeql-action/upload-sarif@v4.38.2
|
||||
if: always()
|
||||
with:
|
||||
sarif_file: trivy-results.sarif
|
||||
@@ -505,7 +505,9 @@ jobs:
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-amd64 \
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-arm64 \
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-arm \
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-386
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-386 \
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-ppc64le \
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-s390x
|
||||
# The copy and the signature below use the digest this run pushed, not whatever the tag points at by then.
|
||||
DIGEST=$(jq -er '."containerimage.descriptor".digest' /tmp/manifest.json)
|
||||
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
|
||||
@@ -549,13 +551,15 @@ jobs:
|
||||
echo "Using skopeo to copy..."
|
||||
retry_with_backoff skopeo copy --all docker://ghcr.io/chrislusf/seaweedfs@${DIGEST} docker://chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}
|
||||
else
|
||||
echo "Using docker buildx imagetools (pulling 4 images from Docker Hub)..."
|
||||
echo "Using docker buildx imagetools (pulling 6 images from Docker Hub)..."
|
||||
# Fallback: create manifest directly on Docker Hub (pulls from Docker Hub - rate limited)
|
||||
retry_with_backoff docker buildx imagetools create -t chrislusf/seaweedfs:${BASE_TAG}${SUFFIX} \
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-amd64 \
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-arm64 \
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-arm \
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-386
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-386 \
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-ppc64le \
|
||||
ghcr.io/chrislusf/seaweedfs:${BASE_TAG}${SUFFIX}-s390x
|
||||
fi
|
||||
|
||||
- name: Sign
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
org.opencontainers.image.vendor=Chris Lu
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4.3.0
|
||||
uses: docker/setup-qemu-action@v4.4.0
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
@@ -149,12 +149,16 @@ jobs:
|
||||
- { variant: normal, tag_suffix: "", dockerfile: ./docker/Dockerfile.go_build, build_args: "", rust_variant: normal, platform: linux/arm64, arch: arm64, runner: ubuntu-24.04-arm, qemu: false }
|
||||
- { variant: normal, tag_suffix: "", dockerfile: ./docker/Dockerfile.go_build, build_args: "", rust_variant: normal, platform: linux/arm/v7, arch: armv7, runner: ubuntu-latest, qemu: true }
|
||||
- { variant: normal, tag_suffix: "", dockerfile: ./docker/Dockerfile.go_build, build_args: "", rust_variant: normal, platform: linux/386, arch: i386, runner: ubuntu-latest, qemu: false }
|
||||
- { variant: normal, tag_suffix: "", dockerfile: ./docker/Dockerfile.go_build, build_args: "", rust_variant: normal, platform: linux/ppc64le, arch: ppc64le, runner: ubuntu-latest, qemu: true }
|
||||
- { variant: normal, tag_suffix: "", dockerfile: ./docker/Dockerfile.go_build, build_args: "", rust_variant: normal, platform: linux/s390x, arch: s390x, runner: ubuntu-latest, qemu: true }
|
||||
|
||||
# Large disk - multi-arch
|
||||
- { variant: large_disk, tag_suffix: _large_disk, dockerfile: ./docker/Dockerfile.go_build, build_args: TAGS=5BytesOffset, rust_variant: large-disk, platform: linux/amd64, arch: amd64, runner: ubuntu-latest, qemu: false }
|
||||
- { variant: large_disk, tag_suffix: _large_disk, dockerfile: ./docker/Dockerfile.go_build, build_args: TAGS=5BytesOffset, rust_variant: large-disk, platform: linux/arm64, arch: arm64, runner: ubuntu-24.04-arm, qemu: false }
|
||||
- { variant: large_disk, tag_suffix: _large_disk, dockerfile: ./docker/Dockerfile.go_build, build_args: TAGS=5BytesOffset, rust_variant: large-disk, platform: linux/arm/v7, arch: armv7, runner: ubuntu-latest, qemu: true }
|
||||
- { variant: large_disk, tag_suffix: _large_disk, dockerfile: ./docker/Dockerfile.go_build, build_args: TAGS=5BytesOffset, rust_variant: large-disk, platform: linux/386, arch: i386, runner: ubuntu-latest, qemu: false }
|
||||
- { variant: large_disk, tag_suffix: _large_disk, dockerfile: ./docker/Dockerfile.go_build, build_args: TAGS=5BytesOffset, rust_variant: large-disk, platform: linux/ppc64le, arch: ppc64le, runner: ubuntu-latest, qemu: true }
|
||||
- { variant: large_disk, tag_suffix: _large_disk, dockerfile: ./docker/Dockerfile.go_build, build_args: TAGS=5BytesOffset, rust_variant: large-disk, platform: linux/s390x, arch: s390x, runner: ubuntu-latest, qemu: true }
|
||||
|
||||
# Full tags - multi-arch
|
||||
- { variant: full, tag_suffix: _full, dockerfile: ./docker/Dockerfile.go_build, build_args: "TAGS=elastic,gocdk,rclone,sqlite,tarantool,tikv,ydb", rust_variant: normal, platform: linux/amd64, arch: amd64, runner: ubuntu-latest, qemu: false }
|
||||
@@ -231,7 +235,7 @@ jobs:
|
||||
|
||||
- name: Set up QEMU
|
||||
if: (github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant) && matrix.qemu
|
||||
uses: docker/setup-qemu-action@v4.3.0
|
||||
uses: docker/setup-qemu-action@v4.4.0
|
||||
|
||||
- name: Create BuildKit config
|
||||
if: github.event_name != 'workflow_dispatch' || github.event.inputs.variant == 'all' || github.event.inputs.variant == matrix.variant
|
||||
@@ -456,7 +460,7 @@ jobs:
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security
|
||||
if: always()
|
||||
uses: github/codeql-action/upload-sarif@v4.37.9
|
||||
uses: github/codeql-action/upload-sarif@v4.38.2
|
||||
with:
|
||||
sarif_file: trivy-results.sarif
|
||||
category: trivy-${{ matrix.variant }}
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
echo "seaweedfs_ref=$seaweed" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v1
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v1
|
||||
|
||||
@@ -35,11 +35,48 @@ jobs:
|
||||
cd telemetry/server
|
||||
go mod tidy
|
||||
echo "Building telemetry server..."
|
||||
GOOS=linux GOARCH=amd64 go build -o ../../telemetry-server .
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ../../telemetry-server .
|
||||
cd ../..
|
||||
ls -la telemetry-server
|
||||
echo "Build completed successfully"
|
||||
|
||||
- name: Generate Service Configuration
|
||||
if: github.event_name == 'workflow_dispatch' && (inputs.setup || inputs.deploy)
|
||||
env:
|
||||
REMOTE_USER: ${{ secrets.TELEMETRY_USER }}
|
||||
run: |
|
||||
# Create systemd service file
|
||||
echo "
|
||||
[Unit]
|
||||
Description=SeaweedFS Telemetry Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$REMOTE_USER
|
||||
WorkingDirectory=/home/$REMOTE_USER/seaweedfs-telemetry
|
||||
ExecStart=/bin/sh -c 'exec /home/$REMOTE_USER/seaweedfs-telemetry/bin/telemetry-server -port=8353 >>/home/$REMOTE_USER/seaweedfs-telemetry/logs/telemetry.log 2>>/home/$REMOTE_USER/seaweedfs-telemetry/logs/telemetry.error.log'
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target" > telemetry.service
|
||||
|
||||
# Setup logrotate configuration
|
||||
echo "# SeaweedFS Telemetry service log rotation
|
||||
/home/$REMOTE_USER/seaweedfs-telemetry/logs/*.log {
|
||||
daily
|
||||
rotate 30
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
create 644 $REMOTE_USER $REMOTE_USER
|
||||
postrotate
|
||||
systemctl restart telemetry.service
|
||||
endscript
|
||||
}" > telemetry_logrotate
|
||||
|
||||
- name: First-time Server Setup
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.setup
|
||||
env:
|
||||
@@ -61,40 +98,6 @@ jobs:
|
||||
touch ~/seaweedfs-telemetry/logs/telemetry.log ~/seaweedfs-telemetry/logs/telemetry.error.log && \
|
||||
chmod 644 ~/seaweedfs-telemetry/logs/*.log"
|
||||
|
||||
# Create systemd service file
|
||||
echo "
|
||||
[Unit]
|
||||
Description=SeaweedFS Telemetry Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$REMOTE_USER
|
||||
WorkingDirectory=/home/$REMOTE_USER/seaweedfs-telemetry
|
||||
ExecStart=/home/$REMOTE_USER/seaweedfs-telemetry/bin/telemetry-server -port=8353
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=append:/home/$REMOTE_USER/seaweedfs-telemetry/logs/telemetry.log
|
||||
StandardError=append:/home/$REMOTE_USER/seaweedfs-telemetry/logs/telemetry.error.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target" > telemetry.service
|
||||
|
||||
# Setup logrotate configuration
|
||||
echo "# SeaweedFS Telemetry service log rotation
|
||||
/home/$REMOTE_USER/seaweedfs-telemetry/logs/*.log {
|
||||
daily
|
||||
rotate 30
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
create 644 $REMOTE_USER $REMOTE_USER
|
||||
postrotate
|
||||
systemctl restart telemetry.service
|
||||
endscript
|
||||
}" > telemetry_logrotate
|
||||
|
||||
# Copy configuration files
|
||||
scp -i ~/.ssh/deploy_key telemetry/grafana-dashboard.json $REMOTE_USER@$REMOTE_HOST:~/seaweedfs-telemetry/
|
||||
scp -i ~/.ssh/deploy_key telemetry/prometheus.yml $REMOTE_USER@$REMOTE_HOST:~/seaweedfs-telemetry/
|
||||
@@ -137,11 +140,18 @@ jobs:
|
||||
scp -i ~/.ssh/deploy_key telemetry/grafana-dashboard.json $REMOTE_USER@$REMOTE_HOST:~/seaweedfs-telemetry/
|
||||
scp -i ~/.ssh/deploy_key telemetry/prometheus.yml $REMOTE_USER@$REMOTE_HOST:~/seaweedfs-telemetry/
|
||||
|
||||
# Copy updated service and logrotate files
|
||||
scp -i ~/.ssh/deploy_key telemetry.service telemetry_logrotate $REMOTE_USER@$REMOTE_HOST:~/seaweedfs-telemetry/
|
||||
|
||||
# Check if service exists and deploy accordingly
|
||||
ssh -i ~/.ssh/deploy_key $REMOTE_USER@$REMOTE_HOST "
|
||||
if systemctl list-unit-files telemetry.service >/dev/null 2>&1; then
|
||||
echo 'Service exists, performing update...'
|
||||
set -e
|
||||
sudo systemctl stop telemetry.service
|
||||
sudo mv ~/seaweedfs-telemetry/telemetry.service /etc/systemd/system/
|
||||
sudo mv ~/seaweedfs-telemetry/telemetry_logrotate /etc/logrotate.d/seaweedfs-telemetry
|
||||
sudo systemctl daemon-reload
|
||||
mkdir -p ~/seaweedfs-telemetry/bin
|
||||
mv ~/seaweedfs-telemetry/tmp/telemetry-server ~/seaweedfs-telemetry/bin/
|
||||
chmod +x ~/seaweedfs-telemetry/bin/telemetry-server
|
||||
|
||||
@@ -116,6 +116,20 @@ jobs:
|
||||
grep -q "security-config" /tmp/security.yaml
|
||||
echo "Security configuration renders correctly"
|
||||
|
||||
echo ""
|
||||
echo "=== Testing admin.allowInsecureBind satisfies the admin auth render guard ==="
|
||||
helm template test $CHART_DIR --set admin.enabled=true --set admin.allowInsecureBind=true \
|
||||
> /tmp/admin-allow-insecure-bind.yaml
|
||||
grep -q -- "-allowInsecureBind" /tmp/admin-allow-insecure-bind.yaml
|
||||
echo "admin.allowInsecureBind renders -allowInsecureBind and passes the render guard"
|
||||
|
||||
if helm template test $CHART_DIR --set admin.enabled=true > /tmp/admin-no-auth.yaml 2>/tmp/admin-no-auth.err; then
|
||||
echo "FAIL: admin.enabled=true with no auth configured should fail to render"
|
||||
exit 1
|
||||
fi
|
||||
grep -q "admin.allowInsecureBind" /tmp/admin-no-auth.err
|
||||
echo "admin with no auth configured still fails the render guard, and the guard mentions admin.allowInsecureBind"
|
||||
|
||||
echo ""
|
||||
echo "=== Testing JWT expiration overrides ==="
|
||||
helm template test $CHART_DIR \
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'seaweed-volume/**'
|
||||
- 'seaweed-common/**'
|
||||
- 'test/perf/**'
|
||||
- '.github/workflows/performance.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'seaweed-volume/**'
|
||||
- 'seaweed-common/**'
|
||||
- 'test/volume_server/**'
|
||||
- 'weed/pb/volume_server.proto'
|
||||
- 'weed/pb/volume_server_pb/**'
|
||||
@@ -13,6 +14,7 @@ on:
|
||||
branches: [ master, main ]
|
||||
paths:
|
||||
- 'seaweed-volume/**'
|
||||
- 'seaweed-common/**'
|
||||
- 'test/volume_server/**'
|
||||
- 'weed/pb/volume_server.proto'
|
||||
- 'weed/pb/volume_server_pb/**'
|
||||
@@ -27,6 +29,29 @@ permissions:
|
||||
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changed paths
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
rust: ${{ steps.filter.outputs.rust }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Filter changed paths
|
||||
id: filter
|
||||
uses: dorny/paths-filter@v4
|
||||
with:
|
||||
filters: |
|
||||
rust:
|
||||
- 'seaweed-volume/**'
|
||||
- '.github/workflows/rust-volume-server-tests.yml'
|
||||
|
||||
rust-unit-tests:
|
||||
name: Rust Unit Tests
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -52,13 +77,73 @@ jobs:
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
seaweed-volume/target
|
||||
key: rust-${{ steps.toolchain.outputs.fingerprint }}-${{ hashFiles('seaweed-volume/Cargo.lock') }}
|
||||
key: rust-${{ steps.toolchain.outputs.fingerprint }}-${{ hashFiles('seaweed-volume/Cargo.lock', 'seaweed-common/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
rust-${{ steps.toolchain.outputs.fingerprint }}-
|
||||
|
||||
- name: Build Rust volume server
|
||||
run: cd seaweed-volume && cargo build --release
|
||||
|
||||
# The crate is warning-free under clippy as of the sweep that added
|
||||
# this step. Uncomment to make that a gate; `[lints.clippy]` in
|
||||
# seaweed-volume/Cargo.toml is where crate-wide exceptions live.
|
||||
# - name: Clippy
|
||||
# run: cd seaweed-volume && cargo clippy --all-targets -- -D warnings
|
||||
|
||||
# The crate is rustfmt-clean as of the PR that added this step.
|
||||
# Uncomment to keep it that way.
|
||||
# - name: Check formatting
|
||||
# run: cd seaweed-volume && cargo fmt --check
|
||||
|
||||
# seaweed-common is a path dependency of this crate, not a member of its
|
||||
# workspace, so the run below does not reach its own tests. It builds into
|
||||
# this job's cached target directory, and the cache key above covers the
|
||||
# shared crate's lock, so the aws-lc-sys that rustls pulls in is restored
|
||||
# with the cache instead of compiled from scratch on every run.
|
||||
- name: Run shared-crate unit tests
|
||||
env:
|
||||
CARGO_TARGET_DIR: ${{ github.workspace }}/seaweed-volume/target
|
||||
run: cd seaweed-common && cargo test
|
||||
|
||||
- name: Run Rust unit tests
|
||||
run: cd seaweed-volume && cargo test
|
||||
|
||||
- name: Run Rust unit tests (redb experimental cursor)
|
||||
run: cd seaweed-volume && cargo test --features redb-experimental-cursor --lib storage::needle_map
|
||||
|
||||
rust-unit-tests-windows:
|
||||
name: Rust Unit Tests (Windows)
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 30
|
||||
needs: [changes]
|
||||
if: needs.changes.outputs.rust == 'true'
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# No glibc on Windows: key the cache on the toolchain and OS only.
|
||||
- name: Fingerprint build toolchain
|
||||
id: toolchain
|
||||
run: echo "fingerprint=windows-rustc-$(rustc -V | awk '{print $2}')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache cargo registry and target
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
seaweed-volume/target
|
||||
key: rust-windows-${{ steps.toolchain.outputs.fingerprint }}-${{ hashFiles('seaweed-volume/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
rust-windows-${{ steps.toolchain.outputs.fingerprint }}-
|
||||
|
||||
- name: Run Rust unit tests
|
||||
run: cd seaweed-volume && cargo test
|
||||
|
||||
@@ -95,7 +180,7 @@ jobs:
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
seaweed-volume/target
|
||||
key: rust-${{ steps.toolchain.outputs.fingerprint }}-${{ hashFiles('seaweed-volume/Cargo.lock') }}
|
||||
key: rust-${{ steps.toolchain.outputs.fingerprint }}-${{ hashFiles('seaweed-volume/Cargo.lock', 'seaweed-common/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
rust-${{ steps.toolchain.outputs.fingerprint }}-
|
||||
|
||||
@@ -177,7 +262,7 @@ jobs:
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
seaweed-volume/target
|
||||
key: rust-${{ steps.toolchain.outputs.fingerprint }}-${{ hashFiles('seaweed-volume/Cargo.lock') }}
|
||||
key: rust-${{ steps.toolchain.outputs.fingerprint }}-${{ hashFiles('seaweed-volume/Cargo.lock', 'seaweed-common/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
rust-${{ steps.toolchain.outputs.fingerprint }}-
|
||||
|
||||
|
||||
@@ -5,12 +5,14 @@ on:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'seaweed-worker/**'
|
||||
- 'seaweed-common/**'
|
||||
- 'weed/pb/plugin.proto'
|
||||
- '.github/workflows/rust-worker-tests.yml'
|
||||
push:
|
||||
branches: [ master, main ]
|
||||
paths:
|
||||
- 'seaweed-worker/**'
|
||||
- 'seaweed-common/**'
|
||||
- 'weed/pb/plugin.proto'
|
||||
- '.github/workflows/rust-worker-tests.yml'
|
||||
|
||||
@@ -49,7 +51,7 @@ jobs:
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
seaweed-worker/target/release
|
||||
key: rust-worker-${{ steps.toolchain.outputs.fingerprint }}-${{ hashFiles('seaweed-worker/Cargo.lock') }}
|
||||
key: rust-worker-${{ steps.toolchain.outputs.fingerprint }}-${{ hashFiles('seaweed-worker/Cargo.lock', 'seaweed-common/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
rust-worker-${{ steps.toolchain.outputs.fingerprint }}-
|
||||
|
||||
@@ -73,6 +75,28 @@ jobs:
|
||||
- name: Build the plugin workers
|
||||
run: cd seaweed-worker && cargo build --release
|
||||
|
||||
# The workspace is warning-free under clippy as of the sweep that added
|
||||
# this step. Uncomment to make that a gate; `[workspace.lints.clippy]`
|
||||
# in seaweed-worker/Cargo.toml is where crate-wide exceptions live.
|
||||
# - name: Clippy
|
||||
# run: cd seaweed-worker && cargo clippy --workspace --all-targets -- -D warnings
|
||||
|
||||
# The workspace is rustfmt-clean as of the PR that added this step.
|
||||
# Uncomment to keep it that way.
|
||||
# - name: Check formatting
|
||||
# run: cd seaweed-worker && cargo fmt --all --check
|
||||
|
||||
# seaweed-common is a path dependency of core and lance, not a member of
|
||||
# this workspace, so `--workspace` below does not reach its own tests.
|
||||
# Release and this job's cached target directory, and the cache key above
|
||||
# covers the shared crate's lock. That lock pins the same rustls and
|
||||
# aws-lc-sys this workspace resolves, so the release build above has
|
||||
# already paid for them.
|
||||
- name: Run shared-crate unit tests
|
||||
env:
|
||||
CARGO_TARGET_DIR: ${{ github.workspace }}/seaweed-worker/target
|
||||
run: cd seaweed-common && cargo test --release
|
||||
|
||||
# The tests that need a live gateway skip themselves without one, the way
|
||||
# the Go integration tests skip without Docker; the lifecycle suite in
|
||||
# test/s3tables/lifecycle is what runs them against a real cluster.
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'seaweed-volume/**'
|
||||
- 'seaweed-common/**'
|
||||
- '.github/workflows/rust_binaries_dev.yml'
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
name: "Snowflake S3Compat API tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'weed/s3api/**'
|
||||
- 'weed/filer/**'
|
||||
- 'weed/server/**'
|
||||
- 'weed/iam/**'
|
||||
- 'weed/command/**'
|
||||
- 'weed/storage/**'
|
||||
- 'weed/operation/**'
|
||||
- 'weed/wdclient/**'
|
||||
- 'weed/cluster/**'
|
||||
- 'weed/pb/**'
|
||||
- 'test/s3/snowflake/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- '.github/workflows/s3-snowflake-tests.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'weed/s3api/**'
|
||||
- 'weed/filer/**'
|
||||
- 'weed/server/**'
|
||||
- 'weed/iam/**'
|
||||
- 'weed/command/**'
|
||||
- 'weed/storage/**'
|
||||
- 'weed/operation/**'
|
||||
- 'weed/wdclient/**'
|
||||
- 'weed/cluster/**'
|
||||
- 'weed/pb/**'
|
||||
- 'test/s3/snowflake/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- '.github/workflows/s3-snowflake-tests.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event.pull_request.number || github.ref }}/s3-snowflake-tests
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
snowflake-s3compat-tests:
|
||||
name: Snowflake S3Compat API tests
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
WORK_DIR: /tmp/seaweedfs-snowflake-tests
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
id: go
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v6
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: 'maven'
|
||||
|
||||
- name: Install SeaweedFS
|
||||
run: |
|
||||
cd weed
|
||||
go install -buildvcs=false
|
||||
weed version
|
||||
|
||||
- name: Run Snowflake S3Compat API tests
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
# Starts weed server, creates the buckets/objects the suite needs,
|
||||
# clones the upstream suite, and runs mvn -Dtest=S3CompatApiTest.
|
||||
bash test/s3/snowflake/run.sh
|
||||
|
||||
- name: Show logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== SeaweedFS Server Log ==="
|
||||
tail -200 "$WORK_DIR/weed.log" || echo "No server log"
|
||||
echo ""
|
||||
echo "=== Surefire results ==="
|
||||
cat "$WORK_DIR"/snowflake-s3compat-api-test-suite/s3compatapi/target/surefire-reports/*.txt 2>/dev/null || echo "No surefire reports"
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: snowflake-s3compat-surefire-reports
|
||||
path: /tmp/seaweedfs-snowflake-tests/snowflake-s3compat-api-test-suite/s3compatapi/target/surefire-reports/
|
||||
retention-days: 14
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
pkill -9 -f "weed server" || true
|
||||
rm -rf "$WORK_DIR" || true
|
||||
@@ -439,6 +439,117 @@ jobs:
|
||||
path: test/s3tables/catalog_clickhouse/test-output.log
|
||||
retention-days: 3
|
||||
|
||||
olake-iceberg-catalog-tests:
|
||||
name: OLake Iceberg Catalog Integration Tests (${{ matrix.tag }})
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Pinned baseline, and latest so new OLake releases are exercised
|
||||
# without a code change. OLake's Iceberg writer is a Java sidecar
|
||||
# whose Iceberg version moves independently of the Go release, so
|
||||
# the latest leg is the one that catches library drift.
|
||||
- olake-image: olakego/source-postgres:v0.10.1
|
||||
tag: "v0.10.1"
|
||||
- olake-image: olakego/source-postgres:latest
|
||||
tag: latest
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
id: go
|
||||
|
||||
- name: Configure Docker Hub mirror
|
||||
run: |
|
||||
echo '{"registry-mirrors": ["https://mirror.gcr.io"]}' | sudo tee /etc/docker/daemon.json
|
||||
sudo systemctl restart docker
|
||||
|
||||
- name: Pre-pull images
|
||||
run: |
|
||||
pull() { for i in 1 2 3; do docker pull "$1" && return 0; sleep 15; done; return 1; }
|
||||
pull ${{ matrix.olake-image }}
|
||||
pull postgres:16
|
||||
pull python:3.11-slim
|
||||
|
||||
- name: Run go mod tidy
|
||||
run: go mod tidy
|
||||
|
||||
- name: Install SeaweedFS
|
||||
run: |
|
||||
go install -buildvcs=false ./weed
|
||||
|
||||
- name: Run OLake Iceberg Catalog Integration Tests
|
||||
timeout-minutes: 25
|
||||
working-directory: test/s3tables/catalog_olake
|
||||
env:
|
||||
OLAKE_IMAGE: ${{ matrix.olake-image }}
|
||||
run: |
|
||||
set -x
|
||||
set -o pipefail
|
||||
echo "=== System Information ==="
|
||||
uname -a
|
||||
free -h
|
||||
df -h
|
||||
docker info
|
||||
echo "=== Starting OLake Iceberg Catalog Tests ==="
|
||||
|
||||
go test -v -timeout 20m . 2>&1 | tee test-output.log || {
|
||||
echo "OLake Iceberg catalog integration tests failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# The suite skips itself when Docker is unavailable, so a green job is not
|
||||
# by itself evidence that anything ran. Assert execution explicitly.
|
||||
- name: Assert the suite actually ran
|
||||
working-directory: test/s3tables/catalog_olake
|
||||
run: |
|
||||
log=test-output.log
|
||||
if [ ! -f "$log" ]; then
|
||||
echo "::error::no test-output.log; the suite did not run"
|
||||
exit 1
|
||||
fi
|
||||
passes=$(grep -c '^--- PASS' "$log" || true)
|
||||
skips=$(grep -c '^--- SKIP' "$log" || true)
|
||||
echo "top-level PASS=$passes SKIP=$skips"
|
||||
if [ "$skips" -gt 0 ]; then
|
||||
echo "::error::the OLake suite skipped $skips top-level test(s); the environment it needs was not provisioned, so this job proves nothing"
|
||||
grep '^--- SKIP' "$log" | head -20
|
||||
exit 1
|
||||
fi
|
||||
if [ "$passes" -lt 1 ]; then
|
||||
echo "::error::the OLake suite recorded no passing top-level test"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Show test output on failure
|
||||
if: failure()
|
||||
working-directory: test/s3tables/catalog_olake
|
||||
run: |
|
||||
echo "=== Test Output ==="
|
||||
if [ -f test-output.log ]; then
|
||||
tail -200 test-output.log
|
||||
fi
|
||||
|
||||
echo "=== Process information ==="
|
||||
ps aux | grep -E "(weed|test|docker|olake|postgres)" || true
|
||||
echo "=== Containers ==="
|
||||
docker ps -a | head -30 || true
|
||||
|
||||
- name: Upload test logs on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: olake-iceberg-catalog-test-logs-${{ matrix.tag }}
|
||||
path: test/s3tables/catalog_olake/test-output.log
|
||||
retention-days: 3
|
||||
|
||||
polaris-integration-tests:
|
||||
name: Polaris Integration Tests
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
@@ -289,6 +289,7 @@ jobs:
|
||||
s3tests/functional/test_s3.py::test_object_write_check_etag \
|
||||
s3tests/functional/test_s3.py::test_object_write_cache_control \
|
||||
s3tests/functional/test_s3.py::test_object_write_expires \
|
||||
s3tests/functional/test_s3.py::test_object_content_encoding_aws_chunked \
|
||||
s3tests/functional/test_s3.py::test_object_write_read_update_read_delete \
|
||||
s3tests/functional/test_s3.py::test_object_metadata_replaced_on_put \
|
||||
s3tests/functional/test_s3.py::test_object_write_file \
|
||||
@@ -311,6 +312,7 @@ jobs:
|
||||
s3tests/functional/test_s3.py::test_get_object_ifmodifiedsince_good \
|
||||
s3tests/functional/test_s3.py::test_get_object_ifmodifiedsince_failed \
|
||||
s3tests/functional/test_s3.py::test_get_object_ifunmodifiedsince_failed \
|
||||
s3tests/functional/test_s3.py::test_get_checksum_object_attributes \
|
||||
s3tests/functional/test_s3.py::test_bucket_head \
|
||||
s3tests/functional/test_s3.py::test_bucket_head_notexist \
|
||||
s3tests/functional/test_s3.py::test_object_raw_authenticated \
|
||||
@@ -1149,6 +1151,7 @@ jobs:
|
||||
s3tests/functional/test_s3.py::test_object_write_check_etag \
|
||||
s3tests/functional/test_s3.py::test_object_write_cache_control \
|
||||
s3tests/functional/test_s3.py::test_object_write_expires \
|
||||
s3tests/functional/test_s3.py::test_object_content_encoding_aws_chunked \
|
||||
s3tests/functional/test_s3.py::test_object_write_read_update_read_delete \
|
||||
s3tests/functional/test_s3.py::test_object_metadata_replaced_on_put \
|
||||
s3tests/functional/test_s3.py::test_object_write_file \
|
||||
@@ -1171,6 +1174,7 @@ jobs:
|
||||
s3tests/functional/test_s3.py::test_get_object_ifmodifiedsince_good \
|
||||
s3tests/functional/test_s3.py::test_get_object_ifmodifiedsince_failed \
|
||||
s3tests/functional/test_s3.py::test_get_object_ifunmodifiedsince_failed \
|
||||
s3tests/functional/test_s3.py::test_get_checksum_object_attributes \
|
||||
s3tests/functional/test_s3.py::test_bucket_head \
|
||||
s3tests/functional/test_s3.py::test_bucket_head_notexist \
|
||||
s3tests/functional/test_s3.py::test_object_raw_authenticated \
|
||||
|
||||
@@ -17,10 +17,11 @@ SeaweedFS is a simple and highly scalable distributed file system. There are two
|
||||
1. to store billions of files!
|
||||
2. to serve the files fast!
|
||||
|
||||
One `weed` binary serves an S3 object store, a POSIX file system, and a lakehouse with S3 Tables, all over the same data. Each blob is one disk read away, capacity grows by starting another volume server, and cloud storage can be cached or tiered transparently.
|
||||
One `weed` binary serves an S3 object store, a POSIX file system, and a lakehouse with S3 Tables, all over the same data. Each blob is one disk read away, capacity grows by starting another volume server, and cloud storage can be cached or tiered transparently. Both read and write operations have O(1) complexity and can run at the full speed supported by the underlying hardware.
|
||||
|
||||
- [Download Binaries for different platforms](https://github.com/seaweedfs/seaweedfs/releases/latest)
|
||||
- [Wiki Documentation](https://github.com/seaweedfs/seaweedfs/wiki)
|
||||
- [HTTP REST API](REST_API.md) for the filer, master, and volume servers
|
||||
- Community: [Slack](https://join.slack.com/t/seaweedfs/shared_invite/enQtMzI4MTMwMjU2MzA3LTEyYzZmZWYzOGQ3MDJlZWMzYmI0OTE4OTJiZjJjODBmMzUxNmYwODg0YjY3MTNlMjBmZDQ1NzQ5NDJhZWI2ZmY), [Twitter](https://twitter.com/SeaweedFS), [Telegram](https://t.me/Seaweedfs), [Reddit](https://www.reddit.com/r/SeaweedFS/), [Mailing List](https://groups.google.com/d/forum/seaweedfs)
|
||||
- [SeaweedFS White Paper](https://github.com/seaweedfs/seaweedfs/wiki/SeaweedFS_Architecture.pdf) and introduction slides: [2025.5](https://docs.google.com/presentation/d/1tdkp45J01oRV68dIm4yoTXKJDof-EhainlA0LMXexQE/edit?usp=sharing), [2021.5](https://docs.google.com/presentation/d/1DcxKWlINc-HNCjhYeERkpGXXm6nTCES8mi2W5G0Z4Ts/edit?usp=sharing), [2019.3](https://www.slideshare.net/chrislusf/seaweedfs-introduction)
|
||||
|
||||
@@ -400,8 +401,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
The text of this page is available for modification and reuse under the terms of the Creative Commons Attribution-Sharealike 3.0 Unported License and the GNU Free Documentation License (unversioned, with no invariant sections, front-cover texts, or back-cover texts).
|
||||
|
||||
[Back to TOC](#table-of-contents)
|
||||
|
||||
# Sponsors #
|
||||
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
# SeaweedFS HTTP REST API
|
||||
|
||||
SeaweedFS exposes three HTTP surfaces:
|
||||
|
||||
| Service | Default port | Addressing |
|
||||
|---------|--------------|------------|
|
||||
| Filer | 8888 | File system paths (`/dir/name`) |
|
||||
| Master | 9333 | File id assignment and cluster topology |
|
||||
| Volume server | 8080 | File content by file id (`vid,fid`) |
|
||||
|
||||
Most clients only need the filer API (paths) or the S3 API. The master and
|
||||
volume APIs are the lower-level blob store interface.
|
||||
|
||||
Conventions applying to all three:
|
||||
|
||||
- Responses are JSON unless noted otherwise. Append `&pretty=y` to pretty-print.
|
||||
- A file id (`fid`) has the form `volumeId,fileKeyCookie`, e.g. `3,01637037d6`.
|
||||
An optional suffix selects a reserved id from a `count` assignment
|
||||
(`3,01637037d6_1`, `_2`, ...), and an optional extension
|
||||
(`3,01637037d6.jpg`) sets the content type on reads.
|
||||
- `replication` is a 3-digit replica placement `xyz`: `x` copies in other
|
||||
data centers, `y` on other racks in the same data center, `z` on other
|
||||
volume servers on the same rack. `000` = no replication, `001` = one copy
|
||||
on the same rack, `010` = one copy on a different rack, `100` = one copy in
|
||||
another data center, `200` = two copies in two other data centers, `110` =
|
||||
one copy in another data center plus one on another rack.
|
||||
- `ttl` units: `m` minute, `h` hour, `d` day, `w` week, `M` month, `y` year.
|
||||
|
||||
## Filer API (port 8888)
|
||||
|
||||
The filer presents a POSIX-like namespace over the volume servers.
|
||||
|
||||
### Upload a file
|
||||
|
||||
```bash
|
||||
# PUT the raw body to the target path
|
||||
curl -T /home/chris/myphoto.jpg "http://localhost:8888/dir/myphoto.jpg"
|
||||
|
||||
# or POST as multipart form (the part filename becomes the entry name)
|
||||
curl -F file=@/home/chris/myphoto.jpg "http://localhost:8888/dir/"
|
||||
```
|
||||
|
||||
Response `201 Created`:
|
||||
|
||||
```json
|
||||
{"name":"myphoto.jpg","size":43234,"eTag":"0x6c656...","mtime":"...","chunks":[...]}
|
||||
```
|
||||
|
||||
Query parameters:
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `collection` | collection name | empty |
|
||||
| `replication` | replica placement code | filer default |
|
||||
| `ttl` | file expiration, e.g. `3d` | never |
|
||||
| `disk` | disk type to store on | filer default |
|
||||
| `fsync` | `true` fsyncs on the volume server | false |
|
||||
| `dataCenter` | preferred data center | empty |
|
||||
| `rack` | preferred rack | empty |
|
||||
| `dataNode` | preferred volume server | empty |
|
||||
| `saveInside` | store small content inside the metadata instead of a volume | false |
|
||||
| `maxMB` | split the upload into chunks of this many MB | filer `-maxMB` |
|
||||
| `mode` | unix permission bits, e.g. `0644` | `0660` |
|
||||
| `op` | `append` appends to an existing file | overwrite |
|
||||
| `skipCheckParentDir` | `true` skips the parent-directory existence check | false |
|
||||
|
||||
### Create a directory
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8888/dir/newdir/"
|
||||
```
|
||||
|
||||
A POST to a path ending in `/` with no content creates the directory,
|
||||
including missing parents.
|
||||
|
||||
### Read a file
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8888/dir/myphoto.jpg"
|
||||
```
|
||||
|
||||
Supports `Range` requests (`Accept-Ranges: bytes`), `ETag`, and the
|
||||
`If-None-Match` / `If-Modified-Since` conditional headers. `HEAD` returns
|
||||
headers only. Entry headers stored as extended attributes are echoed back,
|
||||
minus internal `Seaweed-` and `xattr-` keys.
|
||||
|
||||
Entry metadata instead of content:
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8888/dir/myphoto.jpg?metadata=true"
|
||||
```
|
||||
|
||||
`metadata=true&resolveManifest=true` additionally resolves chunked-manifest
|
||||
entries into their real chunk list.
|
||||
|
||||
### List a directory
|
||||
|
||||
```bash
|
||||
curl -H "Accept: application/json" "http://localhost:8888/dir/?limit=10&lastFileName=a.jpg"
|
||||
```
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `limit` | max entries per page | filer `-dirListLimit` |
|
||||
| `lastFileName` | resume listing after this entry name | empty |
|
||||
| `namePattern` | include only names matching the wildcard | empty |
|
||||
| `namePatternExclude` | exclude names matching the wildcard | empty |
|
||||
|
||||
The JSON response carries `Path`, `Entries`, `Limit`, `LastFileName`,
|
||||
`ShouldDisplayLoadMore`, and `EmptyFolder`. Without the `Accept` header the
|
||||
filer renders its HTML browser.
|
||||
|
||||
### Move and copy
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8888/dir/newname.jpg?mv.from=/dir/myphoto.jpg"
|
||||
curl -X POST "http://localhost:8888/dir/copy.jpg?cp.from=/dir/myphoto.jpg"
|
||||
```
|
||||
|
||||
`mv.from` renames or moves the source to the request path (`204 No Content`).
|
||||
`cp.from` copies it.
|
||||
|
||||
### Append
|
||||
|
||||
```bash
|
||||
curl -T chunk2.bin "http://localhost:8888/dir/file.bin?op=append"
|
||||
```
|
||||
|
||||
### Delete
|
||||
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:8888/dir/myphoto.jpg"
|
||||
curl -X DELETE "http://localhost:8888/dir/?recursive=true"
|
||||
```
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `recursive` | delete a non-empty directory tree | false; when the filer runs with `filer.options.recursive_delete=true`, deletes are recursive unless `recursive=false` |
|
||||
| `ignoreRecursiveError` | keep deleting remaining entries after an error | false |
|
||||
| `skipChunkDeletion` | remove only the metadata, keep volume data | false |
|
||||
|
||||
### Tagging
|
||||
|
||||
Tags are carried as `Seaweed-`-prefixed request headers, not query
|
||||
parameters; `?tagging` selects the tagging handler and `?tagging=K1,K2`
|
||||
lists the keys to remove. Header names are canonicalized on write
|
||||
(`Seaweed-k1` is stored as `Seaweed-K1`), and the delete list is matched
|
||||
case-sensitively against the stored names.
|
||||
|
||||
```bash
|
||||
curl -X PUT -H "Seaweed-k1: v1" -H "Seaweed-k2: v2" "http://localhost:8888/dir/file.jpg?tagging"
|
||||
curl -X DELETE "http://localhost:8888/dir/file.jpg?tagging=K1,K2"
|
||||
```
|
||||
|
||||
### Read by file id
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8888/?proxyChunkId=3,01637037d6"
|
||||
```
|
||||
|
||||
The filer proxies the chunk read to the right volume server, so only the
|
||||
filer port needs to be exposed.
|
||||
|
||||
### Resumable uploads
|
||||
|
||||
The filer serves the [TUS protocol](https://tus.io/) for resumable uploads
|
||||
(`POST`, `PATCH`, `HEAD` on upload URLs). It is enabled by default at
|
||||
`/.tus`; `-tusBasePath` changes the endpoint base path.
|
||||
|
||||
### Health
|
||||
|
||||
`GET /healthz` and `GET /readyz` return `200 OK`.
|
||||
|
||||
## Master API (port 9333)
|
||||
|
||||
Write-affecting endpoints are automatically proxied to the current leader, so
|
||||
any master in the quorum can serve them.
|
||||
|
||||
### Assign a file id
|
||||
|
||||
```bash
|
||||
curl "http://localhost:9333/dir/assign?count=1&replication=001&collection=turbo&dataCenter=dc1&ttl=3d&disk=ssd"
|
||||
{"count":1,"fid":"3,01637037d6","url":"127.0.0.1:8080","publicUrl":"localhost:8080"}
|
||||
```
|
||||
|
||||
Upload the file content to `http://<url>/<fid>` afterwards. With `count>1`,
|
||||
use `<fid>_1`, `<fid>_2`, ... for the additional ids.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `count` | file ids to reserve | 1 |
|
||||
| `collection` | collection name | empty |
|
||||
| `dataCenter` | preferred data center | empty |
|
||||
| `rack` | preferred rack | empty |
|
||||
| `dataNode` | preferred volume server | empty |
|
||||
| `replication` | replica placement | master `-defaultReplication` |
|
||||
| `ttl` | file expiration, e.g. `3d` | never |
|
||||
| `disk` | disk type | empty |
|
||||
| `dataSize` | expected file size in bytes | 0 |
|
||||
| `preallocate` | bytes to preallocate for new volumes | master `-volumePreallocate` |
|
||||
| `writableVolumeCount` | grow this many volumes when none are writable | master default |
|
||||
| `memoryMapMaxSizeMb` | memory-mapped file size (Windows) | 0 |
|
||||
|
||||
### Look up a volume or file id
|
||||
|
||||
```bash
|
||||
curl "http://localhost:9333/dir/lookup?volumeId=3"
|
||||
{"locations":[{"url":"localhost:8080","publicUrl":"localhost:8080"}]}
|
||||
```
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `volumeId` | volume id; a full `vid,fid` is accepted too | required |
|
||||
| `fileId` | like `volumeId`, but also returns a write JWT when security is on | empty |
|
||||
| `collection` | speeds up the lookup | empty |
|
||||
| `read` | `yes` generates a read JWT instead of a write JWT | empty |
|
||||
|
||||
### Store a file in one call
|
||||
|
||||
```bash
|
||||
curl -F file=@/home/chris/report.pdf "http://localhost:9333/submit?collection=turbo&replication=001"
|
||||
{"fileName":"report.pdf","fid":"3,01637037d6","fileUrl":"localhost:8080/3,01637037d6","size":43234,"eTag":"0x6c656..."}
|
||||
```
|
||||
|
||||
`POST /submit` accepts multipart file data plus the `dir/assign` placement
|
||||
parameters (`count`, `collection`, `dataCenter`, `rack`, `replication`,
|
||||
`ttl`, `disk`), assigns a file id, uploads to the volume server, and returns
|
||||
the result.
|
||||
|
||||
### Redirect to a file
|
||||
|
||||
```bash
|
||||
curl -v "http://localhost:9333/3,01637037d6"
|
||||
```
|
||||
|
||||
`GET /{fileId}` answers `308 Permanent Redirect` to a volume server holding
|
||||
the file, preserving the query string (e.g. image-resize parameters).
|
||||
|
||||
### Cluster status
|
||||
|
||||
```bash
|
||||
curl "http://localhost:9333/dir/status?pretty=y" # full topology tree
|
||||
curl "http://localhost:9333/vol/status?pretty=y" # every volume on every node
|
||||
curl "http://localhost:9333/collection/info?collection=turbo"
|
||||
curl "http://localhost:9333/collection/info?collection=turbo&detail=true"
|
||||
```
|
||||
|
||||
`collection/info` returns aggregated `TotalSize`, `FileCount`, `UsedSize`,
|
||||
`VolumeCount`; `detail=true` splits them per volume layout.
|
||||
|
||||
### Grow volumes
|
||||
|
||||
```bash
|
||||
curl "http://localhost:9333/vol/grow?count=4&replication=001&collection=turbo&ttl=5d&disk=ssd&dataCenter=dc1&rack=rack1"
|
||||
{"count":4}
|
||||
```
|
||||
|
||||
`count` is required; the placement parameters match `dir/assign`. One volume
|
||||
serves one write at a time, so pre-allocated volumes raise write concurrency.
|
||||
|
||||
### Vacuum deleted space
|
||||
|
||||
```bash
|
||||
curl "http://localhost:9333/vol/vacuum?garbageThreshold=0.4"
|
||||
```
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `garbageThreshold` | minimum deleted-bytes ratio before a volume is compacted | master `-garbageThreshold` (0.3) |
|
||||
|
||||
Vacuuming makes a volume read-only, copies live needles to a new volume, and
|
||||
swaps it in.
|
||||
|
||||
### Delete a collection
|
||||
|
||||
```bash
|
||||
curl "http://localhost:9333/col/delete?collection=benchmark"
|
||||
```
|
||||
|
||||
Deletes all volumes of the collection, including erasure-coded shards.
|
||||
`204 No Content` on success.
|
||||
|
||||
### Health
|
||||
|
||||
```bash
|
||||
curl -I "http://localhost:9333/healthz" # liveness
|
||||
curl -I "http://localhost:9333/readyz" # readiness
|
||||
curl "http://localhost:9333/" # web UI
|
||||
```
|
||||
|
||||
## Volume server API (port 8080)
|
||||
|
||||
The volume server stores file content by file id. Clients normally get the
|
||||
volume URL from `dir/assign` or `dir/lookup`.
|
||||
|
||||
### Upload
|
||||
|
||||
```bash
|
||||
curl -F file=@/home/chris/myphoto.jpg "http://127.0.0.1:8080/3,01637037d6"
|
||||
{"name":"myphoto.jpg","size":43234,"eTag":"0x6c656...","mime":"image/jpeg","contentMd5":"..."}
|
||||
```
|
||||
|
||||
PUT or POST the body (or a multipart `file` part) to `/{vid},{fid}`.
|
||||
`204 No Content` is returned when the content is unchanged. `?ts=<unix>`
|
||||
sets the stored modification time.
|
||||
|
||||
### Read
|
||||
|
||||
```bash
|
||||
curl "http://127.0.0.1:8080/3,01637037d6"
|
||||
curl "http://127.0.0.1:8080/3,01637037d6.jpg" # sets Content-Type from the extension
|
||||
```
|
||||
|
||||
Supports `Range` and `HEAD`. Image files can be resized server-side:
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `width`, `height` | resize bounds in pixels |
|
||||
| `mode` | `fit` (contain) or `fill` (cover); omitted resizes to `width`/`height` |
|
||||
| `crop_x1`, `crop_y1`, `crop_x2`, `crop_y2` | explicit crop rectangle |
|
||||
| `cm` | `false` returns the chunk-manifest blob instead of resolving it |
|
||||
| `readDeleted` | `true` reads soft-deleted needles |
|
||||
| `collection` | passed through redirects for the right volume |
|
||||
|
||||
### Delete
|
||||
|
||||
```bash
|
||||
curl -X DELETE "http://127.0.0.1:8080/3,01637037d6"
|
||||
{"size":43234}
|
||||
```
|
||||
|
||||
`?ts=<unix>` sets the deletion timestamp. Replicated volumes propagate the
|
||||
delete to every replica.
|
||||
|
||||
### Status
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8080/status?pretty=y" # disk and volume inventory
|
||||
curl -I "http://localhost:8080/healthz" # liveness/readiness
|
||||
```
|
||||
|
||||
`OPTIONS` preflights answer CORS headers. When `-port.public` differs from
|
||||
`-port`, the volume server opens a separate read-only public listener on
|
||||
that port; `-publicUrl` sets the address it advertises to clients.
|
||||
@@ -14,6 +14,9 @@ RUN cd /go/src/github.com/seaweedfs/seaweedfs && \
|
||||
git checkout $BRANCH) || \
|
||||
(echo "ERROR: Branch/commit $BRANCH not found in repository" && \
|
||||
echo "Available branches:" && git branch -a && exit 1))
|
||||
# seaweed-common only exists on revisions that have it; a BRANCH predating it
|
||||
# still needs the directory so the COPY into rust_builder below never fails.
|
||||
RUN mkdir -p /go/src/github.com/seaweedfs/seaweedfs/seaweed-common
|
||||
ARG TARGETOS TARGETARCH TARGETVARIANT
|
||||
RUN cd /go/src/github.com/seaweedfs/seaweedfs/weed \
|
||||
&& export LDFLAGS="-X github.com/seaweedfs/seaweedfs/weed/util/version.COMMIT=$(git rev-parse --short HEAD)" \
|
||||
@@ -31,6 +34,9 @@ ARG TAGS
|
||||
COPY weed-volume-prebuilt/ /prebuilt/
|
||||
COPY weed-worker-prebuilt/ /prebuilt-worker/
|
||||
COPY --from=builder /go/src/github.com/seaweedfs/seaweedfs/seaweed-volume /build/seaweed-volume
|
||||
# seaweed-common is a path dependency of seaweed-volume that lives beside it,
|
||||
# so the source build below needs it in the same relative position.
|
||||
COPY --from=builder /go/src/github.com/seaweedfs/seaweedfs/seaweed-common /build/seaweed-common
|
||||
COPY --from=builder /go/src/github.com/seaweedfs/seaweedfs/weed /build/weed
|
||||
WORKDIR /build/seaweed-volume
|
||||
RUN if [ -f "/prebuilt/weed-volume-${TARGETARCH}" ]; then \
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
FROM alpine:latest
|
||||
|
||||
# Install required packages
|
||||
RUN apk add --no-cache \
|
||||
RUN apk upgrade --no-cache && \
|
||||
apk add --no-cache \
|
||||
ca-certificates \
|
||||
fuse \
|
||||
curl \
|
||||
jq
|
||||
jq \
|
||||
libcrypto3 \
|
||||
libssl3
|
||||
|
||||
# Copy our locally built binary
|
||||
COPY weed-local /usr/bin/weed
|
||||
|
||||
@@ -30,3 +30,5 @@ sleep_minutes = 17 # sleep minutes between each script execution
|
||||
bucket = "volume_bucket" # an existing bucket
|
||||
endpoint = "http://server2:8333"
|
||||
storage_class = "STANDARD_IA"
|
||||
# upload_concurrency = 5 # concurrent multipart part uploads per volume (volume.tier.upload -concurrent overrides)
|
||||
# download_concurrency = 5 # concurrent multipart part downloads per volume (volume.tier.download -concurrent overrides)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/seaweedfs/seaweedfs
|
||||
|
||||
go 1.26.0
|
||||
go 1.26.6
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
@@ -14,7 +14,7 @@ require (
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/dustin/go-humanize v1.1.0
|
||||
github.com/eapache/go-resiliency v1.6.0 // indirect
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
|
||||
github.com/eapache/queue v1.1.0 // indirect
|
||||
@@ -25,7 +25,7 @@ require (
|
||||
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-redsync/redsync/v4 v4.17.0
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/go-sql-driver/mysql v1.10.1
|
||||
github.com/go-zookeeper/zk v1.0.4 // indirect
|
||||
github.com/golang/protobuf v1.5.4
|
||||
github.com/golang/snappy v1.0.0
|
||||
@@ -70,7 +70,7 @@ require (
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
@@ -90,18 +90,18 @@ require (
|
||||
gocloud.dev v0.46.0
|
||||
gocloud.dev/pubsub/natspubsub v0.46.0
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.46.0
|
||||
golang.org/x/crypto v0.56.0
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597
|
||||
golang.org/x/image v0.45.0
|
||||
golang.org/x/image v0.46.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/tools v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.37.0
|
||||
golang.org/x/sys v0.48.0
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/tools v0.49.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.296.0
|
||||
google.golang.org/api v0.297.0
|
||||
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d // indirect
|
||||
google.golang.org/grpc v1.85.0-dev
|
||||
google.golang.org/grpc v1.85.0-dev.0.20260915183914-4e49413dcab7
|
||||
google.golang.org/protobuf v1.36.12
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
modernc.org/b v1.0.0 // indirect
|
||||
@@ -111,7 +111,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/kms v1.33.0
|
||||
cloud.google.com/go/kms v1.35.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/Jille/raft-grpc-transport v1.6.1
|
||||
@@ -122,9 +122,9 @@ require (
|
||||
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.45.1
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.35
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.1
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.4
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.107.3
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/cognusion/imaging v1.0.4
|
||||
@@ -146,21 +146,21 @@ require (
|
||||
github.com/rabbitmq/amqp091-go v1.14.0
|
||||
github.com/rclone/rclone v1.75.1
|
||||
github.com/rdleal/intervalst v1.5.0
|
||||
github.com/redis/go-redis/v9 v9.21.0
|
||||
github.com/redis/go-redis/v9 v9.22.0
|
||||
github.com/schollz/progressbar/v3 v3.19.1
|
||||
github.com/seaweedfs/go-fuse/v2 v2.9.4
|
||||
github.com/shirou/gopsutil/v4 v4.26.7
|
||||
github.com/tarantool/go-option v1.1.0
|
||||
github.com/tarantool/go-tarantool/v3 v3.0.1
|
||||
github.com/tarantool/go-tarantool/v3 v3.0.2
|
||||
github.com/testcontainers/testcontainers-go v0.44.0
|
||||
github.com/tikv/client-go/v2 v2.0.7
|
||||
github.com/twmb/avro v1.8.0
|
||||
github.com/twmb/avro v1.9.0
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/ydb-platform/ydb-go-sdk-auth-environ v0.5.2
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.151.1
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.7.1
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/sync v0.22.0
|
||||
go.uber.org/atomic v1.12.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/tools/godoc v0.1.0-deprecated
|
||||
google.golang.org/grpc/security/advancedtls v1.0.0
|
||||
)
|
||||
@@ -185,7 +185,7 @@ require (
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/apache/arrow-go/v18 v18.7.0 // indirect
|
||||
github.com/apache/thrift v0.24.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.7.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.10.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/bazelbuild/rules_go v0.46.0 // indirect
|
||||
github.com/biogo/store v0.0.0-20201120204734-aad293a2328f // indirect
|
||||
@@ -283,34 +283,34 @@ require (
|
||||
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.1.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.36.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.45.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.38.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
gonum.org/v1/gonum v0.17.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.2 // indirect
|
||||
cel.dev/expr v0.25.3 // indirect
|
||||
cloud.google.com/go/auth v0.23.2 // 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.12.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.30.0 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.7.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 // indirect
|
||||
github.com/Files-com/files-sdk-go/v3 v3.3.194 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.34.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect
|
||||
github.com/IBM/go-sdk-core/v5 v5.23.1 // indirect
|
||||
@@ -327,20 +327,20 @@ require (
|
||||
github.com/appscode/go-querystring v0.0.0-20170504095604-0126cfb3f1dc // indirect
|
||||
github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.39 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.39.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.42.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.35.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.40.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.47.1
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.38.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.51.0
|
||||
github.com/aws/smithy-go v1.28.1
|
||||
github.com/boltdb/bolt v1.3.1 // indirect
|
||||
github.com/bradenaw/juniper v0.15.3 // indirect
|
||||
@@ -363,7 +363,7 @@ require (
|
||||
github.com/elastic/gosigar v0.14.3 // indirect
|
||||
github.com/emersion/go-message v0.18.2 // indirect
|
||||
github.com/emersion/go-vcard v0.0.0-20260618161152-d854b7e0e2d3 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.39.1-0.20260819172001-e6e3fd93e4be // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.1.0 // indirect
|
||||
@@ -388,7 +388,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.20 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.21 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
@@ -438,7 +438,7 @@ require (
|
||||
github.com/oracle/oci-go-sdk/v65 v65.121.0 // indirect
|
||||
github.com/panjf2000/ants/v2 v2.12.1 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.4.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||
github.com/pengsrc/go-shared v0.2.1-0.20190131101655-1999055a4a14 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.29
|
||||
@@ -489,9 +489,9 @@ require (
|
||||
go.etcd.io/bbolt v1.5.0 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.7.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.45.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 // indirect
|
||||
go.opentelemetry.io/otel v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
|
||||
@@ -499,9 +499,9 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.45.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
golang.org/x/term v0.45.0
|
||||
golang.org/x/term v0.46.0
|
||||
golang.org/x/time v0.15.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260817212433-ac3dfec99bb1 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/validator.v2 v2.0.1 // indirect
|
||||
|
||||
@@ -6,8 +6,8 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
|
||||
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
|
||||
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
|
||||
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
|
||||
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
|
||||
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0=
|
||||
cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
@@ -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.33.0 h1:pG0X78m212b2pv9N4fdMoUO69LuZGQ9kSvn8sHBOFAo=
|
||||
cloud.google.com/go/kms v1.33.0/go.mod h1:CSGvW6GnMQbY+1nOHcIzhMtHSbExXlOmCKjWtYVjcpA=
|
||||
cloud.google.com/go/kms v1.35.0 h1:nJ/ktaqspx1nPM9vIcO0SHbhqCAm8nvAxL1siuVgKm0=
|
||||
cloud.google.com/go/kms v1.35.0/go.mod h1:0++71pIHvJL+GmMa8K4jOWFq7gNOX3jm2PRMSJwTKJw=
|
||||
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=
|
||||
@@ -553,10 +553,10 @@ 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/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 h1:zvXfGJCWvywnCA814d8ZiVyt+fm9nnTE8xSb99zRyfo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1/go.mod h1:iptorS+VYKFL2N6PnebpS91dubG35eAOEERnT4PJbQU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1 h1:u93s+zU2JD62im61Bm5CZIc1ZrOJaIAWEg0WOrMVkEo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1/go.mod h1:oXtinPO4OLj9d1DOTrqrL1oRwGhcqadvAmrl6wTeGlk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0/go.mod h1:mCBhUhlMjLLJKr5aqw2TNS/VqJOie8MzWq3DAMJeKso=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
|
||||
@@ -577,8 +577,8 @@ github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDP
|
||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 h1:Nljr4q1GRA/5vCrMONS+g4u4LRHNgOXVSh3O43J2CnI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Codefor/geohash v0.0.0-20140723084247-1b41c28e3a9d h1:iG9B49Q218F/XxXNRM7k/vWf7MKmLIS8AcJV9cGN4nA=
|
||||
@@ -593,8 +593,8 @@ github.com/FilenCloudDienste/filen-sdk-go v0.0.39 h1:tgV5jYL6dsXop9TpDTIQU6UwJjw
|
||||
github.com/FilenCloudDienste/filen-sdk-go v0.0.39/go.mod h1:0cBhKXQg49XbKZZfk5TCDa3sVLP+xMxZTWL+7KY0XR0=
|
||||
github.com/Files-com/files-sdk-go/v3 v3.3.194 h1:dtOFxSTWWRpkmvXa6ycNiw8dVDu1wkgzcXyVV1VafNc=
|
||||
github.com/Files-com/files-sdk-go/v3 v3.3.194/go.mod h1:rl0WumSN9gSo775DgvQv+wMQ8rlb0ES/1hU5jkMtLXg=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.34.0 h1:yzIYdwuro811Z27D3T80Wkd3rqZzb0K43nner7Eh1yE=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.34.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0 h1:bN1gA3of5bXtbnLsRPrwfmbbe7A5UWFlcTHseujLnpc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0/go.mod h1:Yj5vHEz/aAepZGliRJsA6uvHAVAQyEwajq9ORCHPxzM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 h1:jLdiS1vO+XJFyDSWRHBx56r4s/NNtcl5J6KyCcWUX/w=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0/go.mod h1:8lmpHY+1VRoteiOwyrQMDt1YGXOrFKCz+1wJW7n3ODY=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0 h1:cSjUzZ7KU8hicTgzaSv9NmSyM9fTVK3y5lsBUl3wOis=
|
||||
@@ -710,48 +710,48 @@ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+
|
||||
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
|
||||
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.45.1 h1:iIoG3NaLhV6UZpPXyPXlDj2I9oS8tV/nMcMnITCC6Ks=
|
||||
github.com/aws/aws-sdk-go-v2 v1.45.1/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.0 h1:0jsHallhJCeaU0Ko48c/3FK1ctOQ7NpzggxriJOQ8MQ=
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.0/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18 h1:LAfOuhAH331fmOjTQpAaOlH+Ftn7RzSDJ2VFwjdMMy4=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18/go.mod h1:4e5xhuXHx1e4U9EthvbPP1r/DIMp5c2823OL8karzcM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.35 h1:UEzXuET8E42lxBPijuACu/tEK7v5lFPlk0Q+GT5WD9E=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.35/go.mod h1:KaMtJpFa2JlL2BStjjHQVwQpzZEmw+ND/EgVrfFoo2g=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.1 h1:Z8GRNEx0u9sDkZOq4PUnN8mjGwbUQGRzMSXpvt3d8xQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.1/go.mod h1:uBIK00kFo95dnemqfFMTWx0X8YRqsh6ecIoCjjOkZqM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1 h1:YIEBqcqRnpi4Pfv0YHImtgi6czGCwKHANC7SwmUAVD0=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1/go.mod h1:imEf0oufgAo8KAkCHhrOdqGEC0YWx1PPBQH82shSxGw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.4 h1:hTvrJJseKbvw32kmiE0G+u/9ZqpqscjDrTigHIXP2qs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.4/go.mod h1:gWp9O1ZBWwpcIrgV+mVHk4gZUurAEDkgypu/OXOlIaw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.0 h1:AM4hHjww+PSFtt6E+UrBrPlZkWsePCLEt9AjkfQX+yM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.0/go.mod h1:3x/yXezeQjpOvBb4jEMxrS8SXvpdvJ5abv6l5c1gWM8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.34 h1:Pn7OsMwBLbkZ6OnCxWHAjf0L/22H8cnhxZC0uPwtMtg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.34/go.mod h1:eToXR/Gk1uqpn04eSmdgVXwfS0WvH8aG4eBFr8ygbpU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.3.11 h1:eBXB8KZgzQ8A9QB4iJS4aw/u6+4OY3i2hQXPABeAIOg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.3.11/go.mod h1:N9+5pG27Fy61GUL5YXVLXDTLmUudMrgwsuDbgBMNLxQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1 h1:pc138gM1CW+XPc60rEwUlwwuwWFQK16CI1T7v1F9Oec=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1/go.mod h1:1+koxpPIbfBdfzP6vojm5/zTpTQ/micYwlxIiNB3TxI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1 h1:K0JsbZQj+1h208Ro1zHeA4l7bMp0NvRffHQ91q8Ol1s=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1/go.mod h1:W3/vL6EtCIatICGy9ab29QhMuae+cOKPWcMxv02CO+Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1 h1:yhw5KD1phVyP9vijxOUzDfEtJx+bt+L63k+VfuiYFAA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1/go.mod h1:ZW2e0d7DYlRxlS9hEiMXE47gTdX5KRN4byUiNbUpG+Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.3 h1:Hp/VgjP0BysR3OgLlR057Vz2LcbbVnoWeJ+3qWiS/fY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.3/go.mod h1:nwGV5qw7F1IZPgxCvA/ph8N2TAuz+BkRG/bXn808qMA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.3 h1:MUaM4f+kj1ZIBPZfUS8cxP1GKXXZtHJjAthy93AN7SM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.3/go.mod h1:6YmVmEVRI5ZZzRjCSsb9SryKH0hAlMRdgA7kG9aDvBU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.3 h1:fuSCw4Z2qfRCztMPO3GXJNSiEp6Wee+WOLwrHHUMy9c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.3/go.mod h1:6SxcHheD1pPR5+kWm1wGvjlL/YqUsh267sAfEmN4K7A=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.31 h1:uZOinZb+h7lZw8IYzP1z1IuEnueB76/EFkcf/fEW4Ag=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.31/go.mod h1:NRtwAM/p5VRt03TlEUs0pH3TeWamWdf4YyJpSrzPYLc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1 h1:RmmWQPREQdk9U+PfqeHW3MqZaBaNK7TpV9W3RY+b+7g=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1/go.mod h1:0A3W4F+68ZnNk5XcNL/e9HFMwnP8RlEicFfy6eOEDyw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.3 h1:bON1rJf67TSTDCKg816AAIE4xSTtoo9tl0XRkO72R+I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.3/go.mod h1:c5BBpjJcQXpfeq9iASyVKA3T6vX6B6LEXY4mL/gklDY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.39 h1:HLPAVrlLDaN2boN0xJx7MgaQDNEO3Q+c9L6kl/8m47Q=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.39/go.mod h1:Pg/dVfsNkm1hsIDK/gMvCKtmyNfNTV12mrgHqVE/6Oo=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.107.3 h1:IKoCZqfWfZzSBi16QFQ+QcbQ3LRQ7QgB1S5tDAyPBQQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.107.3/go.mod h1:RBpRcXiM4s2pOInVs32GsBonnje+fiAj4mcrStRmlCA=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.7.1 h1:mdMtSVKdQ3+mzBh+l0ogrFYZVQUCg6pJZOirA2ARsYE=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.7.1/go.mod h1:9IqUlsJDbUPcg6cgx3WEzXdjrbWzLDQrak0aaSqlTcI=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.10.0 h1:ZD5qFpWcaOKdTuhBi431pIDkCgrMkMlMT6jlpSPoIRI=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.10.0/go.mod h1:8Nuuf+tR346PjJ3MvZPh9pekbLiLQFWJhzMXfwy7alA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.39.14 h1:p8WdWDh5AwSZdp19Haa3XMyPCICi9Z375a/Nu3IIEZY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.39.14/go.mod h1:NKVY7DER6VXHkt2I/ycmHakALNboi3Rqwt4eEf/1Cnk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.42.24 h1:JP2wjWGmUp8lTCZb13Dv0Eciyc1jbO8pd0HZVMHFlrc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.42.24/go.mod h1:Ql9ziDutk8ERAN9HMaYANCW3lop451ppebkxEJMLCTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.35.1 h1:B6WFn91tobD6gG4724ONHaqrpKsoETGnv98LHe/yIGM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.35.1/go.mod h1:tWuiVBUtPBr8/rgRiYS8Uf85sHcAN+G7XS3D3CEoUh8=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.40.1 h1:6yeYCWFvgbI2TI3K6jr9LtBNhXgJ7g4xqD+DEiaDDmM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.40.1/go.mod h1:naFe83jSMuYkH+QjQPX8n1MLhBkeCFM5Lsnh5m5wz3c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.47.1 h1:Sv2xPnRHlThSUtVujYuUBPI/Il8si6UPHXL8DMiB/F0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.47.1/go.mod h1:mKo/CzaCz8qytGW70NG4vIIGAx1HXTlb5lHNkC5k3lk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.38.0 h1:JGeeBcMlhg1xtOXYpeCaTQBZObtXMPQCUqBcmr65NRA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.38.0/go.mod h1:XwteswG9EOMRFm73UT0t+MbTwyLxMrEXkU6e+v92Lzo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.0 h1:obhahQXDEdVEv8y5bTKXR30LVaxYe1kyYM0L7l2Iq+k=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.0/go.mod h1:6twZZ/aXHNy1vXUO8koUbp++MYzMASkOgEBdkbJYmO0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.51.0 h1:Zpnqa6XtrNzXZnwbdCqHOXpXhMsa01ql/pcRQ1sb4hk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.51.0/go.mod h1:/8JRcdTt//hG0Q4BTmGbuOplT7ABe+5rdtqUHqXvYIM=
|
||||
github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ=
|
||||
github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
@@ -953,8 +953,8 @@ github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.4.0/go.mod h1:gDXhl0OElhzYoDs
|
||||
github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
|
||||
github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg=
|
||||
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
|
||||
github.com/eapache/go-resiliency v1.6.0 h1:CqGDTLtpwuWKn6Nj3uNUdflaq+/kIPsg0gfNzHton30=
|
||||
github.com/eapache/go-resiliency v1.6.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
|
||||
@@ -987,8 +987,8 @@ github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJ
|
||||
github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.39.1-0.20260819172001-e6e3fd93e4be h1:SWe0x6yfglnxuvOiYgTTnNq7QD/yvqthOh1RBI8Bj8w=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.39.1-0.20260819172001-e6e3fd93e4be/go.mod h1:PYEOlng9XcrulfyWpm49jECTPV0LT4q8cO7fLW/xwgk=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
@@ -1109,8 +1109,8 @@ github.com/go-redsync/redsync/v4 v4.17.0 h1:FFJ+uxZs44y4Sq10//IFKic9T94AYl+u3Sog
|
||||
github.com/go-redsync/redsync/v4 v4.17.0/go.mod h1:CKVA6qwT07S/916i+Yd9h1/8YFQhCCpPYTQhvvYytJo=
|
||||
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
|
||||
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME=
|
||||
github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
@@ -1262,8 +1262,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.20 h1:t/xL64VUoN69MuMRQuJETqYGOw4Z9mSRJK9epIEtwFk=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.20/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.21 h1:OFdQ3tnCX/zaQ0Cedur3D3z7kI6HiLX9g3TiAN4/DFU=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.21/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
|
||||
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=
|
||||
@@ -1652,8 +1652,8 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pelletier/go-toml/v2 v2.4.1 h1:j5OMOImsH+j2k7GJ5YO+RxfWwohNiH6t5zB/+h3bagc=
|
||||
github.com/pelletier/go-toml/v2 v2.4.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pengsrc/go-shared v0.2.1-0.20190131101655-1999055a4a14 h1:XeOYlK9W1uCmhjJSsY78Mcuh7MVkNjTzmHx1yBzizSU=
|
||||
github.com/pengsrc/go-shared v0.2.1-0.20190131101655-1999055a4a14/go.mod h1:jVblp62SafmidSkvWrXyxAme3gaTfEtWwRPGz5cpvHg=
|
||||
github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
|
||||
@@ -1770,8 +1770,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rdleal/intervalst v1.5.0 h1:SEB9bCFz5IqD1yhfH1Wv8IBnY/JQxDplwkxHjT6hamU=
|
||||
github.com/rdleal/intervalst v1.5.0/go.mod h1:xO89Z6BC+LQDH+IPQQw/OESt5UADgFD41tYMUINGpxQ=
|
||||
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
|
||||
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||
github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0=
|
||||
github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4=
|
||||
github.com/redis/rueidis v1.0.76 h1:RdDWuvlYBSp+bTrBvaXqJnNEL3VVzsnjo+0psPFgLc4=
|
||||
github.com/redis/rueidis v1.0.76/go.mod h1:UsfHPSbomB6QAVMk4iiFkzRy0nh9o7scDGa+SitvBY4=
|
||||
github.com/redis/rueidis/rueidiscompat v1.0.76 h1:7LikbiqCQqCsZXeZ+akgZMnjIV/J0VHih9PIX4gGZC4=
|
||||
@@ -1897,8 +1897,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
|
||||
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=
|
||||
@@ -1918,8 +1918,8 @@ 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-option v1.1.0 h1:ShoOhNsdL41sRpm4hXCRDjV8H0WzPkd4UnKhLKbW//w=
|
||||
github.com/tarantool/go-option v1.1.0/go.mod h1:hMr9z2JXOWlgdCBpCPSL2nwp8718GKYvNBJ+ZuzJbCo=
|
||||
github.com/tarantool/go-tarantool/v3 v3.0.1 h1:vaUX4xmVmXh2dIJ/LqlX1MXK3iYqAqV6YiE54Wwl/qg=
|
||||
github.com/tarantool/go-tarantool/v3 v3.0.1/go.mod h1:TXxLWhUCgdxXFfelnTSkq+goKRTRj660zxq4/WXPe8k=
|
||||
github.com/tarantool/go-tarantool/v3 v3.0.2 h1:9ZtHllun80QX7KS9tqd3dXa+QAu2BfqFtX1ZWcNnaW8=
|
||||
github.com/tarantool/go-tarantool/v3 v3.0.2/go.mod h1:TXxLWhUCgdxXFfelnTSkq+goKRTRj660zxq4/WXPe8k=
|
||||
github.com/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI=
|
||||
github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE=
|
||||
github.com/testcontainers/testcontainers-go/modules/compose v0.44.0 h1:8YcW51jhgpkkiRVe10Wj9TCBthJmoNpU2fK5WSf7TQ8=
|
||||
@@ -1965,8 +1965,8 @@ github.com/tsuna/gohbase v0.0.0-20201125011725-348991136365/go.mod h1:zj0GJHGvyf
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/twmb/avro v1.8.0 h1:UMWLg+nH4P3yad5Om7yFSohYLy2RG1s7BcFFiOvmK9Q=
|
||||
github.com/twmb/avro v1.8.0/go.mod h1:X0fT1dY2xcbV4YuCE4mYro+qljHl4kUF5uA/2z1rgSk=
|
||||
github.com/twmb/avro v1.9.0 h1:JSiqewo3AANj7vlVCQXBsIZTA3LiiBble7Xp8T4lbtA=
|
||||
github.com/twmb/avro v1.9.0/go.mod h1:X0fT1dY2xcbV4YuCE4mYro+qljHl4kUF5uA/2z1rgSk=
|
||||
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
|
||||
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
|
||||
github.com/twpayne/go-geom v1.6.1 h1:iLE+Opv0Ihm/ABIcvQFGIiFBXd76oBIar9drAwHFhR4=
|
||||
@@ -2103,30 +2103,30 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
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/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.45.0 h1:9jR0ZPRok9ryaOQ2Wx8rg5F7Aon59mxrqbVI60/vlBk=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.45.0/go.mod h1:VSme3o2fvSg5bVg0dRzyHaj4Z5EVhG+g2Fde6LKzmQA=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 h1:MCcYL7J6Vt/X0kjqbMZkekCmwsurbQRbL69vkiye2lk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0/go.mod h1:3jnStNwSufK+f5ktjL4EPcwtig4rtd81NS70lqHuXl8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04=
|
||||
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
|
||||
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 h1:RuynHbfU8JUEw7DyONgkVYg2SVtsoF28y0LGIr69jgA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0/go.mod h1:qZF+/lBs71APw8mlnEZcqZHMzqrYrsFiJOv83lX1OGo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q=
|
||||
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/exporters/zipkin v1.45.0 h1:KN3btaILMTxR4QDHVGAO87lq5ButzK7l+kIfLuxQ1oA=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.45.0/go.mod h1:yNcodmUclM4InyWoOwX/YW4Jri0Gj5FWAlM+NqCrtqY=
|
||||
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
|
||||
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
|
||||
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
|
||||
@@ -2140,13 +2140,13 @@ go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbi
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/atomic v1.12.0 h1:BvcXdFKuviU4fTL/f+SxdQ5qJX/Jix8pAkgdUcb3XOE=
|
||||
go.uber.org/atomic v1.12.0/go.mod h1:I6c4cg+6HCxRjfjSsYtApoFILnpc0CGUdGkXVqbYVNk=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -2163,8 +2163,8 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
gocloud.dev v0.46.0 h1:niIuZwSjMtBx8K+ITB2s5kZullB13PGOS2ZoQPZxQ4Q=
|
||||
@@ -2193,8 +2193,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
|
||||
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -2225,8 +2225,8 @@ golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeap
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
|
||||
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
|
||||
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -2258,8 +2258,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
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.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
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=
|
||||
@@ -2354,8 +2354,8 @@ golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri
|
||||
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
|
||||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -2374,8 +2374,8 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -2477,8 +2477,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
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=
|
||||
@@ -2491,8 +2491,8 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -2511,8 +2511,8 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -2589,8 +2589,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
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.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
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=
|
||||
@@ -2668,8 +2668,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.296.0 h1:Nn5EHeKdGx70MFClaV/II0gsWUm6xhEjb0xYLylVvaA=
|
||||
google.golang.org/api v0.296.0/go.mod h1:02qB8+Ox1ZFzcaKFMguy1nQLJmSIyvV6Ff4txJEXtl4=
|
||||
google.golang.org/api v0.297.0 h1:WktxTsnnx0yZNnsR6j0q6hR21RnnK81FHTOPy/ux4OE=
|
||||
google.golang.org/api v0.297.0/go.mod h1:S4m8x0M6OkQpkOzGk1y9JG2sm4fFQrMh6dxzjCTszhE=
|
||||
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=
|
||||
@@ -2805,8 +2805,8 @@ google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
|
||||
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms=
|
||||
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d/go.mod h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d h1:QwnJwPte4XXAkhPu26LTDIahnsMSUV0kK8HkxbC+Pc4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d/go.mod h1:WRrQ7/7N19PypuT0fxLOL5Lq0waoiRri4FbtHDEKrGE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260817212433-ac3dfec99bb1 h1:lrupDmKL3p5kEX1M92oan027eCKcouzjuPbH6YBK+Rs=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260817212433-ac3dfec99bb1/go.mod h1:q/3oV3jAi5vwelxsVAprMBC8BcM2zmNe+IjRGd+9/ks=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -2849,8 +2849,8 @@ google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsA
|
||||
google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
|
||||
google.golang.org/grpc v1.85.0-dev h1:HxkDyKIIZPpFnroC56tQv5gNuKTmVvi0t7TzOf5zt7g=
|
||||
google.golang.org/grpc v1.85.0-dev/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
|
||||
google.golang.org/grpc v1.85.0-dev.0.20260915183914-4e49413dcab7 h1:5+EEM1fC0yjOZID0NUZVrE2+8M/+1TclNrSz/l1xMYs=
|
||||
google.golang.org/grpc v1.85.0-dev.0.20260915183914-4e49413dcab7/go.mod h1:Ovl0ECo4xx5r4kn/6d4BPSNB7OIFuu6EAjOzjtVAKaM=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 h1:ExN12ndbJ608cboPYflpTny6mXSzPrDLh0iTaVrRrds=
|
||||
google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8=
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
apiVersion: v1
|
||||
description: SeaweedFS
|
||||
name: seaweedfs
|
||||
appVersion: "4.47"
|
||||
appVersion: "4.48"
|
||||
# Dev note: Trigger a helm chart release by `git tag -a helm-<version>`
|
||||
version: 4.47.0
|
||||
version: 4.48.0
|
||||
|
||||
@@ -286,7 +286,7 @@ metadata:
|
||||
app.kubernetes.io/component: s3
|
||||
stringData:
|
||||
# this key must be an inline json config file
|
||||
seaweedfs_s3_config: '{"identities":[{"name":"anvAdmin","credentials":[{"accessKey":"snu8yoP6QAlY0ne4","secretKey":"PNzBcmeLNEdR0oviwm04NQAicOrDH1Km"}],"actions":["Admin","Read","Write"]},{"name":"anvReadOnly","credentials":[{"accessKey":"SCigFee6c5lbi04A","secretKey":"kgFhbT38R8WUYVtiFQ1OiSVOrYr3NKku"}],"actions":["Read"]}]}'
|
||||
seaweedfs_s3_config: '{"identities":[{"name":"anvAdmin","credentials":[{"accessKey":"snu8yoP6QAlY0ne4","secretKey":"PNzBcmeLNEdR0oviwm04NQAicOrDH1Km"}],"actions":["Admin","Read","Write"]},{"name":"anvReadOnly","credentials":[{"accessKey":"SCigFee6c5lbi04A","secretKey":"kgFhbT38R8WUYVtiFQ1OiSVOrYr3NKku"}],"actions":["Read","List"]}]}'
|
||||
```
|
||||
|
||||
#### Source S3 credentials from an existing Secret
|
||||
@@ -376,7 +376,10 @@ start on a non-loopback address without `-adminPassword`, so the chart fails at
|
||||
render time if `admin.ip` is non-loopback and authentication is not configured via
|
||||
`admin.secret.adminPassword`, `admin.secret.existingSecret`, or
|
||||
`WEED_ADMIN_PASSWORD` supplied through `admin.extraEnvironmentVars` /
|
||||
`admin.secretExtraEnvironmentVars`. The whole `127.0.0.0/8` range and `::1` are
|
||||
`admin.secretExtraEnvironmentVars`. Setting `admin.allowInsecureBind` renders
|
||||
`-allowInsecureBind` and bypasses this guard; it leaves the admin API
|
||||
unauthenticated on the network, so use it only when access is otherwise
|
||||
restricted (e.g. network policies). The whole `127.0.0.0/8` range and `::1` are
|
||||
treated as loopback (matching `weed admin`); `localhost` is treated as
|
||||
non-loopback. Set `admin.ip` to a loopback address only if you also replace the
|
||||
httpGet probes (e.g. with an `exec` probe that checks `127.0.0.1`).
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{{- $adminAuthEnabled := include "seaweedfs.admin.authEnabled" . }}
|
||||
{{- $adminIp := .Values.admin.ip | default "0.0.0.0" }}
|
||||
{{- if and (not (include "seaweedfs.admin.isLoopbackIp" $adminIp)) (ne $adminAuthEnabled "true") }}
|
||||
{{- fail (printf "admin.ip is set to %q (non-loopback) but admin authentication is not configured. Since `weed admin` 4.46 refuses to bind a non-loopback address without authentication, the admin container would exit on startup. Set admin.secret.adminPassword or admin.secret.existingSecret, or supply WEED_ADMIN_PASSWORD via admin.extraEnvironmentVars / admin.secretExtraEnvironmentVars, or set admin.ip to a loopback address such as 127.0.0.1 (note: a loopback bind makes the chart's httpGet readiness/liveness probes fail)." $adminIp) -}}
|
||||
{{- fail (printf "admin.ip is set to %q (non-loopback) but admin authentication is not configured. Since `weed admin` 4.46 refuses to bind a non-loopback address without authentication, the admin container would exit on startup. Set admin.secret.adminPassword or admin.secret.existingSecret, or supply WEED_ADMIN_PASSWORD via admin.extraEnvironmentVars / admin.secretExtraEnvironmentVars, or set admin.ip to a loopback address such as 127.0.0.1 (note: a loopback bind makes the chart's httpGet readiness/liveness probes fail), or set admin.allowInsecureBind to true to opt out via -allowInsecureBind (INSECURE: exposes the admin API unauthenticated on the network)." $adminIp) -}}
|
||||
{{- end }}
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
@@ -176,14 +176,17 @@ spec:
|
||||
-dataDir={{ .Values.admin.dataDir }} \
|
||||
{{- end }}
|
||||
{{- if .Values.admin.masters }}
|
||||
-masters={{ .Values.admin.masters }}{{- if or $urlPrefix .Values.admin.extraArgs }} \{{ end }}
|
||||
-masters={{ .Values.admin.masters }} \
|
||||
{{- else if .Values.global.seaweedfs.masterServer }}
|
||||
-masters={{ .Values.global.seaweedfs.masterServer }}{{- if or $urlPrefix .Values.admin.extraArgs }} \{{ end }}
|
||||
-masters={{ .Values.global.seaweedfs.masterServer }} \
|
||||
{{- else }}
|
||||
-masters={{ range $index := until (.Values.master.replicas | int) }}${SEAWEEDFS_FULLNAME}-master-{{ $index }}.${SEAWEEDFS_FULLNAME}-master.{{ $.Release.Namespace }}:{{ $.Values.master.port }}{{ if lt $index (sub ($.Values.master.replicas | int) 1) }},{{ end }}{{ end }}{{- if or $urlPrefix .Values.admin.extraArgs }} \{{ end }}
|
||||
-masters={{ range $index := until (.Values.master.replicas | int) }}${SEAWEEDFS_FULLNAME}-master-{{ $index }}.${SEAWEEDFS_FULLNAME}-master.{{ $.Release.Namespace }}:{{ $.Values.master.port }}{{ if lt $index (sub ($.Values.master.replicas | int) 1) }},{{ end }}{{ end }} \
|
||||
{{- end }}
|
||||
{{- if $urlPrefix }}
|
||||
-urlPrefix={{ $urlPrefix }}{{- if .Values.admin.extraArgs }} \{{ end }}
|
||||
-urlPrefix={{ $urlPrefix }} \
|
||||
{{- end }}
|
||||
{{- if .Values.admin.allowInsecureBind }}
|
||||
-allowInsecureBind \
|
||||
{{- end }}
|
||||
{{- range $index, $arg := .Values.admin.extraArgs }}
|
||||
{{ $arg }}{{- if lt $index (sub (len $.Values.admin.extraArgs) 1) }} \{{ end }}
|
||||
|
||||
@@ -37,13 +37,17 @@ spec:
|
||||
{{- with .Values.allInOne.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- $podAnnotations := mergeOverwrite (deepCopy (.Values.podAnnotations | default dict)) (.Values.allInOne.podAnnotations | default dict) }}
|
||||
{{- $existingS3ConfigSecret := or .Values.allInOne.s3.existingConfigSecret .Values.s3.existingConfigSecret .Values.filer.s3.existingConfigSecret }}
|
||||
{{- if $existingS3ConfigSecret }}
|
||||
{{- $configSecret := (lookup "v1" "Secret" .Release.Namespace $existingS3ConfigSecret) | default dict }}
|
||||
{{- $_ := set $podAnnotations "checksum/s3config" ($configSecret | toYaml | sha256sum) }}
|
||||
{{- else }}
|
||||
{{- $_ := set $podAnnotations "checksum/s3config" (include (print .Template.BasePath "/s3/s3-secret.yaml") . | sha256sum) }}
|
||||
{{- end }}
|
||||
{{- $_ := set $podAnnotations "checksum/master-config" (include (print .Template.BasePath "/master/master-configmap.yaml") . | sha256sum) }}
|
||||
annotations:
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.allInOne.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- toYaml $podAnnotations | nindent 8 }}
|
||||
spec:
|
||||
restartPolicy: {{ default .Values.global.seaweedfs.restartPolicy .Values.allInOne.restartPolicy }}
|
||||
{{- if .Values.allInOne.affinity }}
|
||||
|
||||
@@ -25,6 +25,10 @@ spec:
|
||||
organizations:
|
||||
- "SeaweedFS CA"
|
||||
dnsNames:
|
||||
- '{{ include "seaweedfs.fullname" . }}-admin'
|
||||
- '{{ include "seaweedfs.fullname" . }}-admin.{{ .Release.Namespace }}'
|
||||
- '{{ include "seaweedfs.fullname" . }}-admin.{{ .Release.Namespace }}.svc'
|
||||
- '{{ include "seaweedfs.fullname" . }}-admin.{{ .Release.Namespace }}.svc.cluster.local'
|
||||
- '*.{{ include "seaweedfs.fullname" . }}-admin'
|
||||
- '*.{{ include "seaweedfs.fullname" . }}-admin.{{ .Release.Namespace }}'
|
||||
- '*.{{ include "seaweedfs.fullname" . }}-admin.{{ .Release.Namespace }}.svc'
|
||||
|
||||
@@ -43,19 +43,15 @@ spec:
|
||||
{{- with .Values.filer.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.filer.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- $podAnnotations := mergeOverwrite (deepCopy (.Values.podAnnotations | default dict)) (.Values.filer.podAnnotations | default dict) }}
|
||||
{{- if .Values.filer.s3.existingConfigSecret }}
|
||||
{{- $configSecret := (lookup "v1" "Secret" .Release.Namespace .Values.filer.s3.existingConfigSecret) | default dict }}
|
||||
checksum/s3config: {{ $configSecret | toYaml | sha256sum }}
|
||||
{{- $configSecret := (lookup "v1" "Secret" .Release.Namespace .Values.filer.s3.existingConfigSecret) | default dict }}
|
||||
{{- $_ := set $podAnnotations "checksum/s3config" ($configSecret | toYaml | sha256sum) }}
|
||||
{{- else }}
|
||||
checksum/s3config: {{ include (print .Template.BasePath "/s3/s3-secret.yaml") . | sha256sum }}
|
||||
{{- $_ := set $podAnnotations "checksum/s3config" (include (print .Template.BasePath "/s3/s3-secret.yaml") . | sha256sum) }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
{{- toYaml $podAnnotations | nindent 8 }}
|
||||
spec:
|
||||
restartPolicy: {{ default .Values.global.seaweedfs.restartPolicy .Values.filer.restartPolicy }}
|
||||
{{- if .Values.filer.affinity }}
|
||||
|
||||
@@ -43,13 +43,10 @@ spec:
|
||||
{{- with .Values.master.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- $podAnnotations := mergeOverwrite (deepCopy (.Values.podAnnotations | default dict)) (.Values.master.podAnnotations | default dict) }}
|
||||
{{- $_ := set $podAnnotations "checksum/master-config" (include (print .Template.BasePath "/master/master-configmap.yaml") . | sha256sum) }}
|
||||
annotations:
|
||||
{{ with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.master.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- toYaml $podAnnotations | nindent 8 }}
|
||||
spec:
|
||||
restartPolicy: {{ default .Values.global.seaweedfs.restartPolicy .Values.master.restartPolicy }}
|
||||
{{- if .Values.master.affinity }}
|
||||
|
||||
@@ -35,13 +35,15 @@ spec:
|
||||
{{- with .Values.s3.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- $podAnnotations := mergeOverwrite (deepCopy (.Values.podAnnotations | default dict)) (.Values.s3.podAnnotations | default dict) }}
|
||||
{{- if .Values.s3.existingConfigSecret }}
|
||||
{{- $configSecret := (lookup "v1" "Secret" .Release.Namespace .Values.s3.existingConfigSecret) | default dict }}
|
||||
{{- $_ := set $podAnnotations "checksum/s3config" ($configSecret | toYaml | sha256sum) }}
|
||||
{{- else }}
|
||||
{{- $_ := set $podAnnotations "checksum/s3config" (include (print .Template.BasePath "/s3/s3-secret.yaml") . | sha256sum) }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
{{ with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.s3.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- toYaml $podAnnotations | nindent 8 }}
|
||||
spec:
|
||||
restartPolicy: {{ default .Values.global.seaweedfs.restartPolicy .Values.s3.restartPolicy }}
|
||||
{{- if .Values.s3.affinity }}
|
||||
|
||||
@@ -60,7 +60,7 @@ stringData:
|
||||
read_access_key_id: {{ $access_key_read }}
|
||||
read_secret_access_key: {{ $secret_key_read }}
|
||||
{{- end }}
|
||||
seaweedfs_s3_config: '{"identities":[{"name":"anvAdmin","credentials":[{"accessKey":"{{ $access_key_admin }}","secretKey":"{{ $secret_key_admin }}"}],"actions":["Admin","Read","Write"]},{"name":"anvReadOnly","credentials":[{"accessKey":"{{ $access_key_read }}","secretKey":"{{ $secret_key_read }}"}],"actions":["Read"]}]}'
|
||||
seaweedfs_s3_config: '{"identities":[{"name":"anvAdmin","credentials":[{"accessKey":"{{ $access_key_admin }}","secretKey":"{{ $secret_key_admin }}"}],"actions":["Admin","Read","Write"]},{"name":"anvReadOnly","credentials":[{"accessKey":"{{ $access_key_read }}","secretKey":"{{ $secret_key_read }}"}],"actions":["Read","List"]}]}'
|
||||
{{- if .Values.filer.s3.auditLogConfig }}
|
||||
filer_s3_auditLogConfig.json: |
|
||||
{{ toJson .Values.filer.s3.auditLogConfig | nindent 4 }}
|
||||
|
||||
@@ -105,13 +105,13 @@ true
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Whether admin authentication is enabled from any supported source:
|
||||
admin.secret (adminPassword or existingSecret), or WEED_ADMIN_PASSWORD
|
||||
supplied via extraEnvironmentVars / secretExtraEnvironmentVars (which
|
||||
weed admin picks up through viper's AutomaticEnv). A secret-backed
|
||||
entry counts as enabled even though the chart cannot read its value. */}}
|
||||
{{/* Whether the admin non-loopback bind guard is satisfied: admin.secret
|
||||
(adminPassword or existingSecret), WEED_ADMIN_PASSWORD via
|
||||
extraEnvironmentVars / secretExtraEnvironmentVars, or
|
||||
admin.allowInsecureBind. A secret-backed entry counts as enabled even
|
||||
though the chart cannot read its value. */}}
|
||||
{{- define "seaweedfs.admin.authEnabled" -}}
|
||||
{{- if or .Values.admin.secret.existingSecret .Values.admin.secret.adminPassword -}}
|
||||
{{- if or .Values.admin.secret.existingSecret .Values.admin.secret.adminPassword .Values.admin.allowInsecureBind -}}
|
||||
true
|
||||
{{- else -}}
|
||||
{{- $merged := dict -}}
|
||||
|
||||
@@ -190,6 +190,8 @@ master:
|
||||
podLabels: {}
|
||||
|
||||
# Annotations to be added to the master pods
|
||||
# The chart sets checksum/master-config on master pods; other checksum/* keys
|
||||
# can be used for custom rollouts.
|
||||
podAnnotations: {}
|
||||
|
||||
# Annotations to be added to the master resources
|
||||
@@ -773,6 +775,8 @@ filer:
|
||||
podLabels: {}
|
||||
|
||||
# Annotations to be added to the filer pods
|
||||
# The chart sets checksum/s3config on filer pods; other checksum/* keys can be
|
||||
# used for custom rollouts.
|
||||
podAnnotations: {}
|
||||
|
||||
# Annotations to be added to the filer resource
|
||||
@@ -1078,6 +1082,8 @@ s3:
|
||||
podLabels: {}
|
||||
|
||||
# Annotations to be added to the s3 pods
|
||||
# The chart sets checksum/s3config on s3 pods; other checksum/* keys can be
|
||||
# used for custom rollouts.
|
||||
podAnnotations: {}
|
||||
|
||||
# Annotations to be added to the s3 resources
|
||||
@@ -1333,10 +1339,11 @@ admin:
|
||||
# kubelet's httpGet readiness/liveness probes (which dial the pod IP) to ever
|
||||
# succeed. "0.0.0.0" restores the pre-4.46 behaviour of listening on all
|
||||
# interfaces. A non-loopback address requires authentication: set
|
||||
# admin.secret.adminPassword or admin.secret.existingSecret, or supply
|
||||
# admin.secret.adminPassword or admin.secret.existingSecret, supply
|
||||
# WEED_ADMIN_PASSWORD via admin.extraEnvironmentVars /
|
||||
# admin.secretExtraEnvironmentVars; otherwise the admin container will exit
|
||||
# with a clear error rather than silently staying unready. The whole
|
||||
# admin.secretExtraEnvironmentVars, or opt out with admin.allowInsecureBind;
|
||||
# otherwise the admin container will exit with a clear error rather than
|
||||
# silently staying unready. The whole
|
||||
# 127.0.0.0/8 range and ::1 are treated as loopback (matching weed admin).
|
||||
# Set to a loopback address only if you also replace the httpGet probes.
|
||||
# Note: the -ip flag requires SeaweedFS 4.46 or newer; pinning
|
||||
@@ -1344,6 +1351,9 @@ admin:
|
||||
ip: "0.0.0.0"
|
||||
loggingOverrideLevel: null
|
||||
|
||||
# INSECURE: allow binding a non-loopback ip without authentication.
|
||||
allowInsecureBind: false
|
||||
|
||||
# Admin authentication
|
||||
secret:
|
||||
# Name of an existing secret containing admin credentials. If set, adminUser and adminPassword below are ignored.
|
||||
@@ -1788,6 +1798,8 @@ allInOne:
|
||||
initContainers: "" # Init containers
|
||||
sidecars: "" # Sidecar containers
|
||||
annotations: {} # Annotations for the deployment
|
||||
# The chart sets checksum/master-config and checksum/s3config on all-in-one
|
||||
# pods; other checksum/* keys can be used for custom rollouts.
|
||||
podAnnotations: {} # Annotations for the pods
|
||||
podLabels: {} # Labels for the pods
|
||||
|
||||
@@ -1922,6 +1934,9 @@ certificates:
|
||||
# Labels to be added to all the created pods
|
||||
podLabels: {}
|
||||
# Annotations to be added to all the created pods
|
||||
# The chart sets checksum/master-config and checksum/s3config on pods whose
|
||||
# rendered ConfigMaps or Secrets should trigger rollouts. Other checksum/* keys
|
||||
# can be used for custom rollout annotations.
|
||||
podAnnotations: {}
|
||||
|
||||
networkPolicy:
|
||||
|
||||
+1005
-1055
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 53 KiB |
Generated
+293
@@ -0,0 +1,293 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.44.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "seaweed-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "seaweed-common"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
# The lower of the two consumers' floors (seaweed-volume 1.91.1,
|
||||
# seaweed-worker 1.94.1), so depending on this crate cannot raise either
|
||||
# tree's MSRV. Verified with `cargo +1.91.1 check --all-targets`.
|
||||
rust-version = "1.91.1"
|
||||
description = "Helpers shared by the SeaweedFS Rust volume server and the Rust plugin workers"
|
||||
|
||||
# There is no root manifest: seaweed-volume and seaweed-worker are separate
|
||||
# cargo trees with their own lockfiles, and this crate is a path dependency of
|
||||
# both rather than a member of either. Keeping the lint policy identical in all
|
||||
# three manifests is what stops them drifting.
|
||||
[lints.clippy]
|
||||
# Protobuf message literals keep `..Default::default()` on purpose: it is
|
||||
# what lets a proto gain a field without touching every constructor.
|
||||
needless_update = "allow"
|
||||
|
||||
[dependencies]
|
||||
# The same requirement both consumers already write. Cargo unifies all
|
||||
# semver-compatible `rustls = "0.23"` requirements into one crate per binary,
|
||||
# which is what makes `install_default_crypto_provider` write the same
|
||||
# process-wide static the consuming crate reads. rustls is already in both
|
||||
# trees (the volume server directly, seaweed-worker-core through tonic's
|
||||
# `tls-aws-lc`), so this adds no crate to either graph.
|
||||
rustls = "0.23"
|
||||
@@ -0,0 +1,307 @@
|
||||
//! SeaweedFS server addresses, the way the Go tree does them.
|
||||
//!
|
||||
//! An operator gives a SeaweedFS process an HTTP address and the gRPC port is
|
||||
//! derived from it rather than asked for separately: `host:port` means gRPC on
|
||||
//! `port + 10000`, and the explicit `host:port.grpcPort` form names it outright.
|
||||
//! Dialling the HTTP port by mistake fails as "frame with invalid size", which
|
||||
//! reads like a protocol bug rather than a wrong port, so the rule is worth its
|
||||
//! own module. Mirrors `pb.ServerToGrpcAddress` in
|
||||
//! `weed/pb/grpc_client_server.go`.
|
||||
//!
|
||||
//! The volume server and the workers each had their own copy of this and the
|
||||
//! copies had drifted: the worker's bracketed IPv6 literals and the volume
|
||||
//! server's did not, so `::1:19333` produced `::1:29333`, which the HTTP
|
||||
//! authority parser rejects. One implementation, two thin wrappers.
|
||||
|
||||
use std::fmt;
|
||||
use std::num::ParseIntError;
|
||||
|
||||
/// SeaweedFS's HTTP↔gRPC port-offset convention.
|
||||
pub const GRPC_PORT_OFFSET: u16 = 10000;
|
||||
|
||||
/// Why an address could not be turned into a gRPC address.
|
||||
///
|
||||
/// The `Display` text is the volume server's original wording, because its
|
||||
/// `parse_grpc_address` wrapper hands it straight to callers that put it in a
|
||||
/// `Status` or an `io::Error`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum AddressError {
|
||||
/// No `:` at all, so there is no port to translate.
|
||||
MissingPort(String),
|
||||
/// The HTTP port of the `host:port.grpcPort` form is not a `u16`. It is
|
||||
/// validated even though it is then discarded, so that a malformed address
|
||||
/// is rejected here instead of failing later as an opaque connect error.
|
||||
InvalidHttpPort { port: String, source: ParseIntError },
|
||||
/// The gRPC port of the `host:port.grpcPort` form is not a `u16`.
|
||||
InvalidGrpcPort { port: String, source: ParseIntError },
|
||||
/// The port of the `host:port` form is not a `u16`.
|
||||
InvalidPort { port: String, source: ParseIntError },
|
||||
/// `port + GRPC_PORT_OFFSET` leaves the TCP port range, e.g. `host:60000`.
|
||||
/// Without the check the cast would wrap silently.
|
||||
ImplicitGrpcPortOutOfRange(u16),
|
||||
}
|
||||
|
||||
impl fmt::Display for AddressError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingPort(address) => write!(f, "cannot parse address: {address}"),
|
||||
Self::InvalidHttpPort { port, source } => {
|
||||
write!(f, "invalid http port {port:?}: {source}")
|
||||
}
|
||||
Self::InvalidGrpcPort { port, source } => {
|
||||
write!(f, "invalid grpc port {port:?}: {source}")
|
||||
}
|
||||
Self::InvalidPort { port, source } => write!(f, "invalid port {port:?}: {source}"),
|
||||
Self::ImplicitGrpcPortOutOfRange(port) => write!(
|
||||
f,
|
||||
"implicit grpc port out of range: {port} + {GRPC_PORT_OFFSET} = {}",
|
||||
u32::from(*port) + u32::from(GRPC_PORT_OFFSET)
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AddressError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::InvalidHttpPort { source, .. }
|
||||
| Self::InvalidGrpcPort { source, .. }
|
||||
| Self::InvalidPort { source, .. } => Some(source),
|
||||
Self::MissingPort(_) | Self::ImplicitGrpcPortOutOfRange(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn a SeaweedFS server address (`"host:port.grpcPort"` or `"host:port"`)
|
||||
/// into the `host:grpcPort` form the endpoint builders expect.
|
||||
///
|
||||
/// With the trailing `.grpcPort` segment that segment *is* the gRPC port;
|
||||
/// without it the gRPC port is `port + GRPC_PORT_OFFSET`. An unbracketed IPv6
|
||||
/// literal comes back bracketed, because otherwise the port reads as part of
|
||||
/// the address.
|
||||
pub fn to_grpc_address(server: &str) -> Result<String, AddressError> {
|
||||
// rfind, not find: an IPv6 literal is full of colons and the port is after
|
||||
// the last one.
|
||||
let colon_idx = server
|
||||
.rfind(':')
|
||||
.ok_or_else(|| AddressError::MissingPort(server.to_string()))?;
|
||||
let host = &server[..colon_idx];
|
||||
let port_part = &server[colon_idx + 1..];
|
||||
|
||||
// rfind again rather than split_once: the host may be an IPv4 address, and
|
||||
// only the part after the last colon is being split here anyway.
|
||||
if let Some(dot_idx) = port_part.rfind('.') {
|
||||
let http_port = &port_part[..dot_idx];
|
||||
let grpc_port = &port_part[dot_idx + 1..];
|
||||
http_port
|
||||
.parse::<u16>()
|
||||
.map_err(|source| AddressError::InvalidHttpPort {
|
||||
port: http_port.to_string(),
|
||||
source,
|
||||
})?;
|
||||
let grpc_port =
|
||||
grpc_port
|
||||
.parse::<u16>()
|
||||
.map_err(|source| AddressError::InvalidGrpcPort {
|
||||
port: grpc_port.to_string(),
|
||||
source,
|
||||
})?;
|
||||
return Ok(join_host_port(host, grpc_port));
|
||||
}
|
||||
|
||||
let port: u16 = port_part
|
||||
.parse()
|
||||
.map_err(|source| AddressError::InvalidPort {
|
||||
port: port_part.to_string(),
|
||||
source,
|
||||
})?;
|
||||
let grpc_port = port
|
||||
.checked_add(GRPC_PORT_OFFSET)
|
||||
.ok_or(AddressError::ImplicitGrpcPortOutOfRange(port))?;
|
||||
Ok(join_host_port(host, grpc_port))
|
||||
}
|
||||
|
||||
/// Join a host and a port, bracketing an IPv6 literal that is not bracketed
|
||||
/// already. Public because the address rule is not the only place that has to
|
||||
/// put a host and a port back together.
|
||||
pub fn join_host_port(host: &str, port: u16) -> String {
|
||||
// An IPv6 literal has to keep its brackets or the port reads as part of it.
|
||||
if host.contains(':') && !host.starts_with('[') {
|
||||
format!("[{host}]:{port}")
|
||||
} else {
|
||||
format!("{host}:{port}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AddressError, GRPC_PORT_OFFSET, join_host_port, to_grpc_address};
|
||||
|
||||
// ---- the volume server's cases -------------------------------------
|
||||
|
||||
#[test]
|
||||
fn dotted_form_states_the_grpc_port() {
|
||||
assert_eq!(
|
||||
to_grpc_address("127.0.0.1:8080.18080").unwrap(),
|
||||
"127.0.0.1:18080"
|
||||
);
|
||||
assert_eq!(
|
||||
to_grpc_address("192.168.1.66:8080.18080").unwrap(),
|
||||
"192.168.1.66:18080"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_form_adds_the_offset() {
|
||||
assert_eq!(
|
||||
to_grpc_address("127.0.0.1:8080").unwrap(),
|
||||
"127.0.0.1:18080"
|
||||
);
|
||||
assert_eq!(
|
||||
to_grpc_address("192.168.1.66:8080").unwrap(),
|
||||
"192.168.1.66:18080"
|
||||
);
|
||||
assert_eq!(
|
||||
to_grpc_address("localhost:9333").unwrap(),
|
||||
"localhost:19333"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_dotted_grpc_port_comes_back_normalised() {
|
||||
// The volume server's copy validated this segment as a u16 and then
|
||||
// emitted the original text, so a padded or signed port produced an
|
||||
// authority the URI parser rejects. The parsed value is emitted now.
|
||||
assert_eq!(to_grpc_address("host:8080.018080").unwrap(), "host:18080");
|
||||
assert_eq!(to_grpc_address("host:8080.+18080").unwrap(), "host:18080");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ipv4_host_is_not_confused_with_the_dotted_port() {
|
||||
// Regression: a naive split on '.' breaks on IP addresses.
|
||||
assert_eq!(
|
||||
to_grpc_address("10.0.0.1:8080.18080").unwrap(),
|
||||
"10.0.0.1:18080"
|
||||
);
|
||||
assert_eq!(to_grpc_address("10.0.0.1:8080").unwrap(), "10.0.0.1:18080");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_a_non_numeric_http_port_in_the_dotted_form() {
|
||||
let err = to_grpc_address("host:abc.18080").unwrap_err();
|
||||
assert!(
|
||||
matches!(err, AddressError::InvalidHttpPort { .. }),
|
||||
"{err:?}"
|
||||
);
|
||||
assert!(err.to_string().contains("invalid http port"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_a_non_numeric_grpc_port_in_the_dotted_form() {
|
||||
let err = to_grpc_address("host:8080.xyz").unwrap_err();
|
||||
assert!(
|
||||
matches!(err, AddressError::InvalidGrpcPort { .. }),
|
||||
"{err:?}"
|
||||
);
|
||||
assert!(err.to_string().contains("invalid grpc port"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_an_implicit_port_that_leaves_the_tcp_range() {
|
||||
let err = to_grpc_address("127.0.0.1:60000").unwrap_err();
|
||||
assert!(
|
||||
matches!(err, AddressError::ImplicitGrpcPortOutOfRange(60000)),
|
||||
"{err:?}"
|
||||
);
|
||||
assert!(err.to_string().contains("out of range"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_messages_are_the_volume_servers_wording_verbatim() {
|
||||
// parse_grpc_address hands these straight to callers that put them in a
|
||||
// Status or an io::Error, so the whole string is the contract, not just
|
||||
// the substring the older tests match on. Only the two variants whose
|
||||
// text is entirely ours are pinned exactly; the other three end in a
|
||||
// std ParseIntError message, which is std's to reword.
|
||||
assert_eq!(
|
||||
to_grpc_address("127.0.0.1:60000").unwrap_err().to_string(),
|
||||
"implicit grpc port out of range: 60000 + 10000 = 70000"
|
||||
);
|
||||
assert_eq!(
|
||||
to_grpc_address("hostname").unwrap_err().to_string(),
|
||||
"cannot parse address: hostname"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_an_address_without_a_port() {
|
||||
for source in ["hostname", "no-colon", "localhost"] {
|
||||
let err = to_grpc_address(source).unwrap_err();
|
||||
assert!(matches!(err, AddressError::MissingPort(_)), "{err:?}");
|
||||
assert!(err.to_string().contains("cannot parse"), "{err}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the worker's cases --------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn derives_the_grpc_port() {
|
||||
assert_eq!(
|
||||
to_grpc_address("localhost:23646").unwrap(),
|
||||
"localhost:33646"
|
||||
);
|
||||
assert_eq!(
|
||||
to_grpc_address("127.0.0.1:9333").unwrap(),
|
||||
"127.0.0.1:19333"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn honours_an_explicit_grpc_port() {
|
||||
assert_eq!(
|
||||
to_grpc_address("localhost:23646.33999").unwrap(),
|
||||
"localhost:33999"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_what_it_cannot_parse() {
|
||||
let err = to_grpc_address("localhost:notaport").unwrap_err();
|
||||
assert!(matches!(err, AddressError::InvalidPort { .. }), "{err:?}");
|
||||
assert!(err.to_string().contains("invalid port"), "{err}");
|
||||
}
|
||||
|
||||
// ---- IPv6, which only the worker's copy handled --------------------
|
||||
|
||||
#[test]
|
||||
fn brackets_ipv6_literals() {
|
||||
assert_eq!(to_grpc_address("::1:23646").unwrap(), "[::1]:33646");
|
||||
assert_eq!(to_grpc_address("::1:9333").unwrap(), "[::1]:19333");
|
||||
assert_eq!(
|
||||
to_grpc_address("fe80::1:9333.19333").unwrap(),
|
||||
"[fe80::1]:19333"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_an_already_bracketed_literal_alone() {
|
||||
assert_eq!(to_grpc_address("[::1]:9333").unwrap(), "[::1]:19333");
|
||||
assert_eq!(to_grpc_address("[::1]:9333.19333").unwrap(), "[::1]:19333");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_host_port_brackets_only_unbracketed_literals() {
|
||||
assert_eq!(join_host_port("127.0.0.1", 19333), "127.0.0.1:19333");
|
||||
assert_eq!(join_host_port("localhost", 19333), "localhost:19333");
|
||||
assert_eq!(join_host_port("::1", 19333), "[::1]:19333");
|
||||
assert_eq!(join_host_port("[::1]", 19333), "[::1]:19333");
|
||||
}
|
||||
|
||||
// ---- the offset itself ---------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn the_offset_is_the_seaweedfs_convention() {
|
||||
assert_eq!(GRPC_PORT_OFFSET, 10000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Helpers the SeaweedFS Rust volume server and the Rust plugin workers both need.
|
||||
//!
|
||||
//! `seaweed-volume` and `seaweed-worker` are separate cargo trees with separate
|
||||
//! lockfiles and no root manifest, so anything both of them need was, until this
|
||||
//! crate existed, written twice. The two things in here are the ones where a
|
||||
//! second copy is a correctness risk rather than a typing cost: the HTTP↔gRPC
|
||||
//! address rule, which two copies had already drifted on, and the process-wide
|
||||
//! rustls provider, which only works if every binary installs the same one.
|
||||
|
||||
pub mod address;
|
||||
pub mod tls;
|
||||
@@ -0,0 +1,28 @@
|
||||
//! The process-wide rustls crypto provider.
|
||||
//!
|
||||
//! Both binaries link aws-lc-rs and ring transitively — in the volume server
|
||||
//! through the AWS SDK and reqwest, in the lance worker through lance's `aws`
|
||||
//! backend and reqwest — so rustls cannot auto-select a provider and tonic's
|
||||
//! client TLS panics on first use. Each binary has to pin one, and it has to be
|
||||
//! the same one, which is why the choice lives here rather than in either tree.
|
||||
|
||||
use rustls::crypto::aws_lc_rs;
|
||||
|
||||
/// Pin rustls's process-wide default provider to aws-lc-rs, matching the
|
||||
/// volume server's TLS config. Idempotent: the first call wins and every
|
||||
/// later one is a no-op, so callers do not have to coordinate.
|
||||
pub fn install_default_crypto_provider() {
|
||||
let _ = aws_lc_rs::default_provider().install_default();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::install_default_crypto_provider;
|
||||
|
||||
#[test]
|
||||
fn installing_is_idempotent_and_leaves_a_default_behind() {
|
||||
install_default_crypto_provider();
|
||||
install_default_crypto_provider();
|
||||
assert!(rustls::crypto::CryptoProvider::get_default().is_some());
|
||||
}
|
||||
}
|
||||
Generated
+121
-127
@@ -503,7 +503,7 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -628,13 +628,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
@@ -648,14 +648,13 @@ dependencies = [
|
||||
"multer",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -663,19 +662,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.4.5"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"futures-core",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
@@ -1654,19 +1651,13 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http 1.4.0",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
@@ -1860,7 +1851,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.3",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -2027,16 +2018,6 @@ dependencies = [
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.12.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.1"
|
||||
@@ -2250,9 +2231,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
@@ -2619,17 +2600,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
|
||||
dependencies = [
|
||||
"fixedbitset 0.4.2",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.7.1"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
|
||||
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
|
||||
dependencies = [
|
||||
"fixedbitset 0.5.7",
|
||||
"indexmap 2.13.1",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2836,12 +2818,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.13.5"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||
checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive 0.13.5",
|
||||
"prost-derive 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2867,19 +2849,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost-build"
|
||||
version = "0.13.5"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
||||
checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"multimap",
|
||||
"once_cell",
|
||||
"petgraph 0.7.1",
|
||||
"petgraph 0.8.3",
|
||||
"prettyplease",
|
||||
"prost 0.13.5",
|
||||
"prost-types 0.13.5",
|
||||
"prost 0.14.4",
|
||||
"prost-types 0.14.4",
|
||||
"pulldown-cmark",
|
||||
"pulldown-cmark-to-cmark",
|
||||
"regex",
|
||||
"syn",
|
||||
"tempfile",
|
||||
@@ -2900,9 +2883,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.13.5"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||
checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -2922,11 +2905,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.13.5"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
||||
checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a"
|
||||
dependencies = [
|
||||
"prost 0.13.5",
|
||||
"prost 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2993,6 +2976,26 @@ version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3"
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"memchr",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark-to-cmark"
|
||||
version = "22.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab1ad36992cead65f02aa399a373a42730922f1525d988172634fdefdecb8a60"
|
||||
dependencies = [
|
||||
"pulldown-cmark",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.28"
|
||||
@@ -3018,7 +3021,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.6.3",
|
||||
"socket2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -3056,7 +3059,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.6.3",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
@@ -3262,8 +3265,8 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower 0.5.3",
|
||||
"tower-http 0.6.8",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
@@ -3484,6 +3487,13 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "seaweed-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.3.0"
|
||||
@@ -3719,16 +3729,6 @@ version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
@@ -3990,7 +3990,7 @@ dependencies = [
|
||||
"parking_lot 0.12.5",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.6.3",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -4077,7 +4077,7 @@ version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap 2.13.1",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
@@ -4093,11 +4093,10 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "tonic"
|
||||
version = "0.12.3"
|
||||
version = "0.14.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
|
||||
checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"base64",
|
||||
@@ -4111,13 +4110,12 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"prost 0.13.5",
|
||||
"rustls-pemfile",
|
||||
"socket2 0.5.10",
|
||||
"socket2",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"tower 0.4.13",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -4125,49 +4123,55 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tonic-build"
|
||||
version = "0.12.3"
|
||||
version = "0.14.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11"
|
||||
checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322"
|
||||
dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"prost-build 0.13.5",
|
||||
"prost-types 0.13.5",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-reflection"
|
||||
version = "0.12.3"
|
||||
name = "tonic-prost"
|
||||
version = "0.14.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "878d81f52e7fcfd80026b7fdb6a9b578b3c3653ba987f87f0dce4b64043cba27"
|
||||
checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0"
|
||||
dependencies = [
|
||||
"prost 0.13.5",
|
||||
"prost-types 0.13.5",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"bytes",
|
||||
"prost 0.14.4",
|
||||
"tonic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
name = "tonic-prost-build"
|
||||
version = "0.14.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||
checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"indexmap 1.9.3",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"rand 0.8.7",
|
||||
"slab",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"prost-build 0.14.4",
|
||||
"prost-types 0.14.4",
|
||||
"quote",
|
||||
"syn",
|
||||
"tempfile",
|
||||
"tonic-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-reflection"
|
||||
version = "0.14.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acccd136a4bf19810a1fde9c74edc6129b42a66b44d0c1c8aaa67aeb49a146a7"
|
||||
dependencies = [
|
||||
"prost 0.14.4",
|
||||
"prost-types 0.14.4",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
"tonic-prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4178,26 +4182,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"indexmap",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bytes",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"pin-project-lite",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -4216,9 +4206,10 @@ dependencies = [
|
||||
"http-body 1.0.1",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4495,7 +4486,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
@@ -4521,7 +4512,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
@@ -4564,6 +4555,7 @@ dependencies = [
|
||||
"aws-config",
|
||||
"aws-credential-types",
|
||||
"aws-sdk-s3",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-types",
|
||||
"axum",
|
||||
"base64",
|
||||
@@ -4582,7 +4574,6 @@ dependencies = [
|
||||
"image",
|
||||
"jsonwebtoken",
|
||||
"kamadak-exif",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"md-5",
|
||||
"memmap2",
|
||||
@@ -4591,8 +4582,8 @@ dependencies = [
|
||||
"parking_lot 0.12.5",
|
||||
"pprof",
|
||||
"prometheus",
|
||||
"prost 0.13.5",
|
||||
"prost-types 0.13.5",
|
||||
"prost 0.14.4",
|
||||
"prost-types 0.14.4",
|
||||
"protoc-bin-vendored",
|
||||
"rand 0.10.2",
|
||||
"redb",
|
||||
@@ -4601,6 +4592,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rusty-leveldb",
|
||||
"seaweed-common",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
@@ -4613,13 +4605,15 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"toml",
|
||||
"tonic",
|
||||
"tonic-build",
|
||||
"tonic-prost",
|
||||
"tonic-prost-build",
|
||||
"tonic-reflection",
|
||||
"tower 0.4.13",
|
||||
"tower-http 0.5.2",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"x509-parser",
|
||||
"xxhash-rust",
|
||||
]
|
||||
@@ -4966,7 +4960,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
@@ -4997,7 +4991,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.11.0",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -5016,7 +5010,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
|
||||
@@ -23,32 +23,44 @@ default = ["5bytes"]
|
||||
# Pulls redb's experimental_cursor (and therefore experimental-api-5).
|
||||
redb-experimental-cursor = ["redb/experimental_cursor"]
|
||||
|
||||
[lints.clippy]
|
||||
# Protobuf message literals keep `..Default::default()` on purpose: it is
|
||||
# what lets a proto gain a field without touching every constructor.
|
||||
needless_update = "allow"
|
||||
# Every `unsafe` block states its precondition, right above the block.
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
|
||||
[dependencies]
|
||||
# Helpers the Rust plugin workers (seaweed-worker) need as well. A path
|
||||
# dependency because the two trees are separate cargo workspaces with no
|
||||
# common root manifest.
|
||||
seaweed-common = { path = "../seaweed-common" }
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = { version = "0.1", features = ["net"] }
|
||||
tokio-io-timeout = "1"
|
||||
|
||||
# gRPC + protobuf
|
||||
tonic = { version = "0.12", features = ["tls"] }
|
||||
tonic-reflection = "0.12"
|
||||
prost = "0.13"
|
||||
prost-types = "0.13"
|
||||
tonic = { version = "0.14", features = ["tls-aws-lc"] }
|
||||
tonic-prost = "0.14"
|
||||
tonic-reflection = "0.14"
|
||||
prost = "0.14"
|
||||
prost-types = "0.14"
|
||||
|
||||
# HTTP server
|
||||
axum = { version = "0.7", features = ["multipart"] }
|
||||
axum = { version = "0.8", features = ["multipart"] }
|
||||
http-body = "1"
|
||||
hyper = { version = "1", features = ["full"] }
|
||||
hyper-util = { version = "0.1", features = ["tokio", "service", "server-auto", "http1", "http2"] }
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["cors", "trace"] }
|
||||
tower = { version = "0.5", features = ["util"] }
|
||||
tower-http = { version = "0.6", features = ["cors", "trace"] }
|
||||
|
||||
# CLI
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
# Metrics
|
||||
prometheus = { version = "0.13", default-features = false, features = ["process"] }
|
||||
lazy_static = "1"
|
||||
|
||||
# JWT
|
||||
jsonwebtoken = { version = "10", features = ["rust_crypto"] }
|
||||
@@ -138,11 +150,19 @@ aws-types = "1"
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
pprof = { version = "0.15", features = ["prost-codec"] }
|
||||
|
||||
# GetDiskFreeSpaceExW for per-path disk capacity on Windows (0.61.2 already
|
||||
# in the tree via tempfile/mio, so this unifies rather than adding a version).
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = ["Win32_Storage_FileSystem"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
# Already a transitive dependency of aws-sdk-s3 at a single locked version;
|
||||
# needed directly only for the canned HttpClient in remote_storage::s3 tests.
|
||||
aws-smithy-runtime-api = "1"
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.12"
|
||||
tonic-prost-build = "0.14"
|
||||
# Ships protoc with the build so neither CI nor a developer needs a system
|
||||
# install, and so the version is pinned rather than whatever the platform's
|
||||
# package manager happens to carry.
|
||||
|
||||
@@ -12,7 +12,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?);
|
||||
tonic_build::configure()
|
||||
tonic_prost_build::configure()
|
||||
.build_server(true)
|
||||
.build_client(true)
|
||||
// filer.proto uses proto3 optional, which protoc rejects without this
|
||||
|
||||
@@ -168,6 +168,7 @@ message VacuumVolumeCheckRequest {
|
||||
}
|
||||
message VacuumVolumeCheckResponse {
|
||||
double garbage_ratio = 1;
|
||||
bool disk_space_low = 4; // the volume is read-only solely because its disk is low on space — a cause compaction itself reclaims
|
||||
}
|
||||
|
||||
message VacuumVolumeCompactRequest {
|
||||
@@ -259,6 +260,10 @@ message VolumeDeleteRequest {
|
||||
// when true, do not remove the cloud-tier object backing the volume.
|
||||
// used for moves where another server is taking over the same .vif.
|
||||
bool keep_remote_data = 3;
|
||||
// when true, delete only if every needle is deleted: the volume held
|
||||
// data once but nothing is live anymore. Passing either check,
|
||||
// only_empty or this one, is enough to delete.
|
||||
bool only_garbage = 4;
|
||||
}
|
||||
message VolumeDeleteResponse {
|
||||
}
|
||||
@@ -471,6 +476,7 @@ message VolumeEcShardsDeleteRequest {
|
||||
repeated uint32 shard_ids = 3;
|
||||
bool full_teardown = 4; // pre-encode cleanup: wipe every EC artifact + generation for this volume, not just shard_ids
|
||||
int64 encode_ts_ns = 5; // full_teardown generation fence: delete only a disk whose .vif generation is strictly OLDER than this; preserve same-or-newer, generation 0, and an unreadable .vif. 0 => wipe-all (shell pre-encode / pre-upgrade)
|
||||
uint32 delete_generations_older_than = 6; // post-commit cleanup: delete only staged <base>.*.v<N> artifacts with N strictly below this; 0 disables
|
||||
}
|
||||
message VolumeEcShardsDeleteResponse {
|
||||
bool full_teardown_done = 1; // set by a new server that performed full_teardown; absent from an old server lets the caller detect the silent no-op
|
||||
|
||||
+683
-331
File diff suppressed because it is too large
Load Diff
+38
-35
@@ -6,19 +6,22 @@ use seaweed_volume::config::{self, VolumeServerConfig};
|
||||
use seaweed_volume::metrics;
|
||||
use seaweed_volume::pb::volume_server_pb::volume_server_server::VolumeServerServer;
|
||||
use seaweed_volume::security::tls::{
|
||||
build_rustls_server_config, build_rustls_server_config_with_grpc_client_auth,
|
||||
install_default_crypto_provider, GrpcClientAuthPolicy, TlsPolicy,
|
||||
GrpcClientAuthPolicy, TlsPolicy, build_rustls_server_config,
|
||||
build_rustls_server_config_with_grpc_client_auth, install_default_crypto_provider,
|
||||
};
|
||||
use seaweed_volume::security::{Guard, SigningKey};
|
||||
#[cfg(unix)]
|
||||
use seaweed_volume::server::debug::build_debug_router;
|
||||
use seaweed_volume::server::grpc_client::load_outgoing_grpc_tls;
|
||||
use seaweed_volume::server::grpc_client::{
|
||||
GRPC_INITIAL_WINDOW_SIZE, GRPC_KEEPALIVE_INTERVAL, GRPC_KEEPALIVE_TIMEOUT,
|
||||
GRPC_MAX_MESSAGE_SIZE, load_outgoing_grpc_tls,
|
||||
};
|
||||
use seaweed_volume::server::grpc_server::VolumeGrpcService;
|
||||
#[cfg(unix)]
|
||||
use seaweed_volume::server::profiling::CpuProfileSession;
|
||||
use seaweed_volume::server::request_id::GrpcRequestIdLayer;
|
||||
use seaweed_volume::server::volume_server::{
|
||||
build_metrics_router, RuntimeMetricsConfig, VolumeServerState,
|
||||
RuntimeMetricsConfig, VolumeServerState, build_metrics_router,
|
||||
};
|
||||
use seaweed_volume::server::write_queue::WriteQueue;
|
||||
use seaweed_volume::storage::store::Store;
|
||||
@@ -31,10 +34,10 @@ type CpuProfileParam = Option<CpuProfileSession>;
|
||||
#[cfg(not(unix))]
|
||||
type CpuProfileParam = Option<()>;
|
||||
|
||||
const GRPC_MAX_MESSAGE_SIZE: usize = 1 << 30;
|
||||
const GRPC_KEEPALIVE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(60);
|
||||
const GRPC_KEEPALIVE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(20);
|
||||
const GRPC_INITIAL_WINDOW_SIZE: u32 = 16 * 1024 * 1024;
|
||||
// The two settings that only make sense for the inbound server. The rest of
|
||||
// this server's HTTP/2 tuning — keepalive, window sizes, message size — is
|
||||
// imported from `server::grpc_client` above, which is also what the outgoing
|
||||
// clients dial with, so the two directions cannot drift apart.
|
||||
const GRPC_MAX_HEADER_LIST_SIZE: u32 = 8 * 1024 * 1024;
|
||||
const GRPC_MAX_CONCURRENT_STREAMS: u32 = 1000;
|
||||
|
||||
@@ -343,9 +346,6 @@ async fn run(
|
||||
pre_stop_seconds: config.pre_stop_seconds,
|
||||
volume_state_notify: tokio::sync::Notify::new(),
|
||||
write_queue: std::sync::OnceLock::new(),
|
||||
s3_tier_registry: std::sync::RwLock::new(
|
||||
seaweed_volume::remote_storage::s3_tier::S3TierRegistry::new(),
|
||||
),
|
||||
read_mode: config.read_mode,
|
||||
allow_untrusted_remote_endpoints: config.allow_untrusted_remote_endpoints,
|
||||
master_url,
|
||||
@@ -671,8 +671,7 @@ async fn run(
|
||||
})
|
||||
.await
|
||||
} else {
|
||||
let incoming =
|
||||
tokio_stream::wrappers::TcpListenerStream::new(grpc_listener);
|
||||
let incoming = tokio_stream::wrappers::TcpListenerStream::new(grpc_listener);
|
||||
info!("gRPC server listening on {}", grpc_local_addr);
|
||||
build_grpc_server_builder()
|
||||
.layer(GrpcRequestIdLayer)
|
||||
@@ -1058,15 +1057,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_grpc_server_tls_returns_none_when_files_are_missing() {
|
||||
assert!(build_grpc_server_tls_acceptor(
|
||||
"/missing/server.crt",
|
||||
"/missing/server.key",
|
||||
"/missing/ca.crt",
|
||||
&TlsPolicy::default(),
|
||||
"",
|
||||
&[],
|
||||
)
|
||||
.is_none());
|
||||
assert!(
|
||||
build_grpc_server_tls_acceptor(
|
||||
"/missing/server.crt",
|
||||
"/missing/server.key",
|
||||
"/missing/ca.crt",
|
||||
&TlsPolicy::default(),
|
||||
"",
|
||||
&[],
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1088,19 +1089,21 @@ mod tests {
|
||||
"-----BEGIN CERTIFICATE-----\nZmFrZQ==\n-----END CERTIFICATE-----\n",
|
||||
);
|
||||
|
||||
assert!(build_grpc_server_tls_acceptor(
|
||||
&cert,
|
||||
&key,
|
||||
&ca,
|
||||
&TlsPolicy {
|
||||
min_version: "TLS 1.0".to_string(),
|
||||
max_version: "TLS 1.1".to_string(),
|
||||
cipher_suites: String::new(),
|
||||
},
|
||||
"",
|
||||
&[],
|
||||
)
|
||||
.is_none());
|
||||
assert!(
|
||||
build_grpc_server_tls_acceptor(
|
||||
&cert,
|
||||
&key,
|
||||
&ca,
|
||||
&TlsPolicy {
|
||||
min_version: "TLS 1.0".to_string(),
|
||||
max_version: "TLS 1.1".to_string(),
|
||||
cipher_suites: String::new(),
|
||||
},
|
||||
"",
|
||||
&[],
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+225
-124
@@ -6,7 +6,7 @@ use prometheus::{
|
||||
self, Encoder, GaugeVec, HistogramOpts, HistogramVec, IntCounter, IntCounterVec, IntGauge,
|
||||
IntGaugeVec, Opts, Registry, TextEncoder,
|
||||
};
|
||||
use std::sync::Once;
|
||||
use std::sync::{LazyLock, Once};
|
||||
|
||||
use crate::version;
|
||||
|
||||
@@ -16,220 +16,320 @@ pub struct PushGatewayConfig {
|
||||
pub interval_seconds: u32,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref REGISTRY: Registry = Registry::new();
|
||||
pub static REGISTRY: LazyLock<Registry> = LazyLock::new(Registry::new);
|
||||
|
||||
// ---- Request metrics (Go: VolumeServerRequestCounter, VolumeServerRequestHistogram) ----
|
||||
// ---- Request metrics (Go: VolumeServerRequestCounter, VolumeServerRequestHistogram) ----
|
||||
|
||||
/// Request counter with labels `type` (HTTP method) and `code` (HTTP status).
|
||||
pub static ref REQUEST_COUNTER: IntCounterVec = IntCounterVec::new(
|
||||
Opts::new("SeaweedFS_volumeServer_request_total", "Volume server requests"),
|
||||
/// Request counter with labels `type` (HTTP method) and `code` (HTTP status).
|
||||
pub static REQUEST_COUNTER: LazyLock<IntCounterVec> = LazyLock::new(|| {
|
||||
IntCounterVec::new(
|
||||
Opts::new(
|
||||
"SeaweedFS_volumeServer_request_total",
|
||||
"Volume server requests",
|
||||
),
|
||||
&["type", "code"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Request duration histogram with label `type` (HTTP method).
|
||||
pub static ref REQUEST_DURATION: HistogramVec = HistogramVec::new(
|
||||
/// Request duration histogram with label `type` (HTTP method).
|
||||
pub static REQUEST_DURATION: LazyLock<HistogramVec> = LazyLock::new(|| {
|
||||
HistogramVec::new(
|
||||
HistogramOpts::new(
|
||||
"SeaweedFS_volumeServer_request_seconds",
|
||||
"Volume server request duration in seconds",
|
||||
).buckets(exponential_buckets(0.0001, 2.0, 24)),
|
||||
)
|
||||
.buckets(exponential_buckets(0.0001, 2.0, 24)),
|
||||
&["type"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
// ---- Handler counters (Go: VolumeServerHandlerCounter) ----
|
||||
// ---- Handler counters (Go: VolumeServerHandlerCounter) ----
|
||||
|
||||
/// Handler-level operation counter with label `type`.
|
||||
pub static ref HANDLER_COUNTER: IntCounterVec = IntCounterVec::new(
|
||||
Opts::new("SeaweedFS_volumeServer_handler_total", "Volume server handler counters"),
|
||||
/// Handler-level operation counter with label `type`.
|
||||
pub static HANDLER_COUNTER: LazyLock<IntCounterVec> = LazyLock::new(|| {
|
||||
IntCounterVec::new(
|
||||
Opts::new(
|
||||
"SeaweedFS_volumeServer_handler_total",
|
||||
"Volume server handler counters",
|
||||
),
|
||||
&["type"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
// ---- Vacuuming metrics (Go: VolumeServerVacuuming*) ----
|
||||
// ---- Vacuuming metrics (Go: VolumeServerVacuuming*) ----
|
||||
|
||||
/// Vacuuming compact counter with label `success` (true/false).
|
||||
pub static ref VACUUMING_COMPACT_COUNTER: IntCounterVec = IntCounterVec::new(
|
||||
Opts::new("SeaweedFS_volumeServer_vacuuming_compact_count", "Counter of volume vacuuming Compact counter"),
|
||||
/// Vacuuming compact counter with label `success` (true/false).
|
||||
pub static VACUUMING_COMPACT_COUNTER: LazyLock<IntCounterVec> = LazyLock::new(|| {
|
||||
IntCounterVec::new(
|
||||
Opts::new(
|
||||
"SeaweedFS_volumeServer_vacuuming_compact_count",
|
||||
"Counter of volume vacuuming Compact counter",
|
||||
),
|
||||
&["success"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Vacuuming commit counter with label `success` (true/false).
|
||||
pub static ref VACUUMING_COMMIT_COUNTER: IntCounterVec = IntCounterVec::new(
|
||||
Opts::new("SeaweedFS_volumeServer_vacuuming_commit_count", "Counter of volume vacuuming commit counter"),
|
||||
/// Vacuuming commit counter with label `success` (true/false).
|
||||
pub static VACUUMING_COMMIT_COUNTER: LazyLock<IntCounterVec> = LazyLock::new(|| {
|
||||
IntCounterVec::new(
|
||||
Opts::new(
|
||||
"SeaweedFS_volumeServer_vacuuming_commit_count",
|
||||
"Counter of volume vacuuming commit counter",
|
||||
),
|
||||
&["success"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Vacuuming duration histogram with label `type` (compact/commit).
|
||||
pub static ref VACUUMING_HISTOGRAM: HistogramVec = HistogramVec::new(
|
||||
/// Vacuuming duration histogram with label `type` (compact/commit).
|
||||
pub static VACUUMING_HISTOGRAM: LazyLock<HistogramVec> = LazyLock::new(|| {
|
||||
HistogramVec::new(
|
||||
HistogramOpts::new(
|
||||
"SeaweedFS_volumeServer_vacuuming_seconds",
|
||||
"Volume vacuuming duration in seconds",
|
||||
).buckets(exponential_buckets(0.0001, 2.0, 24)),
|
||||
)
|
||||
.buckets(exponential_buckets(0.0001, 2.0, 24)),
|
||||
&["type"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
// ---- Volume gauges (Go: VolumeServerVolumeGauge, VolumeServerReadOnlyVolumeGauge) ----
|
||||
// ---- Volume gauges (Go: VolumeServerVolumeGauge, VolumeServerReadOnlyVolumeGauge) ----
|
||||
|
||||
/// Volumes per collection and type (volume/ec_shards).
|
||||
pub static ref VOLUME_GAUGE: GaugeVec = GaugeVec::new(
|
||||
/// Volumes per collection and type (volume/ec_shards).
|
||||
pub static VOLUME_GAUGE: LazyLock<GaugeVec> = LazyLock::new(|| {
|
||||
GaugeVec::new(
|
||||
Opts::new("SeaweedFS_volumeServer_volumes", "Number of volumes"),
|
||||
&["collection", "type"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Read-only volumes per collection and type.
|
||||
pub static ref READ_ONLY_VOLUME_GAUGE: GaugeVec = GaugeVec::new(
|
||||
Opts::new("SeaweedFS_volumeServer_read_only_volumes", "Number of read-only volumes."),
|
||||
/// Read-only volumes per collection and type.
|
||||
pub static READ_ONLY_VOLUME_GAUGE: LazyLock<GaugeVec> = LazyLock::new(|| {
|
||||
GaugeVec::new(
|
||||
Opts::new(
|
||||
"SeaweedFS_volumeServer_read_only_volumes",
|
||||
"Number of read-only volumes.",
|
||||
),
|
||||
&["collection", "type"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Maximum number of volumes this server can hold.
|
||||
pub static ref MAX_VOLUMES: IntGauge = IntGauge::new(
|
||||
/// Maximum number of volumes this server can hold.
|
||||
pub static MAX_VOLUMES: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
IntGauge::new(
|
||||
"SeaweedFS_volumeServer_max_volumes",
|
||||
"Maximum number of volumes",
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
// ---- Disk size gauges (Go: VolumeServerDiskSizeGauge) ----
|
||||
// ---- Disk size gauges (Go: VolumeServerDiskSizeGauge) ----
|
||||
|
||||
/// Actual disk size used by volumes per collection and type (normal/deleted_bytes/ec).
|
||||
pub static ref DISK_SIZE_GAUGE: GaugeVec = GaugeVec::new(
|
||||
Opts::new("SeaweedFS_volumeServer_total_disk_size", "Actual disk size used by volumes"),
|
||||
/// Actual disk size used by volumes per collection and type (normal/deleted_bytes/ec).
|
||||
pub static DISK_SIZE_GAUGE: LazyLock<GaugeVec> = LazyLock::new(|| {
|
||||
GaugeVec::new(
|
||||
Opts::new(
|
||||
"SeaweedFS_volumeServer_total_disk_size",
|
||||
"Actual disk size used by volumes",
|
||||
),
|
||||
&["collection", "type"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
// ---- Resource gauges (Go: VolumeServerResourceGauge) ----
|
||||
// ---- Resource gauges (Go: VolumeServerResourceGauge) ----
|
||||
|
||||
/// Disk resource usage per directory and type (all/used/free/avail).
|
||||
pub static ref RESOURCE_GAUGE: GaugeVec = GaugeVec::new(
|
||||
/// Disk resource usage per directory and type (all/used/free/avail).
|
||||
pub static RESOURCE_GAUGE: LazyLock<GaugeVec> = LazyLock::new(|| {
|
||||
GaugeVec::new(
|
||||
Opts::new("SeaweedFS_volumeServer_resource", "Server resource usage"),
|
||||
&["name", "type"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
// ---- In-flight gauges (Go: VolumeServerInFlightRequestsGauge, InFlightDownload/UploadSize) ----
|
||||
// ---- In-flight gauges (Go: VolumeServerInFlightRequestsGauge, InFlightDownload/UploadSize) ----
|
||||
|
||||
/// In-flight requests per HTTP method.
|
||||
pub static ref INFLIGHT_REQUESTS_GAUGE: IntGaugeVec = IntGaugeVec::new(
|
||||
Opts::new("SeaweedFS_volumeServer_in_flight_requests", "Current number of in-flight requests being handled by volume server."),
|
||||
/// In-flight requests per HTTP method.
|
||||
pub static INFLIGHT_REQUESTS_GAUGE: LazyLock<IntGaugeVec> = LazyLock::new(|| {
|
||||
IntGaugeVec::new(
|
||||
Opts::new(
|
||||
"SeaweedFS_volumeServer_in_flight_requests",
|
||||
"Current number of in-flight requests being handled by volume server.",
|
||||
),
|
||||
&["type"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Concurrent download limit in bytes.
|
||||
pub static ref CONCURRENT_DOWNLOAD_LIMIT: IntGauge = IntGauge::new(
|
||||
/// Concurrent download limit in bytes.
|
||||
pub static CONCURRENT_DOWNLOAD_LIMIT: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
IntGauge::new(
|
||||
"SeaweedFS_volumeServer_concurrent_download_limit",
|
||||
"Limit for total concurrent download size in bytes",
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Concurrent upload limit in bytes.
|
||||
pub static ref CONCURRENT_UPLOAD_LIMIT: IntGauge = IntGauge::new(
|
||||
/// Concurrent upload limit in bytes.
|
||||
pub static CONCURRENT_UPLOAD_LIMIT: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
IntGauge::new(
|
||||
"SeaweedFS_volumeServer_concurrent_upload_limit",
|
||||
"Limit for total concurrent upload size in bytes",
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Current in-flight download bytes.
|
||||
pub static ref INFLIGHT_DOWNLOAD_SIZE: IntGauge = IntGauge::new(
|
||||
/// Current in-flight download bytes.
|
||||
pub static INFLIGHT_DOWNLOAD_SIZE: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
IntGauge::new(
|
||||
"SeaweedFS_volumeServer_in_flight_download_size",
|
||||
"In flight total download size.",
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Current in-flight upload bytes.
|
||||
pub static ref INFLIGHT_UPLOAD_SIZE: IntGauge = IntGauge::new(
|
||||
/// Current in-flight upload bytes.
|
||||
pub static INFLIGHT_UPLOAD_SIZE: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
IntGauge::new(
|
||||
"SeaweedFS_volumeServer_in_flight_upload_size",
|
||||
"In flight total upload size.",
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Upload error counter by HTTP status code. Code "0" = transport error (no response).
|
||||
pub static ref UPLOAD_ERROR_COUNTER: IntCounterVec = IntCounterVec::new(
|
||||
Opts::new("SeaweedFS_upload_error_total",
|
||||
"Counter of upload errors by HTTP status code. Code 0 means transport error (no response received)."),
|
||||
&["code"],
|
||||
).expect("metric can be created");
|
||||
/// Upload error counter by HTTP status code. Code "0" = transport error (no response).
|
||||
pub static UPLOAD_ERROR_COUNTER: LazyLock<IntCounterVec> = LazyLock::new(|| {
|
||||
IntCounterVec::new(
|
||||
Opts::new("SeaweedFS_upload_error_total",
|
||||
"Counter of upload errors by HTTP status code. Code 0 means transport error (no response received)."),
|
||||
&["code"],
|
||||
).expect("metric can be created")
|
||||
});
|
||||
|
||||
// ---- Scrubbing metrics (Go: VolumeServerScrub*) ----
|
||||
// ---- Scrubbing metrics (Go: VolumeServerScrub*) ----
|
||||
|
||||
/// Last scrub execution time, as seconds since UNIX epoch, with label `mode`.
|
||||
pub static ref SCRUB_LAST_TIME_SECONDS: GaugeVec = GaugeVec::new(
|
||||
/// Last scrub execution time, as seconds since UNIX epoch, with label `mode`.
|
||||
pub static SCRUB_LAST_TIME_SECONDS: LazyLock<GaugeVec> = LazyLock::new(|| {
|
||||
GaugeVec::new(
|
||||
Opts::new(
|
||||
"SeaweedFS_volumeServer_scrub_last_time_seconds",
|
||||
"Last scrub execution time, as seconds since UNIX epoch.",
|
||||
),
|
||||
&["mode"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Counter of overall volumes with issues detected during scrubbing, with label `mode`.
|
||||
pub static ref SCRUB_VOLUME_FAILURES: IntCounterVec = IntCounterVec::new(
|
||||
/// Counter of overall volumes with issues detected during scrubbing, with label `mode`.
|
||||
pub static SCRUB_VOLUME_FAILURES: LazyLock<IntCounterVec> = LazyLock::new(|| {
|
||||
IntCounterVec::new(
|
||||
Opts::new(
|
||||
"SeaweedFS_volumeServer_scrub_volume_failures",
|
||||
"Counter of overall volumes with issues detected during scrubbing.",
|
||||
),
|
||||
&["mode"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Counter of overall EC shards with issues detected during scrubbing, with label `mode`.
|
||||
pub static ref SCRUB_SHARD_FAILURES: IntCounterVec = IntCounterVec::new(
|
||||
/// Counter of overall EC shards with issues detected during scrubbing, with label `mode`.
|
||||
pub static SCRUB_SHARD_FAILURES: LazyLock<IntCounterVec> = LazyLock::new(|| {
|
||||
IntCounterVec::new(
|
||||
Opts::new(
|
||||
"SeaweedFS_volumeServer_scrub_shard_failures",
|
||||
"Counter of overall EC shards with issues detected during scrubbing.",
|
||||
),
|
||||
&["mode"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Counter of storage read/write EIO errors on volumes and EC shards.
|
||||
/// Mirrors Go's VolumeServerStorageIoErrorCounter.
|
||||
pub static ref STORAGE_IO_ERROR_COUNTER: IntCounter = IntCounter::new(
|
||||
/// Counter of storage read/write EIO errors on volumes and EC shards.
|
||||
/// Mirrors Go's VolumeServerStorageIoErrorCounter.
|
||||
pub static STORAGE_IO_ERROR_COUNTER: LazyLock<IntCounter> = LazyLock::new(|| {
|
||||
IntCounter::new(
|
||||
"SeaweedFS_volumeServer_storage_io_error_total",
|
||||
"Counter of storage read/write EIO errors on volumes and EC shards.",
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Number of volumes quarantined due to storage IO errors.
|
||||
/// Mirrors Go's VolumeServerIoQuarantineGauge.
|
||||
pub static ref IO_QUARANTINE_GAUGE: IntGaugeVec = IntGaugeVec::new(
|
||||
/// Number of volumes quarantined due to storage IO errors.
|
||||
/// Mirrors Go's VolumeServerIoQuarantineGauge.
|
||||
pub static IO_QUARANTINE_GAUGE: LazyLock<IntGaugeVec> = LazyLock::new(|| {
|
||||
IntGaugeVec::new(
|
||||
Opts::new(
|
||||
"SeaweedFS_volumeServer_io_quarantine",
|
||||
"Number of volumes or EC shards quarantined due to storage IO errors.",
|
||||
),
|
||||
&["kind"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
// ---- Legacy aliases for backward compat with existing code ----
|
||||
// ---- Legacy aliases for backward compat with existing code ----
|
||||
|
||||
/// Total number of volumes on this server (flat gauge).
|
||||
pub static ref VOLUMES_TOTAL: IntGauge = IntGauge::new(
|
||||
"volume_server_volumes_total",
|
||||
"Total number of volumes",
|
||||
).expect("metric can be created");
|
||||
/// Total number of volumes on this server (flat gauge).
|
||||
pub static VOLUMES_TOTAL: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
IntGauge::new("volume_server_volumes_total", "Total number of volumes")
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Disk size in bytes per directory.
|
||||
pub static ref DISK_SIZE_BYTES: IntGaugeVec = IntGaugeVec::new(
|
||||
/// Disk size in bytes per directory.
|
||||
pub static DISK_SIZE_BYTES: LazyLock<IntGaugeVec> = LazyLock::new(|| {
|
||||
IntGaugeVec::new(
|
||||
Opts::new("volume_server_disk_size_bytes", "Disk size in bytes"),
|
||||
&["dir"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Disk free bytes per directory.
|
||||
pub static ref DISK_FREE_BYTES: IntGaugeVec = IntGaugeVec::new(
|
||||
/// Disk free bytes per directory.
|
||||
pub static DISK_FREE_BYTES: LazyLock<IntGaugeVec> = LazyLock::new(|| {
|
||||
IntGaugeVec::new(
|
||||
Opts::new("volume_server_disk_free_bytes", "Disk free space in bytes"),
|
||||
&["dir"],
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Current number of in-flight requests (flat gauge).
|
||||
pub static ref INFLIGHT_REQUESTS: IntGauge = IntGauge::new(
|
||||
/// Current number of in-flight requests (flat gauge).
|
||||
pub static INFLIGHT_REQUESTS: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
IntGauge::new(
|
||||
"volume_server_inflight_requests",
|
||||
"Current number of in-flight requests",
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Total number of files stored across all volumes.
|
||||
pub static ref VOLUME_FILE_COUNT: IntGauge = IntGauge::new(
|
||||
/// Total number of files stored across all volumes.
|
||||
pub static VOLUME_FILE_COUNT: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
IntGauge::new(
|
||||
"volume_server_volume_file_count",
|
||||
"Total number of files stored across all volumes",
|
||||
).expect("metric can be created");
|
||||
)
|
||||
.expect("metric can be created")
|
||||
});
|
||||
|
||||
// ---- Build info (Go: BuildInfo) ----
|
||||
// ---- Build info (Go: BuildInfo) ----
|
||||
|
||||
/// Build information gauge, always set to 1. Matches Go:
|
||||
/// Namespace="SeaweedFS", Subsystem="build", Name="info",
|
||||
/// labels: version, commit, sizelimit, goos, goarch.
|
||||
pub static ref BUILD_INFO: GaugeVec = GaugeVec::new(
|
||||
Opts::new("SeaweedFS_build_info", "A metric with a constant '1' value labeled by version, commit, sizelimit, goos, and goarch from which SeaweedFS was built."),
|
||||
&["version", "commit", "sizelimit", "goos", "goarch"],
|
||||
).expect("metric can be created");
|
||||
}
|
||||
/// Build information gauge, always set to 1. Matches Go:
|
||||
/// Namespace="SeaweedFS", Subsystem="build", Name="info",
|
||||
/// labels: version, commit, sizelimit, goos, goarch.
|
||||
pub static BUILD_INFO: LazyLock<GaugeVec> = LazyLock::new(|| {
|
||||
GaugeVec::new(
|
||||
Opts::new("SeaweedFS_build_info", "A metric with a constant '1' value labeled by version, commit, sizelimit, goos, and goarch from which SeaweedFS was built."),
|
||||
&["version", "commit", "sizelimit", "goos", "goarch"],
|
||||
).expect("metric can be created")
|
||||
});
|
||||
|
||||
/// Generate exponential bucket boundaries for histograms.
|
||||
fn exponential_buckets(start: f64, factor: f64, count: usize) -> Vec<f64> {
|
||||
@@ -249,6 +349,7 @@ pub const DOWNLOAD_LIMIT_COND: &str = "downloadLimitCondition";
|
||||
pub const UPLOAD_LIMIT_COND: &str = "uploadLimitCondition";
|
||||
pub const READ_PROXY_REQ: &str = "readProxyRequest";
|
||||
pub const READ_REDIRECT_REQ: &str = "readRedirectRequest";
|
||||
pub const READ_DELETED_NEEDLE: &str = "readDeletedNeedle";
|
||||
pub const EMPTY_READ_PROXY_LOC: &str = "emptyReadProxyLocaction";
|
||||
pub const FAILED_READ_PROXY_REQ: &str = "failedReadProxyRequest";
|
||||
|
||||
@@ -377,10 +478,8 @@ fn delete_partial_match_collection(gauge: &GaugeVec, collection: &str) {
|
||||
type_value = Some(label.get_value().to_string());
|
||||
}
|
||||
}
|
||||
if matches_collection {
|
||||
if let Some(ref tv) = type_value {
|
||||
let _ = gauge.remove_label_values(&[collection, tv]);
|
||||
}
|
||||
if matches_collection && let Some(ref tv) = type_value {
|
||||
let _ = gauge.remove_label_values(&[collection, tv]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -427,7 +526,7 @@ pub async fn push_metrics_once(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::{routing::put, Router};
|
||||
use axum::{Router, routing::put};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[test]
|
||||
@@ -499,7 +598,9 @@ mod tests {
|
||||
register_metrics();
|
||||
|
||||
VOLUME_GAUGE.with_label_values(&["pics", "volume"]).set(2.0);
|
||||
VOLUME_GAUGE.with_label_values(&["pics", "ec_shards"]).set(3.0);
|
||||
VOLUME_GAUGE
|
||||
.with_label_values(&["pics", "ec_shards"])
|
||||
.set(3.0);
|
||||
READ_ONLY_VOLUME_GAUGE
|
||||
.with_label_values(&["pics", "volume"])
|
||||
.set(1.0);
|
||||
|
||||
@@ -119,7 +119,11 @@ pub fn check_blocked_ip(endpoint: &str, ip: IpAddr) -> Result<(), String> {
|
||||
/// reachable for callers whose target legitimately sits on an internal network
|
||||
/// (peer volume servers), while still blocking loopback, link-local (IMDS) and
|
||||
/// unspecified. Mirrors Go's `checkBlockedIPPolicy`.
|
||||
pub fn check_blocked_ip_policy(endpoint: &str, ip: IpAddr, allow_private: bool) -> Result<(), String> {
|
||||
pub fn check_blocked_ip_policy(
|
||||
endpoint: &str,
|
||||
ip: IpAddr,
|
||||
allow_private: bool,
|
||||
) -> Result<(), String> {
|
||||
// Normalize IPv4-mapped IPv6 (`::ffff:a.b.c.d`) to its IPv4 form so the
|
||||
// IPv4 deny rules apply. The OS routes these to the embedded IPv4 address,
|
||||
// so without this `::ffff:127.0.0.1` / `::ffff:169.254.169.254` would slip
|
||||
@@ -173,10 +177,10 @@ pub fn check_blocked_ip_policy(endpoint: &str, ip: IpAddr, allow_private: bool)
|
||||
// same host wherever the matching relay exists (common in IPv6-only cloud).
|
||||
// to_ipv4_mapped above only covers ::ffff: mapped addresses, so pull the
|
||||
// embedded IPv4 out of the other forms and re-check it against the rules.
|
||||
if let IpAddr::V6(v6) = ip {
|
||||
if let Some(v4) = embedded_transition_ipv4(v6) {
|
||||
return check_blocked_ip_policy(endpoint, IpAddr::V4(v4), allow_private);
|
||||
}
|
||||
if let IpAddr::V6(v6) = ip
|
||||
&& let Some(v4) = embedded_transition_ipv4(v6)
|
||||
{
|
||||
return check_blocked_ip_policy(endpoint, IpAddr::V4(v4), allow_private);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -214,9 +218,7 @@ fn precheck_endpoint(endpoint: &str) -> Result<HostCheck, String> {
|
||||
|
||||
// Authority is everything up to the first '/', '?', or '#'.
|
||||
let after = &trimmed[scheme_end + 3..];
|
||||
let authority_end = after
|
||||
.find(|c| c == '/' || c == '?' || c == '#')
|
||||
.unwrap_or(after.len());
|
||||
let authority_end = after.find(['/', '?', '#']).unwrap_or(after.len());
|
||||
let authority = &after[..authority_end];
|
||||
|
||||
// Strip optional userinfo ("user:pass@").
|
||||
@@ -233,7 +235,7 @@ fn precheck_endpoint(endpoint: &str) -> Result<HostCheck, String> {
|
||||
return Err(format!(
|
||||
"remote endpoint {:?} has a malformed IPv6 host",
|
||||
endpoint
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -309,7 +311,10 @@ pub async fn validate_replica_target(target: &str) -> Result<(), String> {
|
||||
return Err("replica target is empty".to_string());
|
||||
}
|
||||
if trimmed.contains("://") || trimmed.contains(['/', '?', '#', '@', '\\']) {
|
||||
return Err(format!("replica target {:?} must be a bare host:port", target));
|
||||
return Err(format!(
|
||||
"replica target {:?} must be a bare host:port",
|
||||
target
|
||||
));
|
||||
}
|
||||
|
||||
// Require an explicit host:port, handling `[IPv6]:port`. A bracketless IPv6
|
||||
@@ -318,12 +323,22 @@ pub async fn validate_replica_target(target: &str) -> Result<(), String> {
|
||||
let host = if let Some(rest) = trimmed.strip_prefix('[') {
|
||||
match rest.split_once(']') {
|
||||
Some((h, port)) if port.starts_with(':') && port.len() > 1 => h,
|
||||
_ => return Err(format!("replica target {:?} must be a bare host:port", target)),
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"replica target {:?} must be a bare host:port",
|
||||
target
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match trimmed.rsplit_once(':') {
|
||||
Some((h, port)) if !port.is_empty() && !h.contains(':') => h,
|
||||
_ => return Err(format!("replica target {:?} must be a bare host:port", target)),
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"replica target {:?} must be a bare host:port",
|
||||
target
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -342,7 +357,10 @@ pub async fn validate_replica_target(target: &str) -> Result<(), String> {
|
||||
|
||||
let addrs = resolve_host(host).await?;
|
||||
if addrs.is_empty() {
|
||||
return Err(format!("resolve replica target host {:?}: no addresses", host));
|
||||
return Err(format!(
|
||||
"resolve replica target host {:?}: no addresses",
|
||||
host
|
||||
));
|
||||
}
|
||||
for ip in addrs {
|
||||
check_blocked_ip_policy(target, ip, true)?;
|
||||
@@ -350,6 +368,56 @@ pub async fn validate_replica_target(target: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve `host`, re-apply the replica deny list (private peers allowed) to
|
||||
/// every resolved address, and connect to the first one that passes -- the
|
||||
/// connect-time twin of [`validate_replica_target`], so a hostname whose DNS
|
||||
/// answer flips to a blocked address after the up-front check is still refused.
|
||||
/// Mirrors Go's `guardedDialerPolicy` with allowPrivate=true.
|
||||
pub async fn guarded_tcp_connect(
|
||||
host: &str,
|
||||
port: u16,
|
||||
endpoint: &str,
|
||||
) -> std::io::Result<tokio::net::TcpStream> {
|
||||
use std::io::{Error, ErrorKind};
|
||||
|
||||
let denied = |e: String| Error::new(ErrorKind::PermissionDenied, e);
|
||||
|
||||
if is_blocked_imds_host(&host.to_ascii_lowercase()) {
|
||||
return Err(denied(format!(
|
||||
"remote endpoint {:?} targets instance metadata service",
|
||||
endpoint
|
||||
)));
|
||||
}
|
||||
if let Ok(ip) = host.parse::<IpAddr>() {
|
||||
check_blocked_ip_policy(endpoint, ip, true).map_err(denied)?;
|
||||
return tokio::net::TcpStream::connect((ip, port)).await;
|
||||
}
|
||||
|
||||
let lookup = tokio::net::lookup_host((host.to_string(), port));
|
||||
let addrs = tokio::time::timeout(std::time::Duration::from_secs(2), lookup)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
Error::new(
|
||||
ErrorKind::TimedOut,
|
||||
format!("resolve remote endpoint host {:?}: timed out", host),
|
||||
)
|
||||
})??;
|
||||
|
||||
let mut first_block_err: Option<String> = None;
|
||||
for addr in addrs {
|
||||
if let Err(e) = check_blocked_ip_policy(endpoint, addr.ip(), true) {
|
||||
if first_block_err.is_none() {
|
||||
first_block_err = Some(e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return tokio::net::TcpStream::connect(addr).await;
|
||||
}
|
||||
Err(denied(first_block_err.unwrap_or_else(|| {
|
||||
format!("resolve remote endpoint host {:?}: no addresses", host)
|
||||
})))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -380,22 +448,30 @@ mod tests {
|
||||
#[test]
|
||||
fn rejects_empty_and_bad_scheme() {
|
||||
assert!(precheck_endpoint("").unwrap_err().contains("empty"));
|
||||
assert!(precheck_endpoint("ftp://example.com/")
|
||||
.unwrap_err()
|
||||
.contains("http or https"));
|
||||
assert!(precheck_endpoint("example.com/")
|
||||
.unwrap_err()
|
||||
.contains("http or https"));
|
||||
assert!(
|
||||
precheck_endpoint("ftp://example.com/")
|
||||
.unwrap_err()
|
||||
.contains("http or https")
|
||||
);
|
||||
assert!(
|
||||
precheck_endpoint("example.com/")
|
||||
.unwrap_err()
|
||||
.contains("http or https")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_imds_hostnames() {
|
||||
assert!(precheck_endpoint("http://metadata.google.internal/")
|
||||
.unwrap_err()
|
||||
.contains("metadata service"));
|
||||
assert!(precheck_endpoint("http://metadata/")
|
||||
.unwrap_err()
|
||||
.contains("metadata service"));
|
||||
assert!(
|
||||
precheck_endpoint("http://metadata.google.internal/")
|
||||
.unwrap_err()
|
||||
.contains("metadata service")
|
||||
);
|
||||
assert!(
|
||||
precheck_endpoint("http://metadata/")
|
||||
.unwrap_err()
|
||||
.contains("metadata service")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -415,27 +491,41 @@ mod tests {
|
||||
#[test]
|
||||
fn check_blocked_ip_matches_resolved_categories() {
|
||||
// Mirror Go's "host resolves to X" cases at the address level.
|
||||
assert!(check_blocked_ip("e", ip("127.0.0.1"))
|
||||
.unwrap_err()
|
||||
.contains("loopback"));
|
||||
assert!(check_blocked_ip("e", ip("169.254.10.20"))
|
||||
.unwrap_err()
|
||||
.contains("link-local"));
|
||||
assert!(check_blocked_ip("e", ip("10.1.2.3"))
|
||||
.unwrap_err()
|
||||
.contains("private"));
|
||||
assert!(check_blocked_ip("e", ip("172.20.0.5"))
|
||||
.unwrap_err()
|
||||
.contains("private"));
|
||||
assert!(check_blocked_ip("e", ip("192.168.1.1"))
|
||||
.unwrap_err()
|
||||
.contains("private"));
|
||||
assert!(check_blocked_ip("e", ip("100.64.0.42"))
|
||||
.unwrap_err()
|
||||
.contains("CGNAT"));
|
||||
assert!(check_blocked_ip("e", ip("fc00::1"))
|
||||
.unwrap_err()
|
||||
.contains("private"));
|
||||
assert!(
|
||||
check_blocked_ip("e", ip("127.0.0.1"))
|
||||
.unwrap_err()
|
||||
.contains("loopback")
|
||||
);
|
||||
assert!(
|
||||
check_blocked_ip("e", ip("169.254.10.20"))
|
||||
.unwrap_err()
|
||||
.contains("link-local")
|
||||
);
|
||||
assert!(
|
||||
check_blocked_ip("e", ip("10.1.2.3"))
|
||||
.unwrap_err()
|
||||
.contains("private")
|
||||
);
|
||||
assert!(
|
||||
check_blocked_ip("e", ip("172.20.0.5"))
|
||||
.unwrap_err()
|
||||
.contains("private")
|
||||
);
|
||||
assert!(
|
||||
check_blocked_ip("e", ip("192.168.1.1"))
|
||||
.unwrap_err()
|
||||
.contains("private")
|
||||
);
|
||||
assert!(
|
||||
check_blocked_ip("e", ip("100.64.0.42"))
|
||||
.unwrap_err()
|
||||
.contains("CGNAT")
|
||||
);
|
||||
assert!(
|
||||
check_blocked_ip("e", ip("fc00::1"))
|
||||
.unwrap_err()
|
||||
.contains("private")
|
||||
);
|
||||
assert!(check_blocked_ip("e", ip("52.216.10.10")).is_ok());
|
||||
assert!(check_blocked_ip("e", ip("2606:4700:4700::1111")).is_ok());
|
||||
}
|
||||
@@ -478,33 +568,45 @@ mod tests {
|
||||
assert!(check_blocked_ip("e", ip("2001::f7f7:f7f7")).is_ok());
|
||||
assert!(check_blocked_ip("e", ip("::808:808")).is_ok());
|
||||
// Bracketed transition literal via the full endpoint path.
|
||||
assert!(precheck_endpoint("http://[64:ff9b::a9fe:a9fe]/")
|
||||
.unwrap_err()
|
||||
.contains("metadata"));
|
||||
assert!(
|
||||
precheck_endpoint("http://[64:ff9b::a9fe:a9fe]/")
|
||||
.unwrap_err()
|
||||
.contains("metadata")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_ipv4_mapped_ipv6() {
|
||||
// IPv4-mapped IPv6 must be unmapped so the IPv4 rules catch it.
|
||||
assert!(check_blocked_ip("e", ip("::ffff:127.0.0.1"))
|
||||
.unwrap_err()
|
||||
.contains("loopback"));
|
||||
assert!(check_blocked_ip("e", ip("::ffff:169.254.169.254"))
|
||||
.unwrap_err()
|
||||
.contains("metadata"));
|
||||
assert!(check_blocked_ip("e", ip("::ffff:10.0.0.1"))
|
||||
.unwrap_err()
|
||||
.contains("private"));
|
||||
assert!(
|
||||
check_blocked_ip("e", ip("::ffff:127.0.0.1"))
|
||||
.unwrap_err()
|
||||
.contains("loopback")
|
||||
);
|
||||
assert!(
|
||||
check_blocked_ip("e", ip("::ffff:169.254.169.254"))
|
||||
.unwrap_err()
|
||||
.contains("metadata")
|
||||
);
|
||||
assert!(
|
||||
check_blocked_ip("e", ip("::ffff:10.0.0.1"))
|
||||
.unwrap_err()
|
||||
.contains("private")
|
||||
);
|
||||
// A mapped public address still passes, and genuine IPv6 loopback is
|
||||
// still caught by the V6 path.
|
||||
assert!(check_blocked_ip("e", ip("::ffff:52.216.10.10")).is_ok());
|
||||
assert!(check_blocked_ip("e", ip("::1"))
|
||||
.unwrap_err()
|
||||
.contains("loopback"));
|
||||
assert!(
|
||||
check_blocked_ip("e", ip("::1"))
|
||||
.unwrap_err()
|
||||
.contains("loopback")
|
||||
);
|
||||
// Bracketed mapped literal via the full endpoint path.
|
||||
assert!(precheck_endpoint("http://[::ffff:127.0.0.1]/")
|
||||
.unwrap_err()
|
||||
.contains("loopback"));
|
||||
assert!(
|
||||
precheck_endpoint("http://[::ffff:127.0.0.1]/")
|
||||
.unwrap_err()
|
||||
.contains("loopback")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -514,60 +616,86 @@ mod tests {
|
||||
assert!(check_blocked_ip_policy("e", ip("192.168.1.5"), true).is_ok());
|
||||
assert!(check_blocked_ip_policy("e", ip("100.64.0.42"), true).is_ok());
|
||||
// Loopback / IMDS / unspecified stay blocked even when private is allowed.
|
||||
assert!(check_blocked_ip_policy("e", ip("127.0.0.1"), true)
|
||||
.unwrap_err()
|
||||
.contains("loopback"));
|
||||
assert!(check_blocked_ip_policy("e", ip("169.254.169.254"), true)
|
||||
.unwrap_err()
|
||||
.contains("metadata"));
|
||||
assert!(check_blocked_ip_policy("e", ip("0.0.0.0"), true)
|
||||
.unwrap_err()
|
||||
.contains("unspecified"));
|
||||
assert!(
|
||||
check_blocked_ip_policy("e", ip("127.0.0.1"), true)
|
||||
.unwrap_err()
|
||||
.contains("loopback")
|
||||
);
|
||||
assert!(
|
||||
check_blocked_ip_policy("e", ip("169.254.169.254"), true)
|
||||
.unwrap_err()
|
||||
.contains("metadata")
|
||||
);
|
||||
assert!(
|
||||
check_blocked_ip_policy("e", ip("0.0.0.0"), true)
|
||||
.unwrap_err()
|
||||
.contains("unspecified")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_replica_target_rejects_and_allows() {
|
||||
// A path plus a trailing ?a= would otherwise swallow ?type=replicate.
|
||||
assert!(validate_replica_target("127.0.0.1:7000/status/x/?a=")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("bare host:port"));
|
||||
assert!(validate_replica_target("http://10.0.0.7:8080")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("bare host:port"));
|
||||
assert!(validate_replica_target("user@10.0.0.7:8080")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("bare host:port"));
|
||||
assert!(validate_replica_target("10.0.0.7")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("bare host:port"));
|
||||
assert!(validate_replica_target("peer.example.com")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("bare host:port"));
|
||||
assert!(validate_replica_target("127.0.0.1:8080")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("loopback"));
|
||||
assert!(validate_replica_target("[::1]:8080")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("loopback"));
|
||||
assert!(validate_replica_target("169.254.169.254:80")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("metadata"));
|
||||
assert!(validate_replica_target("metadata:80")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("metadata"));
|
||||
assert!(validate_replica_target("")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("empty"));
|
||||
assert!(
|
||||
validate_replica_target("127.0.0.1:7000/status/x/?a=")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("bare host:port")
|
||||
);
|
||||
assert!(
|
||||
validate_replica_target("http://10.0.0.7:8080")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("bare host:port")
|
||||
);
|
||||
assert!(
|
||||
validate_replica_target("user@10.0.0.7:8080")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("bare host:port")
|
||||
);
|
||||
assert!(
|
||||
validate_replica_target("10.0.0.7")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("bare host:port")
|
||||
);
|
||||
assert!(
|
||||
validate_replica_target("peer.example.com")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("bare host:port")
|
||||
);
|
||||
assert!(
|
||||
validate_replica_target("127.0.0.1:8080")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("loopback")
|
||||
);
|
||||
assert!(
|
||||
validate_replica_target("[::1]:8080")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("loopback")
|
||||
);
|
||||
assert!(
|
||||
validate_replica_target("169.254.169.254:80")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("metadata")
|
||||
);
|
||||
assert!(
|
||||
validate_replica_target("metadata:80")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("metadata")
|
||||
);
|
||||
assert!(
|
||||
validate_replica_target("")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.contains("empty")
|
||||
);
|
||||
// Legitimate peer volume servers on private networks pass.
|
||||
assert!(validate_replica_target("10.0.0.7:8080").await.is_ok());
|
||||
assert!(validate_replica_target("192.168.1.5:8080").await.is_ok());
|
||||
|
||||
@@ -7,7 +7,7 @@ pub mod endpoint_guard;
|
||||
pub mod s3;
|
||||
pub mod s3_tier;
|
||||
|
||||
pub use endpoint_guard::{validate_remote_endpoint, validate_replica_target};
|
||||
pub use endpoint_guard::{guarded_tcp_connect, validate_remote_endpoint, validate_replica_target};
|
||||
|
||||
use crate::pb::remote_pb::{RemoteConf, RemoteStorageLocation};
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
//!
|
||||
//! Works with AWS S3, MinIO, SeaweedFS S3, and all S3-compatible providers.
|
||||
|
||||
use aws_sdk_s3::config::{BehaviorVersion, Credentials, Region};
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
use aws_sdk_s3::Client;
|
||||
use aws_sdk_s3::config::{BehaviorVersion, Credentials, Region};
|
||||
use aws_sdk_s3::error::{DisplayErrorContext, SdkError};
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
|
||||
use super::{RemoteEntry, RemoteStorageClient, RemoteStorageError};
|
||||
use crate::pb::remote_pb::{RemoteConf, RemoteStorageLocation};
|
||||
@@ -25,6 +26,23 @@ impl S3RemoteStorageClient {
|
||||
endpoint: &str,
|
||||
force_path_style: bool,
|
||||
) -> Self {
|
||||
let client = Client::from_conf(
|
||||
Self::config_builder(access_key, secret_key, region, endpoint, force_path_style)
|
||||
.build(),
|
||||
);
|
||||
|
||||
S3RemoteStorageClient { client, conf }
|
||||
}
|
||||
|
||||
/// Build the SDK config for the given credentials and endpoint. Split out so
|
||||
/// tests can attach a canned HTTP client before building the [`Client`].
|
||||
fn config_builder(
|
||||
access_key: &str,
|
||||
secret_key: &str,
|
||||
region: &str,
|
||||
endpoint: &str,
|
||||
force_path_style: bool,
|
||||
) -> aws_sdk_s3::config::Builder {
|
||||
let region = if region.is_empty() {
|
||||
"us-east-1"
|
||||
} else {
|
||||
@@ -49,9 +67,7 @@ impl S3RemoteStorageClient {
|
||||
s3_config = s3_config.endpoint_url(endpoint);
|
||||
}
|
||||
|
||||
let client = Client::from_conf(s3_config.build());
|
||||
|
||||
S3RemoteStorageClient { client, conf }
|
||||
s3_config
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,13 +91,14 @@ impl RemoteStorageClient for S3RemoteStorageClient {
|
||||
req = req.range(format!("bytes={}-", offset));
|
||||
}
|
||||
|
||||
let resp = req.send().await.map_err(|e| {
|
||||
let msg = format!("{}", e);
|
||||
if msg.contains("NoSuchKey") || msg.contains("404") {
|
||||
let resp = req.send().await.map_err(|e| match e {
|
||||
// Go compares `aerr.Code()` to NoSuchKey on GET
|
||||
// (s3_storage_client.go:436): a bare 404 maps to "NotFound"
|
||||
// and stays a generic error, as it does here.
|
||||
SdkError::ServiceError(ref se) if se.err().is_no_such_key() => {
|
||||
RemoteStorageError::ObjectNotFound(format!("{}/{}", loc.bucket, key))
|
||||
} else {
|
||||
RemoteStorageError::Other(format!("s3 get object: {}", e))
|
||||
}
|
||||
e => RemoteStorageError::Other(format!("s3 get object: {}", DisplayErrorContext(&e))),
|
||||
})?;
|
||||
|
||||
let data = resp
|
||||
@@ -108,7 +125,9 @@ impl RemoteStorageClient for S3RemoteStorageClient {
|
||||
.body(ByteStream::from(data.to_vec()))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RemoteStorageError::Other(format!("s3 put object: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
RemoteStorageError::Other(format!("s3 put object: {}", DisplayErrorContext(&e)))
|
||||
})?;
|
||||
|
||||
Ok(RemoteEntry {
|
||||
size: data.len() as i64,
|
||||
@@ -134,13 +153,18 @@ impl RemoteStorageClient for S3RemoteStorageClient {
|
||||
.key(key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let msg = format!("{}", e);
|
||||
if msg.contains("404") || msg.contains("NotFound") {
|
||||
.map_err(|e| match e {
|
||||
// Go checks only the raw HTTP status on HEAD
|
||||
// (s3_storage_client.go:373): a HEAD response carries no
|
||||
// error body, so a 404 is not-found whatever code the SDK
|
||||
// assigns, and a non-404 is not.
|
||||
SdkError::ServiceError(ref se) if se.raw().status().as_u16() == 404 => {
|
||||
RemoteStorageError::ObjectNotFound(format!("{}/{}", loc.bucket, key))
|
||||
} else {
|
||||
RemoteStorageError::Other(format!("s3 head object: {}", e))
|
||||
}
|
||||
e => RemoteStorageError::Other(format!(
|
||||
"s3 head object: {}",
|
||||
DisplayErrorContext(&e)
|
||||
)),
|
||||
})?;
|
||||
|
||||
Ok(RemoteEntry {
|
||||
@@ -160,18 +184,17 @@ impl RemoteStorageClient for S3RemoteStorageClient {
|
||||
.key(key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RemoteStorageError::Other(format!("s3 delete object: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
RemoteStorageError::Other(format!("s3 delete object: {}", DisplayErrorContext(&e)))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_buckets(&self) -> Result<Vec<String>, RemoteStorageError> {
|
||||
let resp = self
|
||||
.client
|
||||
.list_buckets()
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RemoteStorageError::Other(format!("s3 list buckets: {}", e)))?;
|
||||
let resp = self.client.list_buckets().send().await.map_err(|e| {
|
||||
RemoteStorageError::Other(format!("s3 list buckets: {}", DisplayErrorContext(&e)))
|
||||
})?;
|
||||
|
||||
Ok(resp
|
||||
.buckets()
|
||||
@@ -184,3 +207,178 @@ impl RemoteStorageClient for S3RemoteStorageClient {
|
||||
&self.conf
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use aws_sdk_s3::config::http::{HttpRequest, HttpResponse};
|
||||
use aws_sdk_s3::config::retry::RetryConfig;
|
||||
use aws_sdk_s3::config::{HttpClient, RuntimeComponents};
|
||||
use aws_sdk_s3::primitives::SdkBody;
|
||||
use aws_smithy_runtime_api::client::http::{
|
||||
HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector,
|
||||
};
|
||||
use aws_smithy_runtime_api::http::StatusCode;
|
||||
|
||||
/// An SDK HTTP client that answers every request with one canned response,
|
||||
/// so the error-mapping paths can be exercised without a network or a
|
||||
/// running S3 server.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct CannedResponse {
|
||||
pub(crate) status: u16,
|
||||
pub(crate) body: &'static str,
|
||||
}
|
||||
|
||||
impl HttpConnector for CannedResponse {
|
||||
fn call(&self, _request: HttpRequest) -> HttpConnectorFuture {
|
||||
let status = StatusCode::try_from(self.status).expect("valid HTTP status");
|
||||
HttpConnectorFuture::ready(Ok(HttpResponse::new(status, SdkBody::from(self.body))))
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for CannedResponse {
|
||||
fn http_connector(
|
||||
&self,
|
||||
_settings: &HttpConnectorSettings,
|
||||
_components: &RuntimeComponents,
|
||||
) -> SharedHttpConnector {
|
||||
SharedHttpConnector::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn client_with(status: u16, body: &'static str) -> S3RemoteStorageClient {
|
||||
let config = S3RemoteStorageClient::config_builder(
|
||||
"AKIATEST",
|
||||
"secret",
|
||||
"us-east-1",
|
||||
"http://127.0.0.1:1",
|
||||
true,
|
||||
)
|
||||
.http_client(CannedResponse { status, body })
|
||||
.retry_config(RetryConfig::disabled())
|
||||
.build();
|
||||
S3RemoteStorageClient {
|
||||
client: Client::from_conf(config),
|
||||
conf: RemoteConf::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn location() -> RemoteStorageLocation {
|
||||
RemoteStorageLocation {
|
||||
name: "remote".to_string(),
|
||||
bucket: "bucket".to_string(),
|
||||
path: "/dir/missing".to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const NO_SUCH_KEY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message><Key>dir/missing</Key></Error>"#;
|
||||
|
||||
const NOT_FOUND_BODY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Error><Code>NotFound</Code><Message>Not Found</Message></Error>"#;
|
||||
|
||||
const ACCESS_DENIED: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Error><Code>AccessDenied</Code><Message>Access Denied</Message></Error>"#;
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_no_such_key_is_object_not_found() {
|
||||
let err = client_with(404, NO_SUCH_KEY)
|
||||
.read_file(&location(), 0, 0)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(&err, RemoteStorageError::ObjectNotFound(path) if path == "bucket/dir/missing"),
|
||||
"expected ObjectNotFound, got {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_bare_404_is_not_object_not_found() {
|
||||
// Go compares codes, not statuses, on GET: a body-less 404 stays generic.
|
||||
let err = client_with(404, "")
|
||||
.read_file(&location(), 0, 0)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, RemoteStorageError::Other(_)),
|
||||
"expected Other, got {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn head_404_is_object_not_found() {
|
||||
let err = client_with(404, "")
|
||||
.stat_file(&location())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(&err, RemoteStorageError::ObjectNotFound(path) if path == "bucket/dir/missing"),
|
||||
"expected ObjectNotFound, got {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn head_404_with_foreign_error_body_is_object_not_found() {
|
||||
// The raw status check makes a 404 not-found whatever body it carries.
|
||||
let err = client_with(404, NO_SUCH_KEY)
|
||||
.stat_file(&location())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, RemoteStorageError::ObjectNotFound(_)),
|
||||
"expected ObjectNotFound, got {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn head_not_found_code_on_a_non_404_status_is_not_object_not_found() {
|
||||
// A NotFound body on a non-404 status stays an error, as in Go.
|
||||
let err = client_with(400, NOT_FOUND_BODY)
|
||||
.stat_file(&location())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(&err, RemoteStorageError::Other(msg) if msg.contains("NotFound")),
|
||||
"expected Other naming the code, got {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_access_denied_keeps_service_error_code() {
|
||||
let err = client_with(403, ACCESS_DENIED)
|
||||
.read_file(&location(), 0, 0)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
matches!(err, RemoteStorageError::Other(_)),
|
||||
"expected Other, got {err:?}"
|
||||
);
|
||||
assert!(
|
||||
msg.contains("AccessDenied"),
|
||||
"message should carry the S3 error code, got: {msg}"
|
||||
);
|
||||
assert!(
|
||||
!msg.ends_with("service error"),
|
||||
"message should not be the bare SdkError Display, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn head_access_denied_keeps_service_error_code() {
|
||||
let err = client_with(403, ACCESS_DENIED)
|
||||
.stat_file(&location())
|
||||
.await
|
||||
.unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
matches!(err, RemoteStorageError::Other(_)),
|
||||
"expected Other, got {err:?}"
|
||||
);
|
||||
assert!(
|
||||
msg.contains("AccessDenied"),
|
||||
"message should carry the S3 error code, got: {msg}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,66 @@ use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::sync::{Arc, OnceLock, RwLock};
|
||||
|
||||
use aws_sdk_s3::config::{BehaviorVersion, Credentials, Region};
|
||||
use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart};
|
||||
use aws_sdk_s3::Client;
|
||||
use aws_sdk_s3::config::http::HttpResponse;
|
||||
use aws_sdk_s3::config::{BehaviorVersion, Credentials, Region};
|
||||
use aws_sdk_s3::error::{DisplayErrorContext, SdkError};
|
||||
use aws_sdk_s3::operation::get_object::GetObjectError;
|
||||
use aws_sdk_s3::operation::head_object::HeadObjectError;
|
||||
use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart};
|
||||
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
/// Concurrency limit for multipart upload/download (matches Go's s3manager).
|
||||
const CONCURRENCY: usize = 5;
|
||||
|
||||
/// A tier transfer failure. The variant is what callers match on; the
|
||||
/// message is the operator-facing text.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TierError {
|
||||
/// The remote object does not exist.
|
||||
#[error("{0}")]
|
||||
NotFound(String),
|
||||
/// An S3 request or a local file operation failed.
|
||||
#[error("{0}")]
|
||||
Io(String),
|
||||
/// The tier I/O runtime could not be built or dropped the task.
|
||||
#[error("{0}")]
|
||||
RuntimeUnavailable(String),
|
||||
/// The progress callback asked to stop.
|
||||
#[error("{0}")]
|
||||
Aborted(String),
|
||||
}
|
||||
|
||||
// Not-found rules as in remote_storage/s3.rs: HEAD by the raw 404 status,
|
||||
// GET by the NoSuchKey code only.
|
||||
fn head_object_error(key: &str, e: SdkError<HeadObjectError, HttpResponse>) -> TierError {
|
||||
let message = format!("failed to head object {}: {}", key, DisplayErrorContext(&e));
|
||||
match e {
|
||||
SdkError::ServiceError(ref se) if se.raw().status().as_u16() == 404 => {
|
||||
TierError::NotFound(message)
|
||||
}
|
||||
_ => TierError::Io(message),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_object_error(
|
||||
key: &str,
|
||||
range: &str,
|
||||
e: SdkError<GetObjectError, HttpResponse>,
|
||||
) -> TierError {
|
||||
let message = format!(
|
||||
"failed to get object {} range {}: {}",
|
||||
key,
|
||||
range,
|
||||
DisplayErrorContext(&e)
|
||||
);
|
||||
match e {
|
||||
SdkError::ServiceError(ref se) if se.err().is_no_such_key() => TierError::NotFound(message),
|
||||
_ => TierError::Io(message),
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for an S3 tier backend.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct S3TierConfig {
|
||||
@@ -89,7 +140,7 @@ impl S3TierBackend {
|
||||
&self,
|
||||
file_path: &str,
|
||||
progress_fn: F,
|
||||
) -> Result<(String, u64), String>
|
||||
) -> Result<(String, u64), TierError>
|
||||
where
|
||||
F: FnMut(i64, f32) -> Result<(), String> + Send + Sync + 'static,
|
||||
{
|
||||
@@ -97,7 +148,7 @@ impl S3TierBackend {
|
||||
|
||||
let metadata = tokio::fs::metadata(file_path)
|
||||
.await
|
||||
.map_err(|e| format!("failed to stat file {}: {}", file_path, e))?;
|
||||
.map_err(|e| TierError::Io(format!("failed to stat file {}: {}", file_path, e)))?;
|
||||
let file_size = metadata.len();
|
||||
|
||||
// Calculate part size: start at 64MB, scale up for very large files (matches Go)
|
||||
@@ -119,11 +170,16 @@ impl S3TierBackend {
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("failed to create multipart upload: {}", e))?;
|
||||
.map_err(|e| {
|
||||
TierError::Io(format!(
|
||||
"failed to create multipart upload: {}",
|
||||
DisplayErrorContext(&e)
|
||||
))
|
||||
})?;
|
||||
|
||||
let upload_id = create_resp
|
||||
.upload_id()
|
||||
.ok_or_else(|| "no upload_id in multipart upload response".to_string())?
|
||||
.ok_or_else(|| TierError::Io("no upload_id in multipart upload response".to_string()))?
|
||||
.to_string();
|
||||
|
||||
// Build list of (part_number, offset, size) for all parts
|
||||
@@ -159,19 +215,21 @@ impl S3TierBackend {
|
||||
let _permit = sem
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| format!("semaphore error: {}", e))?;
|
||||
.map_err(|e| TierError::Io(format!("semaphore error: {}", e)))?;
|
||||
|
||||
// Read this part's data from the file at the correct offset
|
||||
let mut file = tokio::fs::File::open(&fp)
|
||||
.await
|
||||
.map_err(|e| format!("failed to open file {}: {}", fp, e))?;
|
||||
.map_err(|e| TierError::Io(format!("failed to open file {}: {}", fp, e)))?;
|
||||
file.seek(std::io::SeekFrom::Start(off))
|
||||
.await
|
||||
.map_err(|e| format!("failed to seek to offset {}: {}", off, e))?;
|
||||
.map_err(|e| {
|
||||
TierError::Io(format!("failed to seek to offset {}: {}", off, e))
|
||||
})?;
|
||||
let mut buf = vec![0u8; size];
|
||||
file.read_exact(&mut buf)
|
||||
.await
|
||||
.map_err(|e| format!("failed to read file at offset {}: {}", off, e))?;
|
||||
file.read_exact(&mut buf).await.map_err(|e| {
|
||||
TierError::Io(format!("failed to read file at offset {}: {}", off, e))
|
||||
})?;
|
||||
|
||||
let upload_part_resp = client
|
||||
.upload_part()
|
||||
@@ -183,7 +241,12 @@ impl S3TierBackend {
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
format!("failed to upload part {} at offset {}: {}", pn, off, e)
|
||||
TierError::Io(format!(
|
||||
"failed to upload part {} at offset {}: {}",
|
||||
pn,
|
||||
off,
|
||||
DisplayErrorContext(&e)
|
||||
))
|
||||
})?;
|
||||
|
||||
let e_tag = upload_part_resp.e_tag().unwrap_or_default().to_string();
|
||||
@@ -202,9 +265,9 @@ impl S3TierBackend {
|
||||
};
|
||||
(guard.1)(uploaded as i64, pct)
|
||||
};
|
||||
progress_result?;
|
||||
progress_result.map_err(TierError::Aborted)?;
|
||||
|
||||
Ok::<_, String>(
|
||||
Ok::<_, TierError>(
|
||||
CompletedPart::builder()
|
||||
.e_tag(e_tag)
|
||||
.part_number(pn)
|
||||
@@ -219,7 +282,7 @@ impl S3TierBackend {
|
||||
for handle in handles {
|
||||
let part = handle
|
||||
.await
|
||||
.map_err(|e| format!("upload task panicked: {}", e))??;
|
||||
.map_err(|e| TierError::Io(format!("upload task panicked: {}", e)))??;
|
||||
completed_parts.push(part);
|
||||
}
|
||||
|
||||
@@ -236,9 +299,14 @@ impl S3TierBackend {
|
||||
.multipart_upload(completed_upload)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("failed to complete multipart upload: {}", e))?;
|
||||
.map_err(|e| {
|
||||
TierError::Io(format!(
|
||||
"failed to complete multipart upload: {}",
|
||||
DisplayErrorContext(&e)
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok::<(), String>(())
|
||||
Ok::<(), TierError>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
@@ -281,7 +349,7 @@ impl S3TierBackend {
|
||||
dest_path: &str,
|
||||
key: &str,
|
||||
progress_fn: F,
|
||||
) -> Result<u64, String>
|
||||
) -> Result<u64, TierError>
|
||||
where
|
||||
F: FnMut(i64, f32) -> Result<(), String> + Send + Sync + 'static,
|
||||
{
|
||||
@@ -293,7 +361,7 @@ impl S3TierBackend {
|
||||
.key(key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("failed to head object {}: {}", key, e))?;
|
||||
.map_err(|e| head_object_error(key, e))?;
|
||||
|
||||
let file_size = head_resp.content_length().unwrap_or(0) as u64;
|
||||
|
||||
@@ -305,10 +373,12 @@ impl S3TierBackend {
|
||||
.truncate(true)
|
||||
.open(dest_path)
|
||||
.await
|
||||
.map_err(|e| format!("failed to open dest file {}: {}", dest_path, e))?;
|
||||
.map_err(|e| {
|
||||
TierError::Io(format!("failed to open dest file {}: {}", dest_path, e))
|
||||
})?;
|
||||
file.set_len(file_size)
|
||||
.await
|
||||
.map_err(|e| format!("failed to set file length: {}", e))?;
|
||||
.map_err(|e| TierError::Io(format!("failed to set file length: {}", e)))?;
|
||||
}
|
||||
|
||||
let part_size: u64 = 64 * 1024 * 1024;
|
||||
@@ -344,7 +414,7 @@ impl S3TierBackend {
|
||||
let _permit = sem
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| format!("semaphore error: {}", e))?;
|
||||
.map_err(|e| TierError::Io(format!("semaphore error: {}", e)))?;
|
||||
|
||||
let end = off + size - 1;
|
||||
let range = format!("bytes={}-{}", off, end);
|
||||
@@ -356,13 +426,13 @@ impl S3TierBackend {
|
||||
.range(&range)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("failed to get object {} range {}: {}", key, range, e))?;
|
||||
.map_err(|e| get_object_error(&key, &range, e))?;
|
||||
|
||||
let body = get_resp
|
||||
.body
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|e| format!("failed to read body: {}", e))?;
|
||||
.map_err(|e| TierError::Io(format!("failed to read body: {}", e)))?;
|
||||
let bytes = body.into_bytes();
|
||||
|
||||
// Write at the correct offset (like Go's WriteAt)
|
||||
@@ -370,13 +440,17 @@ impl S3TierBackend {
|
||||
.write(true)
|
||||
.open(&dp)
|
||||
.await
|
||||
.map_err(|e| format!("failed to open dest file {}: {}", dp, e))?;
|
||||
.map_err(|e| {
|
||||
TierError::Io(format!("failed to open dest file {}: {}", dp, e))
|
||||
})?;
|
||||
file.seek(std::io::SeekFrom::Start(off))
|
||||
.await
|
||||
.map_err(|e| format!("failed to seek to offset {}: {}", off, e))?;
|
||||
.map_err(|e| {
|
||||
TierError::Io(format!("failed to seek to offset {}: {}", off, e))
|
||||
})?;
|
||||
file.write_all(&bytes)
|
||||
.await
|
||||
.map_err(|e| format!("failed to write to {}: {}", dp, e))?;
|
||||
.map_err(|e| TierError::Io(format!("failed to write to {}: {}", dp, e)))?;
|
||||
|
||||
// Report progress. The lock is released before the result is
|
||||
// propagated so an aborting callback cannot poison the mutex
|
||||
@@ -392,9 +466,9 @@ impl S3TierBackend {
|
||||
};
|
||||
(guard.1)(downloaded as i64, pct)
|
||||
};
|
||||
progress_result?;
|
||||
progress_result.map_err(TierError::Aborted)?;
|
||||
|
||||
Ok::<_, String>(())
|
||||
Ok::<_, TierError>(())
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -402,7 +476,7 @@ impl S3TierBackend {
|
||||
for handle in handles {
|
||||
handle
|
||||
.await
|
||||
.map_err(|e| format!("download task panicked: {}", e))??;
|
||||
.map_err(|e| TierError::Io(format!("download task panicked: {}", e)))??;
|
||||
}
|
||||
|
||||
// fsync the file so its content is durable before the caller trims the .vif
|
||||
@@ -411,16 +485,21 @@ impl S3TierBackend {
|
||||
.write(true)
|
||||
.open(dest_path)
|
||||
.await
|
||||
.map_err(|e| format!("failed to open {} for fsync: {}", dest_path, e))?;
|
||||
.map_err(|e| TierError::Io(format!("failed to open {} for fsync: {}", dest_path, e)))?;
|
||||
synced
|
||||
.sync_all()
|
||||
.await
|
||||
.map_err(|e| format!("failed to fsync {}: {}", dest_path, e))?;
|
||||
.map_err(|e| TierError::Io(format!("failed to fsync {}: {}", dest_path, e)))?;
|
||||
|
||||
Ok(file_size)
|
||||
}
|
||||
|
||||
pub async fn read_range(&self, key: &str, offset: u64, size: usize) -> Result<Vec<u8>, String> {
|
||||
pub async fn read_range(
|
||||
&self,
|
||||
key: &str,
|
||||
offset: u64,
|
||||
size: usize,
|
||||
) -> Result<Vec<u8>, TierError> {
|
||||
let end = offset + (size as u64).saturating_sub(1);
|
||||
let range = format!("bytes={}-{}", offset, end);
|
||||
let resp = self
|
||||
@@ -431,29 +510,35 @@ impl S3TierBackend {
|
||||
.range(&range)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("failed to get object {} range {}: {}", key, range, e))?;
|
||||
.map_err(|e| get_object_error(key, &range, e))?;
|
||||
|
||||
let body = resp
|
||||
.body
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|e| format!("failed to read object {} body: {}", key, e))?;
|
||||
.map_err(|e| TierError::Io(format!("failed to read object {} body: {}", key, e)))?;
|
||||
Ok(body.into_bytes().to_vec())
|
||||
}
|
||||
|
||||
/// Delete a file from S3.
|
||||
pub async fn delete_file(&self, key: &str) -> Result<(), String> {
|
||||
pub async fn delete_file(&self, key: &str) -> Result<(), TierError> {
|
||||
self.client
|
||||
.delete_object()
|
||||
.bucket(&self.bucket)
|
||||
.key(key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("failed to delete object {}: {}", key, e))?;
|
||||
.map_err(|e| {
|
||||
TierError::Io(format!(
|
||||
"failed to delete object {}: {}",
|
||||
key,
|
||||
DisplayErrorContext(&e)
|
||||
))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_file_blocking(&self, key: &str) -> Result<(), String> {
|
||||
pub fn delete_file_blocking(&self, key: &str) -> Result<(), TierError> {
|
||||
let client = self.client.clone();
|
||||
let bucket = self.bucket.clone();
|
||||
let key = key.to_string();
|
||||
@@ -464,7 +549,13 @@ impl S3TierBackend {
|
||||
.key(&key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("failed to delete object {}: {}", key, e))?;
|
||||
.map_err(|e| {
|
||||
TierError::Io(format!(
|
||||
"failed to delete object {}: {}",
|
||||
key,
|
||||
DisplayErrorContext(&e)
|
||||
))
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -474,7 +565,7 @@ impl S3TierBackend {
|
||||
key: &str,
|
||||
offset: u64,
|
||||
size: usize,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
) -> Result<Vec<u8>, TierError> {
|
||||
let client = self.client.clone();
|
||||
let bucket = self.bucket.clone();
|
||||
let key = key.to_string();
|
||||
@@ -488,13 +579,12 @@ impl S3TierBackend {
|
||||
.range(&range)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("failed to get object {} range {}: {}", key, range, e))?;
|
||||
.map_err(|e| get_object_error(&key, &range, e))?;
|
||||
|
||||
let body = resp
|
||||
.body
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|e| format!("failed to read object {} body: {}", key, e))?;
|
||||
let body =
|
||||
resp.body.collect().await.map_err(|e| {
|
||||
TierError::Io(format!("failed to read object {} body: {}", key, e))
|
||||
})?;
|
||||
Ok(body.into_bytes().to_vec())
|
||||
})
|
||||
}
|
||||
@@ -555,18 +645,279 @@ pub fn global_s3_tier_registry() -> &'static RwLock<S3TierRegistry> {
|
||||
GLOBAL_S3_TIER_REGISTRY.get_or_init(|| RwLock::new(S3TierRegistry::new()))
|
||||
}
|
||||
|
||||
fn block_on_tier_future<F, T>(future: F) -> Result<T, String>
|
||||
where
|
||||
F: Future<Output = Result<T, String>> + Send + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
std::thread::spawn(move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
/// The one process-wide runtime for tiered-S3 I/O issued from synchronous
|
||||
/// storage code. A per-call runtime tore down the SDK's pooled connections
|
||||
/// after every 64 KiB chunk, re-dialing TLS per read; a long-lived runtime
|
||||
/// keeps the pool warm.
|
||||
///
|
||||
/// Built on first use. A build failure is returned, not cached or panicked:
|
||||
/// callers sit inside `Volume::destroy` and needle reads, whose own error
|
||||
/// paths must run, and a later call may succeed.
|
||||
static TIER_RUNTIME: std::sync::Mutex<Option<tokio::runtime::Runtime>> =
|
||||
std::sync::Mutex::new(None);
|
||||
|
||||
fn tier_handle() -> Result<tokio::runtime::Handle, TierError> {
|
||||
let mut slot = TIER_RUNTIME
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
if slot.is_none() {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.thread_name("tier-io")
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|e| format!("failed to build tokio runtime: {}", e))?;
|
||||
runtime.block_on(future)
|
||||
})
|
||||
.join()
|
||||
.map_err(|_| "tier runtime thread panicked".to_string())?
|
||||
.map_err(|e| {
|
||||
TierError::RuntimeUnavailable(format!(
|
||||
"failed to build the tier I/O tokio runtime: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
*slot = Some(runtime);
|
||||
}
|
||||
Ok(slot.as_ref().expect("just initialised").handle().clone())
|
||||
}
|
||||
|
||||
/// Run `future` on the tier runtime and block the calling thread until it
|
||||
/// finishes. The caller may be a worker of *another* tokio runtime, so this
|
||||
/// waits on a channel rather than `Handle::block_on`, which panics when
|
||||
/// called from inside any runtime context.
|
||||
fn block_on_tier_future<F, T>(future: F) -> Result<T, TierError>
|
||||
where
|
||||
F: Future<Output = Result<T, TierError>> + Send + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
let handle = tier_handle()?;
|
||||
let task = handle.spawn(future);
|
||||
let (tx, rx) = std::sync::mpsc::sync_channel(1);
|
||||
handle.spawn(async move {
|
||||
// The receiver only goes away if the caller was unwound; nothing to
|
||||
// report then.
|
||||
let _ = tx.send(task.await);
|
||||
});
|
||||
match rx.recv() {
|
||||
Ok(Ok(result)) => result,
|
||||
Ok(Err(join_error)) => Err(describe_join_error(join_error)),
|
||||
Err(_) => Err(TierError::RuntimeUnavailable(
|
||||
"tier I/O runtime dropped the task before it finished".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn a `JoinError` into a message that keeps the panic payload, so an
|
||||
/// SDK panic surfaces as "boom" rather than a fixed "thread panicked".
|
||||
fn describe_join_error(join_error: tokio::task::JoinError) -> TierError {
|
||||
if join_error.is_panic() {
|
||||
let payload = join_error.into_panic();
|
||||
let message = if let Some(s) = payload.downcast_ref::<&str>() {
|
||||
(*s).to_string()
|
||||
} else if let Some(s) = payload.downcast_ref::<String>() {
|
||||
s.clone()
|
||||
} else {
|
||||
"non-string panic payload".to_string()
|
||||
};
|
||||
TierError::Io(format!("tier I/O task panicked: {}", message))
|
||||
} else {
|
||||
TierError::RuntimeUnavailable(format!("tier I/O task failed: {}", join_error))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::remote_storage::s3::tests::{CannedResponse, NO_SUCH_KEY};
|
||||
use std::collections::HashSet;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
fn probe() -> Result<(tokio::runtime::Id, Option<String>), TierError> {
|
||||
block_on_tier_future(async {
|
||||
Ok((
|
||||
Handle::current().id(),
|
||||
std::thread::current().name().map(str::to_string),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_on_tier_future_reuses_one_runtime() {
|
||||
let (first_runtime, first_thread) = probe().expect("first call");
|
||||
let (second_runtime, second_thread) = probe().expect("second call");
|
||||
assert_eq!(
|
||||
first_runtime, second_runtime,
|
||||
"each call must run on the same long-lived tier runtime"
|
||||
);
|
||||
assert_eq!(first_thread.as_deref(), Some("tier-io"));
|
||||
assert_eq!(second_thread.as_deref(), Some("tier-io"));
|
||||
|
||||
let mut runtimes = HashSet::new();
|
||||
for _ in 0..20 {
|
||||
let (id, _) = probe().expect("probe");
|
||||
runtimes.insert(id);
|
||||
}
|
||||
assert_eq!(runtimes.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_on_tier_future_returns_the_value_and_the_error() {
|
||||
assert_eq!(block_on_tier_future(async { Ok(7u32) }).unwrap(), 7);
|
||||
let err = block_on_tier_future::<_, u32>(async { Err(TierError::NotFound("nope".into())) })
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(&err, TierError::NotFound(m) if m == "nope"),
|
||||
"{err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_on_tier_future_works_from_a_std_thread() {
|
||||
let (id, _) = std::thread::spawn(probe)
|
||||
.join()
|
||||
.expect("probe thread")
|
||||
.expect("probe");
|
||||
assert_eq!(id, tier_handle().expect("tier runtime").id());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn block_on_tier_future_works_from_spawn_blocking() {
|
||||
let (id, _) = tokio::task::spawn_blocking(probe)
|
||||
.await
|
||||
.expect("spawn_blocking")
|
||||
.expect("probe");
|
||||
assert_eq!(id, tier_handle().expect("tier runtime").id());
|
||||
assert_ne!(id, Handle::current().id());
|
||||
}
|
||||
|
||||
// Called straight from another runtime's async context: the case that
|
||||
// would panic with `Handle::block_on` ("Cannot start a runtime from
|
||||
// within a runtime").
|
||||
#[tokio::test]
|
||||
async fn block_on_tier_future_works_from_a_current_thread_runtime() {
|
||||
let (id, _) = probe().expect("probe");
|
||||
assert_eq!(id, tier_handle().expect("tier runtime").id());
|
||||
assert_ne!(id, Handle::current().id());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn block_on_tier_future_works_from_a_multi_thread_runtime_worker() {
|
||||
let (id, _) = probe().expect("probe");
|
||||
assert_eq!(id, tier_handle().expect("tier runtime").id());
|
||||
assert_ne!(id, Handle::current().id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_on_tier_future_reports_the_panic_payload() {
|
||||
let err = block_on_tier_future::<_, ()>(async {
|
||||
if std::hint::black_box(true) {
|
||||
panic!("boom {}", 42);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.expect_err("a panicking future must be an error");
|
||||
assert!(matches!(err, TierError::Io(_)), "got: {err:?}");
|
||||
assert!(err.to_string().contains("boom 42"), "got: {err}");
|
||||
assert!(err.to_string().contains("panicked"), "got: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_on_tier_future_reports_a_str_panic_payload() {
|
||||
let err = block_on_tier_future::<_, ()>(async {
|
||||
if std::hint::black_box(true) {
|
||||
panic!("static boom");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.expect_err("a panicking future must be an error");
|
||||
assert!(err.to_string().contains("static boom"), "got: {err}");
|
||||
}
|
||||
|
||||
fn backend_answering(status: u16, body: &'static str) -> S3TierBackend {
|
||||
let config = aws_sdk_s3::Config::builder()
|
||||
.behavior_version(BehaviorVersion::latest())
|
||||
.region(Region::new("us-east-1"))
|
||||
.credentials_provider(Credentials::new("AKIATEST", "secret", None, None, "test"))
|
||||
.endpoint_url("http://127.0.0.1:1")
|
||||
.force_path_style(true)
|
||||
.http_client(CannedResponse { status, body })
|
||||
.retry_config(aws_sdk_s3::config::retry::RetryConfig::disabled())
|
||||
.build();
|
||||
S3TierBackend {
|
||||
client: Client::from_conf(config),
|
||||
bucket: "bucket".to_string(),
|
||||
storage_class: "STANDARD".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn download_head_404_is_not_found() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dest = tmp.path().join("1.dat");
|
||||
let err = backend_answering(404, "")
|
||||
.download_file(dest.to_str().unwrap(), "missing", |_, _| Ok(()))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TierError::NotFound(_)), "{err:?}");
|
||||
assert!(
|
||||
err.to_string()
|
||||
.starts_with("failed to head object missing: "),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn download_head_403_is_io() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dest = tmp.path().join("1.dat");
|
||||
let err = backend_answering(403, "")
|
||||
.download_file(dest.to_str().unwrap(), "denied", |_, _| Ok(()))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TierError::Io(_)), "{err:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_range_no_such_key_is_not_found() {
|
||||
let err = backend_answering(404, NO_SUCH_KEY)
|
||||
.read_range("missing", 0, 8)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TierError::NotFound(_)), "{err:?}");
|
||||
assert!(
|
||||
err.to_string()
|
||||
.starts_with("failed to get object missing range bytes=0-7: "),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_range_bare_404_is_io() {
|
||||
// As in Go, GET is not-found by the NoSuchKey code, not the status.
|
||||
let err = backend_answering(404, "")
|
||||
.read_range("missing", 0, 8)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TierError::Io(_)), "{err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_range_blocking_no_such_key_is_not_found() {
|
||||
let err = backend_answering(404, NO_SUCH_KEY)
|
||||
.read_range_blocking("missing", 0, 8)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, TierError::NotFound(_)), "{err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_name_to_type_id_splits_on_dot() {
|
||||
assert_eq!(
|
||||
backend_name_to_type_id("s3"),
|
||||
("s3".to_string(), "default".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
backend_name_to_type_id("s3.eu"),
|
||||
("s3".to_string(), "eu".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
backend_name_to_type_id("s3.a.b"),
|
||||
(String::new(), String::new())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::collections::HashSet;
|
||||
use std::net::IpAddr;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ============================================================================
|
||||
@@ -297,10 +297,10 @@ impl Guard {
|
||||
/// Extract host from "host:port" or "[::1]:port" format.
|
||||
fn extract_host(addr: &str) -> String {
|
||||
// Handle IPv6 with brackets
|
||||
if addr.starts_with('[') {
|
||||
if let Some(end) = addr.find(']') {
|
||||
return addr[1..end].to_string();
|
||||
}
|
||||
if addr.starts_with('[')
|
||||
&& let Some(end) = addr.find(']')
|
||||
{
|
||||
return addr[1..end].to_string();
|
||||
}
|
||||
// Handle host:port
|
||||
if let Some(pos) = addr.rfind(':') {
|
||||
@@ -481,9 +481,11 @@ mod tests {
|
||||
let token = gen_jwt(&key, 3600, "3,01637037d6").unwrap();
|
||||
|
||||
// Correct file ID
|
||||
assert!(guard
|
||||
.check_jwt_for_file(Some(&token), "3,01637037d6", true)
|
||||
.is_ok());
|
||||
assert!(
|
||||
guard
|
||||
.check_jwt_for_file(Some(&token), "3,01637037d6", true)
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
// Wrong file ID
|
||||
let err = guard.check_jwt_for_file(Some(&token), "4,deadbeef", true);
|
||||
|
||||
@@ -3,12 +3,12 @@ use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustls::client::danger::HandshakeSignatureValid;
|
||||
use rustls::crypto::aws_lc_rs;
|
||||
use rustls::crypto::CryptoProvider;
|
||||
use rustls::crypto::aws_lc_rs;
|
||||
use rustls::pki_types::UnixTime;
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||
use rustls::server::danger::{ClientCertVerified, ClientCertVerifier};
|
||||
use rustls::server::WebPkiClientVerifier;
|
||||
use rustls::server::danger::{ClientCertVerified, ClientCertVerifier};
|
||||
use rustls::{
|
||||
CipherSuite, DigitallySignedStruct, DistinguishedName, RootCertStore, ServerConfig,
|
||||
SignatureScheme, SupportedCipherSuite, SupportedProtocolVersion,
|
||||
@@ -120,10 +120,11 @@ impl ClientCertVerifier for CommonNameVerifier {
|
||||
|
||||
// aws-lc-rs and ring both get linked transitively, so rustls can't auto-select
|
||||
// a provider and tonic's client TLS panics on first use. Pin the default to
|
||||
// aws-lc-rs, matching the server config. Idempotent.
|
||||
pub fn install_default_crypto_provider() {
|
||||
let _ = aws_lc_rs::default_provider().install_default();
|
||||
}
|
||||
// aws-lc-rs, matching the server config. Idempotent. The body lives in
|
||||
// seaweed-common so this binary and the Rust plugin workers cannot end up
|
||||
// installing different providers; re-exported here so callers keep their
|
||||
// import path.
|
||||
pub use seaweed_common::tls::install_default_crypto_provider;
|
||||
|
||||
pub fn build_rustls_server_config(
|
||||
cert_path: &str,
|
||||
@@ -376,7 +377,7 @@ fn go_tls_version_for_supported(version: &SupportedProtocolVersion) -> GoTlsVers
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{build_supported_versions, common_name_is_allowed, parse_cipher_suites, TlsPolicy};
|
||||
use super::{TlsPolicy, build_supported_versions, common_name_is_allowed, parse_cipher_suites};
|
||||
use rustls::crypto::aws_lc_rs;
|
||||
use std::collections::HashSet;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use axum::Router;
|
||||
use axum::body::Body;
|
||||
use axum::extract::Query;
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::http::{StatusCode, header};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{any, get};
|
||||
use axum::Router;
|
||||
use pprof::protos::Message;
|
||||
use serde::Deserialize;
|
||||
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
//! Construction of the volume server's *outgoing* gRPC clients: TLS material,
|
||||
//! endpoint tuning, dial bounds, and the three client constructors every call
|
||||
//! site goes through.
|
||||
//!
|
||||
//! The keepalive, window-size and message-size constants below are shared with
|
||||
//! the *inbound* server built in `main.rs`, which imports them from here rather
|
||||
//! than declaring its own. Changing one therefore changes both directions at
|
||||
//! once, which is deliberate: a volume server talks to its peers with the same
|
||||
//! HTTP/2 settings it offers them.
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
use hyper::http::Uri;
|
||||
use tonic::service::interceptor::InterceptedService;
|
||||
use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity};
|
||||
use tonic::{Request, Status};
|
||||
|
||||
use crate::config::VolumeServerConfig;
|
||||
use crate::pb::filer_pb::seaweed_filer_client::SeaweedFilerClient;
|
||||
use crate::pb::master_pb::seaweed_client::SeaweedClient;
|
||||
use crate::pb::volume_server_pb::volume_server_client::VolumeServerClient;
|
||||
use crate::server::request_id::outgoing_request_id_interceptor;
|
||||
|
||||
pub const GRPC_MAX_MESSAGE_SIZE: usize = 1 << 30;
|
||||
const GRPC_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(60);
|
||||
const GRPC_KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
const GRPC_INITIAL_WINDOW_SIZE: u32 = 16 * 1024 * 1024;
|
||||
pub const GRPC_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(60);
|
||||
pub const GRPC_KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
pub const GRPC_INITIAL_WINDOW_SIZE: u32 = 16 * 1024 * 1024;
|
||||
|
||||
/// Bound on the TCP connect of every outgoing dial. `build_grpc_endpoint` is
|
||||
/// private and `connect_channel` is the only way out of this module, so every
|
||||
/// call site picks this up whether it thinks about timeouts or not.
|
||||
///
|
||||
/// It bounds the TCP handshake only — tonic hands it to
|
||||
/// `HttpConnector::set_connect_timeout`. A peer that completes the handshake
|
||||
/// and then stalls in the TLS or HTTP/2 exchange is not covered; callers that
|
||||
/// need that bound wrap the whole dial (see `connect_ping_target`).
|
||||
const GRPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OutgoingGrpcTlsConfig {
|
||||
@@ -40,7 +66,9 @@ pub fn load_outgoing_grpc_tls(
|
||||
(&config.grpc_client_cert_file, &config.grpc_client_key_file)
|
||||
} else {
|
||||
if !config.grpc_client_cert_file.is_empty() || !config.grpc_client_key_file.is_empty() {
|
||||
tracing::warn!("grpc.volume.client_cert and grpc.volume.client_key must both be set, falling back to grpc.volume.cert and grpc.volume.key");
|
||||
tracing::warn!(
|
||||
"grpc.volume.client_cert and grpc.volume.client_key must both be set, falling back to grpc.volume.cert and grpc.volume.key"
|
||||
);
|
||||
}
|
||||
(&config.grpc_cert_file, &config.grpc_key_file)
|
||||
};
|
||||
@@ -79,7 +107,7 @@ pub fn grpc_endpoint_uri(grpc_host_port: &str, tls: Option<&OutgoingGrpcTlsConfi
|
||||
format!("{}://{}", scheme, grpc_host_port)
|
||||
}
|
||||
|
||||
pub fn build_grpc_endpoint(
|
||||
fn build_grpc_endpoint(
|
||||
grpc_host_port: &str,
|
||||
tls: Option<&OutgoingGrpcTlsConfig>,
|
||||
) -> Result<Endpoint, GrpcClientError> {
|
||||
@@ -115,6 +143,184 @@ pub fn build_grpc_endpoint(
|
||||
Ok(endpoint)
|
||||
}
|
||||
|
||||
/// Connect `endpoint` through a connector that re-validates every resolved
|
||||
/// address at connect time (Go's `guardedDialerPolicy` mirror), pinning a
|
||||
/// validated copy/tail source against DNS rebinding. `allow_untrusted`
|
||||
/// preserves the plain connect for operators that opted out.
|
||||
pub async fn connect_guarded(
|
||||
endpoint: Endpoint,
|
||||
target: &str,
|
||||
allow_untrusted: bool,
|
||||
) -> Result<Channel, GrpcClientError> {
|
||||
if allow_untrusted {
|
||||
return endpoint
|
||||
.connect()
|
||||
.await
|
||||
.map_err(|e| GrpcClientError(format!("connect {} failed: {}", target, e)));
|
||||
}
|
||||
let target_owned = target.to_string();
|
||||
let connector = tower::service_fn(move |uri: Uri| {
|
||||
let target = target_owned.clone();
|
||||
async move {
|
||||
let host = uri.host().unwrap_or_default().to_string();
|
||||
let port = uri.port_u16().unwrap_or(80);
|
||||
crate::remote_storage::guarded_tcp_connect(&host, port, &target)
|
||||
.await
|
||||
.map(hyper_util::rt::TokioIo::new)
|
||||
}
|
||||
});
|
||||
endpoint
|
||||
.connect_with_connector(connector)
|
||||
.await
|
||||
.map_err(|e| GrpcClientError(format!("connect {} failed: {}", target, e)))
|
||||
}
|
||||
|
||||
/// How a dial is bounded.
|
||||
///
|
||||
/// `connect_timeout` is handed to the TCP connector. `request_timeout` becomes
|
||||
/// [`Endpoint::timeout`], which tonic installs as a `GrpcTimeout` layer in
|
||||
/// front of *every* request the resulting channel carries — it is not a
|
||||
/// property of one call.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct GrpcDialOptions {
|
||||
/// Bound on establishing the connection to the peer.
|
||||
pub connect_timeout: Duration,
|
||||
/// Deadline applied to each RPC on the channel, or `None` to leave them
|
||||
/// unbounded.
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl GrpcDialOptions {
|
||||
/// A short request/response call: connect within 5 s, answer within 10 s.
|
||||
pub fn unary() -> Self {
|
||||
Self {
|
||||
connect_timeout: GRPC_CONNECT_TIMEOUT,
|
||||
request_timeout: Some(Duration::from_secs(10)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A call the peer may take a while to answer: connect within 5 s, answer
|
||||
/// within 30 s.
|
||||
pub fn long() -> Self {
|
||||
Self {
|
||||
connect_timeout: GRPC_CONNECT_TIMEOUT,
|
||||
request_timeout: Some(Duration::from_secs(30)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A bounded connect with no deadline on the RPCs themselves.
|
||||
///
|
||||
/// `request_timeout` must stay `None` here. [`Endpoint::timeout`] is not a
|
||||
/// transfer budget: tonic layers it as a `GrpcTimeout` around the
|
||||
/// response future, which resolves when the server's *first response
|
||||
/// headers* arrive, so it bounds how long the peer may take to start
|
||||
/// answering — per request, for every request the channel carries. A 10 s
|
||||
/// value picked to suit one short call would therefore also be the header
|
||||
/// deadline for the `VolumeCopy` that shares the dial, and a busy source
|
||||
/// that takes longer than that to open its file would lose the whole copy.
|
||||
/// `VolumeCopy`, `VolumeTailSender` and `VolumeEcShardsCopy` have never
|
||||
/// carried one.
|
||||
pub fn stream() -> Self {
|
||||
Self {
|
||||
connect_timeout: GRPC_CONNECT_TIMEOUT,
|
||||
request_timeout: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dial a peer and return a connected channel.
|
||||
///
|
||||
/// The error carries only the transport failure: every caller already wraps it
|
||||
/// with the address and the operation it was attempting.
|
||||
pub async fn connect_channel(
|
||||
grpc_host_port: &str,
|
||||
tls: Option<&OutgoingGrpcTlsConfig>,
|
||||
opts: GrpcDialOptions,
|
||||
) -> Result<Channel, GrpcClientError> {
|
||||
let mut endpoint =
|
||||
build_grpc_endpoint(grpc_host_port, tls)?.connect_timeout(opts.connect_timeout);
|
||||
if let Some(request_timeout) = opts.request_timeout {
|
||||
endpoint = endpoint.timeout(request_timeout);
|
||||
}
|
||||
endpoint
|
||||
.connect()
|
||||
.await
|
||||
.map_err(|e| GrpcClientError(e.to_string()))
|
||||
}
|
||||
|
||||
/// Dial a copy/tail source and return a connected channel, re-validating every
|
||||
/// resolved address at connect time.
|
||||
///
|
||||
/// The guarded equivalent of [`connect_channel`]: same `opts` bounds, but the
|
||||
/// dial goes through [`connect_guarded`] so a source address that passed
|
||||
/// validation cannot be re-pointed by DNS between the check and the connect.
|
||||
/// The bounds are applied to the endpoint *before* delegating, so the
|
||||
/// `allow_untrusted` opt-out is timed too.
|
||||
///
|
||||
/// `target` is the caller-facing source address (the unparsed
|
||||
/// `"ip:port.grpcPort"` form), which is what the guard pins against; the error
|
||||
/// carries only the transport failure, as every caller already wraps it with
|
||||
/// the address and the operation it was attempting.
|
||||
pub async fn connect_channel_guarded(
|
||||
grpc_host_port: &str,
|
||||
target: &str,
|
||||
tls: Option<&OutgoingGrpcTlsConfig>,
|
||||
opts: GrpcDialOptions,
|
||||
allow_untrusted: bool,
|
||||
) -> Result<Channel, GrpcClientError> {
|
||||
let mut endpoint =
|
||||
build_grpc_endpoint(grpc_host_port, tls)?.connect_timeout(opts.connect_timeout);
|
||||
if let Some(request_timeout) = opts.request_timeout {
|
||||
endpoint = endpoint.timeout(request_timeout);
|
||||
}
|
||||
connect_guarded(endpoint, target, allow_untrusted).await
|
||||
}
|
||||
|
||||
/// The outgoing request-id interceptor as a concrete type, so the client
|
||||
/// aliases below can name it.
|
||||
pub type RequestIdInterceptor = fn(Request<()>) -> Result<Request<()>, Status>;
|
||||
|
||||
/// A volume-server client with the request-id interceptor attached.
|
||||
pub type VolumeServerGrpcClient =
|
||||
VolumeServerClient<InterceptedService<Channel, RequestIdInterceptor>>;
|
||||
/// A master client with the request-id interceptor attached.
|
||||
pub type MasterGrpcClient = SeaweedClient<InterceptedService<Channel, RequestIdInterceptor>>;
|
||||
/// A filer client with the request-id interceptor attached.
|
||||
pub type FilerGrpcClient = SeaweedFilerClient<InterceptedService<Channel, RequestIdInterceptor>>;
|
||||
|
||||
/// Wrap a connected channel in a volume-server client that forwards the
|
||||
/// current request id and lifts both message-size limits.
|
||||
pub fn volume_server_client(channel: Channel) -> VolumeServerGrpcClient {
|
||||
VolumeServerClient::with_interceptor(
|
||||
channel,
|
||||
outgoing_request_id_interceptor as RequestIdInterceptor,
|
||||
)
|
||||
.max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE)
|
||||
.max_encoding_message_size(GRPC_MAX_MESSAGE_SIZE)
|
||||
}
|
||||
|
||||
/// Wrap a connected channel in a master client that forwards the current
|
||||
/// request id and lifts both message-size limits.
|
||||
pub fn master_client(channel: Channel) -> MasterGrpcClient {
|
||||
SeaweedClient::with_interceptor(
|
||||
channel,
|
||||
outgoing_request_id_interceptor as RequestIdInterceptor,
|
||||
)
|
||||
.max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE)
|
||||
.max_encoding_message_size(GRPC_MAX_MESSAGE_SIZE)
|
||||
}
|
||||
|
||||
/// Wrap a connected channel in a filer client that forwards the current
|
||||
/// request id and lifts both message-size limits.
|
||||
pub fn filer_client(channel: Channel) -> FilerGrpcClient {
|
||||
SeaweedFilerClient::with_interceptor(
|
||||
channel,
|
||||
outgoing_request_id_interceptor as RequestIdInterceptor,
|
||||
)
|
||||
.max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE)
|
||||
.max_encoding_message_size(GRPC_MAX_MESSAGE_SIZE)
|
||||
}
|
||||
|
||||
/// Parse a SeaweedFS server address (`"ip:port.grpcPort"` or
|
||||
/// `"ip:port"`) into the `host:grpcPort` form `build_grpc_endpoint`
|
||||
/// expects. With the trailing `.grpcPort` segment, that segment IS
|
||||
@@ -124,53 +330,27 @@ pub fn build_grpc_endpoint(
|
||||
/// Shared between `grpc_server.rs` and the distributed-EC-read path
|
||||
/// in `store_ec.rs` — keep this as the single source of truth so the
|
||||
/// HTTP↔gRPC port translation can't drift between callers.
|
||||
///
|
||||
/// The rule itself lives in `seaweed_common::address`, which the Rust
|
||||
/// plugin workers share; this wrapper only flattens the typed error
|
||||
/// back to the `String` its callers already handle. Unbracketed IPv6
|
||||
/// literals come back bracketed, which this copy used to get wrong.
|
||||
pub fn parse_grpc_address(source: &str) -> Result<String, String> {
|
||||
let colon_idx = source
|
||||
.rfind(':')
|
||||
.ok_or_else(|| format!("cannot parse address: {}", source))?;
|
||||
let host = &source[..colon_idx];
|
||||
let port_part = &source[colon_idx + 1..];
|
||||
|
||||
if let Some(dot_idx) = port_part.rfind('.') {
|
||||
// Format: "ip:port.grpcPort". Validate BOTH ports as u16
|
||||
// so a malformed HTTP port (e.g. `host:abc.18080`) is
|
||||
// rejected here rather than tripping a downstream
|
||||
// `build_grpc_endpoint` URI parse failure with a less
|
||||
// useful error.
|
||||
let http_port = &port_part[..dot_idx];
|
||||
let grpc_port = &port_part[dot_idx + 1..];
|
||||
http_port
|
||||
.parse::<u16>()
|
||||
.map_err(|e| format!("invalid http port {:?}: {}", http_port, e))?;
|
||||
grpc_port
|
||||
.parse::<u16>()
|
||||
.map_err(|e| format!("invalid grpc port {:?}: {}", grpc_port, e))?;
|
||||
return Ok(format!("{}:{}", host, grpc_port));
|
||||
}
|
||||
|
||||
// Format: "ip:port" → grpc = port + 10000. Reject inputs whose
|
||||
// implicit grpc port would overflow the TCP port range (e.g.
|
||||
// `host:60000` produces 70000 — invalid). Without this check
|
||||
// the cast silently wraps and the endpoint call later fails
|
||||
// with an opaque connection error.
|
||||
let port: u16 = port_part
|
||||
.parse()
|
||||
.map_err(|e| format!("invalid port {:?}: {}", port_part, e))?;
|
||||
let grpc_port = port as u32 + 10000;
|
||||
if grpc_port > u16::MAX as u32 {
|
||||
return Err(format!(
|
||||
"implicit grpc port out of range: {} + 10000 = {}",
|
||||
port, grpc_port
|
||||
));
|
||||
}
|
||||
Ok(format!("{}:{}", host, grpc_port))
|
||||
seaweed_common::address::to_grpc_address(source).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{build_grpc_endpoint, grpc_endpoint_uri, load_outgoing_grpc_tls};
|
||||
use super::{
|
||||
GrpcDialOptions, build_grpc_endpoint, connect_channel, grpc_endpoint_uri,
|
||||
load_outgoing_grpc_tls, volume_server_client,
|
||||
};
|
||||
use crate::config::{NeedleMapKind, ReadMode, VolumeServerConfig};
|
||||
use crate::pb::volume_server_pb;
|
||||
use crate::security::tls::TlsPolicy;
|
||||
use crate::server::request_id::scope_request_id;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
const TEST_CERT_PEM: &str = "-----BEGIN CERTIFICATE-----\nMIIBPDCB76ADAgECAhRuRPQgeAu43BT/M7EfAWSdapVdYDAFBgMrZXAwFDESMBAG\nA1UEAwwJbG9jYWxob3N0MB4XDTI2MDcwNTE2MTUyOVoXDTM2MDcwMjE2MTUyOVow\nFDESMBAGA1UEAwwJbG9jYWxob3N0MCowBQYDK2VwAyEAr/3bNIFI+8V32oCiY6y+\nXRFmZpdNQ2g//VtRkT+nQg+jUzBRMB0GA1UdDgQWBBTsy9tLf1zPiXCQfgci6zNi\ndEzRSjAfBgNVHSMEGDAWgBTsy9tLf1zPiXCQfgci6zNidEzRSjAPBgNVHRMBAf8E\nBTADAQH/MAUGAytlcANBAIvsdw0IbvOBBkb9cd7BfMJfIP9pQQrAL03pCRWJFnFh\nSysaLVgFXI4T078IiaM874oO+iB+5vNbWEpc7CkGow4=\n-----END CERTIFICATE-----\n";
|
||||
const TEST_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIHbyn71Kk+Y7KT3sBctit7uZpErpoH6qDbFj6P8qGaZH\n-----END PRIVATE KEY-----\n";
|
||||
@@ -365,4 +545,131 @@ mod tests {
|
||||
let err = parse_grpc_address("hostname").unwrap_err();
|
||||
assert!(err.contains("cannot parse"), "{}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_grpc_address_brackets_ipv6_literals() {
|
||||
use super::parse_grpc_address;
|
||||
// This used to come back as `::1:29333`, which is not a valid
|
||||
// authority: `build_grpc_endpoint` reads the last colon as the port
|
||||
// separator and rejects the rest.
|
||||
assert_eq!(parse_grpc_address("::1:19333").unwrap(), "[::1]:29333");
|
||||
assert_eq!(parse_grpc_address("::1:9333.19333").unwrap(), "[::1]:19333");
|
||||
// Already bracketed, so it is left alone.
|
||||
assert_eq!(parse_grpc_address("[::1]:9333").unwrap(), "[::1]:19333");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_grpc_endpoint_accepts_an_ipv6_master_address() {
|
||||
use super::parse_grpc_address;
|
||||
let endpoint = build_grpc_endpoint(&parse_grpc_address("::1:9333").unwrap(), None).unwrap();
|
||||
assert_eq!(endpoint.uri().port_u16(), Some(19333));
|
||||
}
|
||||
/// A minimal HTTP/2 server that records the gRPC request headers it is
|
||||
/// sent and answers every call with a trailers-only `unimplemented`. It is
|
||||
/// enough to prove what a helper-built client puts on the wire, without
|
||||
/// standing up the whole `VolumeServer` service behind a tonic server.
|
||||
async fn serve_header_capture() -> (u16, Arc<Mutex<Option<String>>>) {
|
||||
use hyper::service::service_fn;
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let seen: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
|
||||
let captured = Arc::clone(&seen);
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Ok((stream, _)) = listener.accept().await {
|
||||
let captured = Arc::clone(&captured);
|
||||
tokio::spawn(async move {
|
||||
let _ = hyper::server::conn::http2::Builder::new(TokioExecutor::new())
|
||||
.serve_connection(
|
||||
TokioIo::new(stream),
|
||||
service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
|
||||
let captured = Arc::clone(&captured);
|
||||
async move {
|
||||
let value = req
|
||||
.headers()
|
||||
.get("x-amz-request-id")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(str::to_string);
|
||||
*captured.lock().unwrap() = value;
|
||||
Ok::<_, std::convert::Infallible>(
|
||||
hyper::http::Response::builder()
|
||||
.status(200)
|
||||
.header("content-type", "application/grpc")
|
||||
.header("grpc-status", "12")
|
||||
.body(tonic::body::Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
(port, seen)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_helper_built_client_sends_the_scoped_request_id() {
|
||||
let (port, seen) = serve_header_capture().await;
|
||||
|
||||
let channel = connect_channel(
|
||||
&format!("127.0.0.1:{}", port),
|
||||
None,
|
||||
GrpcDialOptions::unary(),
|
||||
)
|
||||
.await
|
||||
.expect("dial the header-capturing server");
|
||||
|
||||
let mut client = volume_server_client(channel);
|
||||
// The interceptor has a request id to forward only inside a scope, so
|
||||
// the call has to run inside one for this to test anything.
|
||||
let _ = scope_request_id("REQUEST-ID-ON-THE-WIRE".to_string(), async move {
|
||||
client
|
||||
.ping(volume_server_pb::PingRequest {
|
||||
target: String::new(),
|
||||
target_type: String::new(),
|
||||
})
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
seen.lock().unwrap().as_deref(),
|
||||
Some("REQUEST-ID-ON-THE-WIRE"),
|
||||
"a client built by volume_server_client must carry the outgoing request id"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dial_presets_match_the_call_sites_they_replace() {
|
||||
assert_eq!(
|
||||
GrpcDialOptions::unary().connect_timeout,
|
||||
Duration::from_secs(5)
|
||||
);
|
||||
assert_eq!(
|
||||
GrpcDialOptions::unary().request_timeout,
|
||||
Some(Duration::from_secs(10))
|
||||
);
|
||||
assert_eq!(
|
||||
GrpcDialOptions::long().connect_timeout,
|
||||
Duration::from_secs(5)
|
||||
);
|
||||
assert_eq!(
|
||||
GrpcDialOptions::long().request_timeout,
|
||||
Some(Duration::from_secs(30))
|
||||
);
|
||||
assert_eq!(
|
||||
GrpcDialOptions::stream().connect_timeout,
|
||||
Duration::from_secs(5)
|
||||
);
|
||||
assert_eq!(
|
||||
GrpcDialOptions::stream().request_timeout,
|
||||
None,
|
||||
"a streaming dial must not put a per-request deadline on the channel"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+4809
-1184
File diff suppressed because it is too large
Load Diff
+1312
-491
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -36,16 +36,20 @@ pub fn collect_mem_status() -> volume_server_pb::MemStatus {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_system_memory_linux() -> Option<(u64, u64)> {
|
||||
unsafe {
|
||||
let mut info: libc::sysinfo = std::mem::zeroed();
|
||||
if libc::sysinfo(&mut info) == 0 {
|
||||
let unit = info.mem_unit as u64;
|
||||
let total = info.totalram as u64 * unit;
|
||||
let free = info.freeram as u64 * unit;
|
||||
return Some((total, free));
|
||||
}
|
||||
// SAFETY: `libc::sysinfo` is plain data — integers and trailing padding,
|
||||
// no pointers and no restricted niches — so the all-zero value is a valid
|
||||
// one for the kernel to overwrite.
|
||||
let mut info: libc::sysinfo = unsafe { std::mem::zeroed() };
|
||||
// SAFETY: `&mut info` is a live, aligned, exclusive pointer to a
|
||||
// `sysinfo` that the kernel only writes through, and its fields are read
|
||||
// below only after the call reports success.
|
||||
if unsafe { libc::sysinfo(&mut info) } != 0 {
|
||||
return None;
|
||||
}
|
||||
None
|
||||
let unit = info.mem_unit as u64;
|
||||
let total = info.totalram as u64 * unit;
|
||||
let free = info.freeram as u64 * unit;
|
||||
Some((total, free))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
use tonic::Status;
|
||||
|
||||
use crate::remote_storage::s3_tier::TierError;
|
||||
use crate::storage::volume::VolumeError;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod debug;
|
||||
pub mod grpc_client;
|
||||
@@ -13,3 +18,111 @@ pub mod store_ec;
|
||||
pub mod ui;
|
||||
pub mod volume_server;
|
||||
pub mod write_queue;
|
||||
|
||||
/// Map a storage error onto the gRPC code that describes it.
|
||||
impl From<VolumeError> for Status {
|
||||
fn from(err: VolumeError) -> Self {
|
||||
let message = err.to_string();
|
||||
match err {
|
||||
VolumeError::NotFound
|
||||
| VolumeError::VolumeNotFound(_)
|
||||
| VolumeError::Tier(TierError::NotFound(_)) => Status::not_found(message),
|
||||
VolumeError::ReadOnly(_) | VolumeError::NotEmpty => {
|
||||
Status::failed_precondition(message)
|
||||
}
|
||||
VolumeError::InsufficientSpace { .. } => Status::resource_exhausted(message),
|
||||
VolumeError::AlreadyExists => Status::already_exists(message),
|
||||
_ => Status::internal(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Same mapping, with the RPC's own context prefixed (`compact volume 7: ...`).
|
||||
pub fn status_with_context(context: &str, err: VolumeError) -> Status {
|
||||
let status = Status::from(err);
|
||||
Status::new(status.code(), format!("{context}: {}", status.message()))
|
||||
}
|
||||
|
||||
/// Render a configured disk directory as an absolute path for display, so the
|
||||
/// status JSON and the UI show the same thing for a relative `-dir`. Falls
|
||||
/// back to the configured spelling when the current directory cannot be read.
|
||||
pub(crate) fn absolute_display_path(path: &str) -> String {
|
||||
let p = std::path::Path::new(path);
|
||||
if p.is_absolute() {
|
||||
return path.to_string();
|
||||
}
|
||||
std::env::current_dir()
|
||||
.map(|cwd| cwd.join(p).to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| path.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::storage::types::VolumeId;
|
||||
|
||||
#[test]
|
||||
fn test_volume_error_maps_to_grpc_code() {
|
||||
use tonic::Code;
|
||||
|
||||
let code = |e: VolumeError| Status::from(e).code();
|
||||
assert_eq!(
|
||||
code(VolumeError::VolumeNotFound(VolumeId(7))),
|
||||
Code::NotFound
|
||||
);
|
||||
assert_eq!(code(VolumeError::NotFound), Code::NotFound);
|
||||
assert_eq!(
|
||||
code(VolumeError::ReadOnly(VolumeId(7))),
|
||||
Code::FailedPrecondition
|
||||
);
|
||||
assert_eq!(
|
||||
VolumeError::ReadOnly(VolumeId(7)).to_string(),
|
||||
"volume 7 is read only"
|
||||
);
|
||||
assert_eq!(
|
||||
code(VolumeError::InsufficientSpace {
|
||||
vid: VolumeId(7),
|
||||
required: 2,
|
||||
free: 1,
|
||||
}),
|
||||
Code::ResourceExhausted
|
||||
);
|
||||
assert_eq!(code(VolumeError::AlreadyExists), Code::AlreadyExists);
|
||||
assert_eq!(code(VolumeError::NotInitialized), Code::Internal);
|
||||
assert_eq!(
|
||||
code(TierError::NotFound("gone".into()).into()),
|
||||
Code::NotFound
|
||||
);
|
||||
for tier in [
|
||||
TierError::Io("io".into()),
|
||||
TierError::RuntimeUnavailable("rt".into()),
|
||||
TierError::Aborted("bye".into()),
|
||||
] {
|
||||
assert_eq!(code(tier.into()), Code::Internal);
|
||||
}
|
||||
|
||||
let status = status_with_context(
|
||||
"backend s3.default copy file /data/1.dat",
|
||||
TierError::NotFound("failed to head object k: NotFound".into()).into(),
|
||||
);
|
||||
assert_eq!(status.code(), Code::NotFound);
|
||||
assert_eq!(
|
||||
status.message(),
|
||||
"backend s3.default copy file /data/1.dat: failed to head object k: NotFound"
|
||||
);
|
||||
|
||||
let status = status_with_context(
|
||||
"compact volume 7",
|
||||
VolumeError::InsufficientSpace {
|
||||
vid: VolumeId(7),
|
||||
required: 2,
|
||||
free: 1,
|
||||
},
|
||||
);
|
||||
assert_eq!(status.code(), Code::ResourceExhausted);
|
||||
assert_eq!(
|
||||
status.message(),
|
||||
"compact volume 7: not enough free space: required 2, free 1"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ impl<S> Layer<S> for GrpcRequestIdLayer {
|
||||
|
||||
impl<S, B> Service<http::Request<B>> for GrpcRequestIdService<S>
|
||||
where
|
||||
S: Service<http::Request<B>, Response = http::Response<tonic::body::BoxBody>> + Send + 'static,
|
||||
S: Service<http::Request<B>, Response = http::Response<tonic::body::Body>> + Send + 'static,
|
||||
S::Future: Send + 'static,
|
||||
B: Send + 'static,
|
||||
{
|
||||
type Response = http::Response<tonic::body::BoxBody>;
|
||||
type Response = http::Response<tonic::body::Body>;
|
||||
type Error = S::Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
||||
|
||||
@@ -57,7 +57,7 @@ where
|
||||
let future = self.inner.call(request);
|
||||
|
||||
Box::pin(async move {
|
||||
let mut response: http::Response<tonic::body::BoxBody> =
|
||||
let mut response: http::Response<tonic::body::Body> =
|
||||
scope_request_id(request_id.clone(), future).await?;
|
||||
if let Ok(value) = HeaderValue::from_str(&request_id) {
|
||||
response.headers_mut().insert("x-amz-request-id", value);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use crate::server::absolute_display_path;
|
||||
use crate::server::server_stats;
|
||||
use crate::server::volume_server::VolumeServerState;
|
||||
use crate::storage::store::Store;
|
||||
@@ -423,13 +424,12 @@ fn collect_ui_data(
|
||||
shard_id: shard.shard_id,
|
||||
size: shard_size,
|
||||
});
|
||||
if created_at == "-" {
|
||||
if let Ok(metadata) = std::fs::metadata(shard.file_name()) {
|
||||
if let Ok(modified) = metadata.modified() {
|
||||
let ts: chrono::DateTime<chrono::Local> = modified.into();
|
||||
created_at = ts.format("%Y-%m-%d %H:%M").to_string();
|
||||
}
|
||||
}
|
||||
if created_at == "-"
|
||||
&& let Ok(metadata) = std::fs::metadata(shard.file_name())
|
||||
&& let Ok(modified) = metadata.modified()
|
||||
{
|
||||
let ts: chrono::DateTime<chrono::Local> = modified.into();
|
||||
created_at = ts.format("%Y-%m-%d %H:%M").to_string();
|
||||
}
|
||||
}
|
||||
let preferred_size = ec_volume.dat_file_size.max(0) as u64;
|
||||
@@ -451,16 +451,6 @@ fn collect_ui_data(
|
||||
(disk_rows, volumes, remote_volumes, ec_volumes)
|
||||
}
|
||||
|
||||
fn absolute_display_path(path: &str) -> String {
|
||||
let p = std::path::Path::new(path);
|
||||
if p.is_absolute() {
|
||||
return path.to_string();
|
||||
}
|
||||
std::env::current_dir()
|
||||
.map(|cwd| cwd.join(p).to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| path.to_string())
|
||||
}
|
||||
|
||||
fn join_i64(values: &[i64]) -> String {
|
||||
values
|
||||
.iter()
|
||||
|
||||
@@ -14,12 +14,12 @@ use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use axum::{
|
||||
extract::{connect_info::ConnectInfo, Request, State},
|
||||
http::{header, HeaderValue, Method, StatusCode},
|
||||
Router,
|
||||
extract::{Request, State, connect_info::ConnectInfo},
|
||||
http::{HeaderValue, Method, StatusCode, header},
|
||||
middleware::{self, Next},
|
||||
response::{IntoResponse, Response},
|
||||
routing::{any, get},
|
||||
Router,
|
||||
};
|
||||
|
||||
use crate::config::ReadMode;
|
||||
@@ -73,8 +73,6 @@ pub struct VolumeServerState {
|
||||
pub volume_state_notify: tokio::sync::Notify,
|
||||
/// Optional batched write queue for improved throughput under load.
|
||||
pub write_queue: std::sync::OnceLock<WriteQueue>,
|
||||
/// Registry of S3 tier backends for tiered storage operations.
|
||||
pub s3_tier_registry: std::sync::RwLock<crate::remote_storage::s3_tier::S3TierRegistry>,
|
||||
/// Read mode: local, proxy, or redirect for non-local volumes.
|
||||
pub read_mode: ReadMode,
|
||||
/// If true, FetchAndWriteNeedle skips remote S3 endpoint validation,
|
||||
@@ -200,9 +198,7 @@ pub fn to_http_address(addr: &str) -> std::borrow::Cow<'_, str> {
|
||||
// rather than being silently rewritten. Mirrors the validation already
|
||||
// done in `to_grpc_address` for the inverse direction.
|
||||
if let (Ok(_), Ok(_)) = (http_port.parse::<u16>(), grpc_port.parse::<u16>()) {
|
||||
return std::borrow::Cow::Owned(
|
||||
addr[..ports_sep_index + 1 + dot_idx].to_string(),
|
||||
);
|
||||
return std::borrow::Cow::Owned(addr[..ports_sep_index + 1 + dot_idx].to_string());
|
||||
}
|
||||
}
|
||||
std::borrow::Cow::Borrowed(addr)
|
||||
@@ -312,16 +308,15 @@ async fn admin_store_handler(state: State<Arc<VolumeServerState>>, request: Requ
|
||||
)
|
||||
}
|
||||
};
|
||||
if method == Method::GET {
|
||||
if let Some(response_bytes) = response
|
||||
if method == Method::GET
|
||||
&& let Some(response_bytes) = response
|
||||
.headers()
|
||||
.get(header::CONTENT_LENGTH)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.parse::<i64>().ok())
|
||||
.filter(|value| *value > 0)
|
||||
{
|
||||
super::server_stats::record_bytes_out(response_bytes);
|
||||
}
|
||||
{
|
||||
super::server_stats::record_bytes_out(response_bytes);
|
||||
}
|
||||
super::server_stats::record_request_close();
|
||||
crate::metrics::INFLIGHT_REQUESTS_GAUGE
|
||||
@@ -358,16 +353,15 @@ async fn public_store_handler(state: State<Arc<VolumeServerState>>, request: Req
|
||||
}
|
||||
_ => StatusCode::OK.into_response(),
|
||||
};
|
||||
if method == Method::GET {
|
||||
if let Some(response_bytes) = response
|
||||
if method == Method::GET
|
||||
&& let Some(response_bytes) = response
|
||||
.headers()
|
||||
.get(header::CONTENT_LENGTH)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.parse::<i64>().ok())
|
||||
.filter(|value| *value > 0)
|
||||
{
|
||||
super::server_stats::record_bytes_out(response_bytes);
|
||||
}
|
||||
{
|
||||
super::server_stats::record_bytes_out(response_bytes);
|
||||
}
|
||||
super::server_stats::record_request_close();
|
||||
crate::metrics::INFLIGHT_REQUESTS_GAUGE
|
||||
@@ -432,13 +426,13 @@ pub fn build_admin_router_with_ui(state: Arc<VolumeServerState>, ui_enabled: boo
|
||||
.route("/healthz", get(handlers::healthz_handler))
|
||||
.route("/favicon.ico", get(handlers::favicon_handler))
|
||||
.route(
|
||||
"/seaweedfsstatic/*path",
|
||||
"/seaweedfsstatic/{*path}",
|
||||
get(handlers::static_asset_handler),
|
||||
)
|
||||
.route("/", any(admin_store_handler))
|
||||
.route("/:path", any(admin_store_handler))
|
||||
.route("/:vid/:fid", any(admin_store_handler))
|
||||
.route("/:vid/:fid/:filename", any(admin_store_handler))
|
||||
.route("/{path}", any(admin_store_handler))
|
||||
.route("/{vid}/{fid}", any(admin_store_handler))
|
||||
.route("/{vid}/{fid}/{filename}", any(admin_store_handler))
|
||||
.fallback(admin_store_handler);
|
||||
if ui_enabled {
|
||||
// Note: /stats/* endpoints are commented out in Go's volume_server.go (L130-134).
|
||||
@@ -455,13 +449,13 @@ pub fn build_public_router(state: Arc<VolumeServerState>) -> Router {
|
||||
Router::new()
|
||||
.route("/favicon.ico", get(handlers::favicon_handler))
|
||||
.route(
|
||||
"/seaweedfsstatic/*path",
|
||||
"/seaweedfsstatic/{*path}",
|
||||
get(handlers::static_asset_handler),
|
||||
)
|
||||
.route("/", any(public_store_handler))
|
||||
.route("/:path", any(public_store_handler))
|
||||
.route("/:vid/:fid", any(public_store_handler))
|
||||
.route("/:vid/:fid/:filename", any(public_store_handler))
|
||||
.route("/{path}", any(public_store_handler))
|
||||
.route("/{vid}/{fid}", any(public_store_handler))
|
||||
.route("/{vid}/{fid}/{filename}", any(public_store_handler))
|
||||
.fallback(public_store_handler)
|
||||
.layer(middleware::from_fn(common_headers_middleware))
|
||||
.with_state(state)
|
||||
@@ -516,7 +510,10 @@ mod tests {
|
||||
// "host:abc.def"), and silently rewriting it would just hide the bug.
|
||||
assert_eq!(to_http_address("host:abc.def"), "host:abc.def");
|
||||
assert_eq!(to_http_address("host:9333.notaport"), "host:9333.notaport");
|
||||
assert_eq!(to_http_address("host:notaport.19333"), "host:notaport.19333");
|
||||
assert_eq!(
|
||||
to_http_address("host:notaport.19333"),
|
||||
"host:notaport.19333"
|
||||
);
|
||||
// Out-of-range ports must not be silently truncated either.
|
||||
assert_eq!(to_http_address("host:99999.19333"), "host:99999.19333");
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! Instead of each upload handler directly calling `write_needle`, writes are
|
||||
//! submitted to a queue. A background worker drains the queue in batches (up to
|
||||
//! 128 entries), groups them by volume ID, and processes them together under a
|
||||
//! single store lock. Requests that asked for `fsync` are flushed by
|
||||
//! `write_needle` itself, one flush per durable write.
|
||||
//! single store lock. Durable writes to a volume share their .dat and .idx
|
||||
//! flushes (see `Volume::write_needles_grouped`).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -159,8 +159,12 @@ fn process_batch(state: Arc<VolumeServerState>, batch: Vec<WriteRequest>) {
|
||||
let mut store = state.store.write().unwrap();
|
||||
|
||||
for (vid, entries) in groups {
|
||||
for (mut needle, fsync, response_tx) in entries {
|
||||
let result = store.write_volume_needle(vid, &mut needle, fsync);
|
||||
let (mut writes, senders): (Vec<_>, Vec<_>) = entries
|
||||
.into_iter()
|
||||
.map(|(needle, fsync, response_tx)| ((needle, fsync), response_tx))
|
||||
.unzip();
|
||||
let results = store.write_volume_needles(vid, &mut writes);
|
||||
for (response_tx, result) in senders.into_iter().zip(results) {
|
||||
// Send result back; ignore error if receiver dropped.
|
||||
let _ = response_tx.send(result);
|
||||
}
|
||||
@@ -178,8 +182,8 @@ mod tests {
|
||||
use crate::server::volume_server::RuntimeMetricsConfig;
|
||||
use crate::storage::needle_map::NeedleMapKind;
|
||||
use crate::storage::store::Store;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32};
|
||||
use std::sync::RwLock;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32};
|
||||
|
||||
let store = Store::new(NeedleMapKind::InMemory);
|
||||
let guard = Guard::new(&[], SigningKey(vec![]), 0, SigningKey(vec![]), 0);
|
||||
@@ -207,9 +211,6 @@ mod tests {
|
||||
pre_stop_seconds: 0,
|
||||
volume_state_notify: tokio::sync::Notify::new(),
|
||||
write_queue: std::sync::OnceLock::new(),
|
||||
s3_tier_registry: std::sync::RwLock::new(
|
||||
crate::remote_storage::s3_tier::S3TierRegistry::new(),
|
||||
),
|
||||
read_mode: crate::config::ReadMode::Local,
|
||||
allow_untrusted_remote_endpoints: false,
|
||||
master_url: String::new(),
|
||||
@@ -315,6 +316,63 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The queue hands a volume's batch to the grouped path, so ten durable
|
||||
/// writes cost one .dat sync and one .idx sync, not ten of each.
|
||||
#[test]
|
||||
fn test_process_batch_group_commits_fsync_writes() {
|
||||
use crate::config::MinFreeSpace;
|
||||
use crate::storage::types::{DiskType, NeedleId};
|
||||
use crate::storage::volume::VolumeSpec;
|
||||
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let dir = tmp.path().to_str().unwrap();
|
||||
let state = make_test_state();
|
||||
{
|
||||
let mut store = state.store.write().unwrap();
|
||||
store
|
||||
.add_location(
|
||||
dir,
|
||||
dir,
|
||||
10,
|
||||
DiskType::HardDrive,
|
||||
MinFreeSpace::Percent(1.0),
|
||||
Vec::new(),
|
||||
)
|
||||
.unwrap();
|
||||
store
|
||||
.add_volume(VolumeId(1), DiskType::HardDrive, &VolumeSpec::default())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let mut receivers = Vec::new();
|
||||
let batch = (1..=10u64)
|
||||
.map(|id| {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
receivers.push(response_rx);
|
||||
WriteRequest {
|
||||
volume_id: VolumeId(1),
|
||||
needle: Needle {
|
||||
id: NeedleId(id),
|
||||
cookie: 0x1111.into(),
|
||||
data: vec![id as u8; 8],
|
||||
data_size: 8,
|
||||
..Needle::default()
|
||||
},
|
||||
fsync: true,
|
||||
response_tx,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
process_batch(state.clone(), batch);
|
||||
|
||||
for mut rx in receivers {
|
||||
assert!(matches!(rx.try_recv().unwrap(), Ok((_, _, false))));
|
||||
}
|
||||
let store = state.store.read().unwrap();
|
||||
let (_, vol) = store.find_volume(VolumeId(1)).unwrap();
|
||||
assert_eq!(vol.sync_counts_for_test(), (1, 1));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_write_queue_dropped_sender() {
|
||||
// When the queue is dropped, subsequent submits should fail gracefully.
|
||||
|
||||
@@ -15,15 +15,15 @@ use tracing::warn;
|
||||
use crate::config::MinFreeSpace;
|
||||
use crate::storage::erasure_coding::ec_bitrot::remove_bitrot_sidecars;
|
||||
use crate::storage::erasure_coding::ec_shard::{
|
||||
EcVolumeShard, DATA_SHARDS_COUNT, ERASURE_CODING_LARGE_BLOCK_SIZE,
|
||||
ERASURE_CODING_SMALL_BLOCK_SIZE,
|
||||
DATA_SHARDS_COUNT, ERASURE_CODING_LARGE_BLOCK_SIZE, ERASURE_CODING_SMALL_BLOCK_SIZE,
|
||||
EcVolumeShard, ShardId,
|
||||
};
|
||||
use crate::storage::erasure_coding::ec_volume::EcVolume;
|
||||
use crate::storage::erasure_coding::ec_volume::{EcVolume, is_usable_ecx_file};
|
||||
use crate::storage::needle_map::NeedleMapKind;
|
||||
use crate::storage::super_block::{ReplicaPlacement, SUPER_BLOCK_SIZE};
|
||||
use crate::storage::super_block::SUPER_BLOCK_SIZE;
|
||||
use crate::storage::types::*;
|
||||
use crate::storage::volume::{
|
||||
remove_volume_files, volume_file_name, VifVolumeInfo, Volume, VolumeError,
|
||||
VifVolumeInfo, Volume, VolumeError, VolumeSpec, remove_volume_files, volume_file_name,
|
||||
};
|
||||
|
||||
/// A single disk location managing volumes in one directory.
|
||||
@@ -131,10 +131,10 @@ impl DiskLocation {
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name().into_string().unwrap_or_default();
|
||||
if let Some((collection, vid)) = parse_volume_filename(&name) {
|
||||
if seen.insert((collection.clone(), vid)) {
|
||||
dat_files.push((collection, vid));
|
||||
}
|
||||
if let Some((collection, vid)) = parse_volume_filename(&name)
|
||||
&& seen.insert((collection.clone(), vid))
|
||||
{
|
||||
dat_files.push((collection, vid));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +205,6 @@ impl DiskLocation {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Load existing data only; never create a phantom `.dat`. A lone
|
||||
// `.vif`/`.idx` (e.g. an EC sidecar whose `.ecx` is on a sibling
|
||||
// disk) would otherwise have Volume::new write an 8-byte stub that
|
||||
@@ -280,30 +279,33 @@ impl DiskLocation {
|
||||
let opened = Mutex::new(Vec::with_capacity(to_load.len()));
|
||||
std::thread::scope(|scope| {
|
||||
for _ in 0..workers {
|
||||
scope.spawn(|| loop {
|
||||
let i = next.fetch_add(1, Ordering::Relaxed);
|
||||
let Some((vid, collections)) = to_load.get(i) else {
|
||||
return;
|
||||
};
|
||||
for collection in collections {
|
||||
match Volume::new(
|
||||
&self.directory,
|
||||
&self.idx_directory,
|
||||
collection,
|
||||
*vid,
|
||||
needle_map_kind,
|
||||
None, // replica placement read from superblock
|
||||
None, // TTL read from superblock
|
||||
0, // no preallocate on load
|
||||
Version::current(),
|
||||
) {
|
||||
Ok(mut v) => {
|
||||
v.location_disk_space_low = self.is_disk_space_low.clone();
|
||||
opened.lock().unwrap().push((collection.clone(), *vid, v));
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(volume_id = vid.0, error = %e, "failed to load volume");
|
||||
scope.spawn(|| {
|
||||
loop {
|
||||
let i = next.fetch_add(1, Ordering::Relaxed);
|
||||
let Some((vid, collections)) = to_load.get(i) else {
|
||||
return;
|
||||
};
|
||||
for collection in collections {
|
||||
// Replica placement and TTL are read back from the
|
||||
// superblock, and a load never preallocates.
|
||||
match Volume::new(
|
||||
&self.directory,
|
||||
&self.idx_directory,
|
||||
*vid,
|
||||
needle_map_kind,
|
||||
&VolumeSpec {
|
||||
collection,
|
||||
..Default::default()
|
||||
},
|
||||
) {
|
||||
Ok(mut v) => {
|
||||
v.location_disk_space_low = self.is_disk_space_low.clone();
|
||||
opened.lock().unwrap().push((collection.clone(), *vid, v));
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(volume_id = vid.0, error = %e, "failed to load volume");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,10 +329,10 @@ impl DiskLocation {
|
||||
.strip_suffix(".cpc")
|
||||
.or_else(|| name.strip_suffix(".cpd"))
|
||||
.or_else(|| name.strip_suffix(".cpx"));
|
||||
if let Some(stem) = stem {
|
||||
if let Some(key) = parse_collection_volume_id(stem) {
|
||||
pending.insert(key);
|
||||
}
|
||||
if let Some(stem) = stem
|
||||
&& let Some(key) = parse_collection_volume_id(stem)
|
||||
{
|
||||
pending.insert(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,8 +376,10 @@ impl DiskLocation {
|
||||
let mut expected_shard_size: Option<i64> = None;
|
||||
let dat_exists = match fs::metadata(&dat_path) {
|
||||
Ok(meta) if meta.len() > SUPER_BLOCK_SIZE as u64 => {
|
||||
expected_shard_size =
|
||||
Some(calculate_expected_shard_size(meta.len() as i64, data_shards));
|
||||
expected_shard_size = Some(calculate_expected_shard_size(
|
||||
meta.len() as i64,
|
||||
data_shards,
|
||||
));
|
||||
true
|
||||
}
|
||||
Ok(_) => false,
|
||||
@@ -399,7 +403,13 @@ impl DiskLocation {
|
||||
if size != prev {
|
||||
// Inconsistent sizes signal corruption or mixed
|
||||
// generations; not trusted for deletion -> keep.
|
||||
warn!(volume_id = vid.0, shard = i, size, expected = prev, "EC shard size mismatch; keeping shards");
|
||||
warn!(
|
||||
volume_id = vid.0,
|
||||
shard = i,
|
||||
size,
|
||||
expected = prev,
|
||||
"EC shard size mismatch; keeping shards"
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
@@ -426,11 +436,16 @@ impl DiskLocation {
|
||||
if shard_count == 0 {
|
||||
return false;
|
||||
}
|
||||
if let (Some(actual), Some(expected)) = (actual_shard_size, expected_shard_size) {
|
||||
if actual < expected {
|
||||
warn!(volume_id = vid.0, actual, expected, "shards smaller than the .dat's full encode; reclaiming the complete .dat");
|
||||
return false;
|
||||
}
|
||||
if let (Some(actual), Some(expected)) = (actual_shard_size, expected_shard_size)
|
||||
&& actual < expected
|
||||
{
|
||||
warn!(
|
||||
volume_id = vid.0,
|
||||
actual,
|
||||
expected,
|
||||
"shards smaller than the .dat's full encode; reclaiming the complete .dat"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -464,6 +479,13 @@ impl DiskLocation {
|
||||
if self.idx_directory != self.directory {
|
||||
remove_bitrot_sidecars(&idx_base)?;
|
||||
}
|
||||
|
||||
// Staged 2PC generations (<base>.ecNN.v<N>, versioned .ecx/.ecj/.vif)
|
||||
// belong to this volume's EC state too; leaving them orphans the files.
|
||||
crate::storage::erasure_coding::ec_shard::remove_ec_generation_files(&base, 0)?;
|
||||
if self.idx_directory != self.directory {
|
||||
crate::storage::erasure_coding::ec_shard::remove_ec_generation_files(&idx_base, 0)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -510,10 +532,10 @@ impl DiskLocation {
|
||||
pub(crate) fn ec_generation_ts_ns(&self, collection: &str, vid: VolumeId) -> Option<i64> {
|
||||
for dir in [&self.directory, &self.idx_directory] {
|
||||
let vif = format!("{}.vif", volume_file_name(dir, collection, vid));
|
||||
if let Ok(s) = fs::read_to_string(&vif) {
|
||||
if let Ok(vi) = serde_json::from_str::<VifVolumeInfo>(&s) {
|
||||
return Some(vi.ec_shard_config.map(|c| c.encode_ts_ns).unwrap_or(0));
|
||||
}
|
||||
if let Ok(s) = fs::read_to_string(&vif)
|
||||
&& let Ok(vi) = serde_json::from_str::<VifVolumeInfo>(&s)
|
||||
{
|
||||
return Some(vi.ec_shard_config.map(|c| c.encode_ts_ns).unwrap_or(0));
|
||||
}
|
||||
if self.directory == self.idx_directory {
|
||||
break;
|
||||
@@ -545,27 +567,19 @@ impl DiskLocation {
|
||||
pub fn create_volume(
|
||||
&mut self,
|
||||
vid: VolumeId,
|
||||
collection: &str,
|
||||
needle_map_kind: NeedleMapKind,
|
||||
replica_placement: Option<ReplicaPlacement>,
|
||||
ttl: Option<crate::storage::needle::ttl::TTL>,
|
||||
preallocate: u64,
|
||||
version: Version,
|
||||
spec: &VolumeSpec<'_>,
|
||||
) -> Result<(), VolumeError> {
|
||||
let mut v = Volume::new(
|
||||
&self.directory,
|
||||
&self.idx_directory,
|
||||
collection,
|
||||
vid,
|
||||
needle_map_kind,
|
||||
replica_placement,
|
||||
ttl,
|
||||
preallocate,
|
||||
version,
|
||||
spec,
|
||||
)?;
|
||||
v.location_disk_space_low = self.is_disk_space_low.clone();
|
||||
crate::metrics::VOLUME_GAUGE
|
||||
.with_label_values(&[collection, "volume"])
|
||||
.with_label_values(&[spec.collection, "volume"])
|
||||
.inc();
|
||||
self.volumes.insert(vid, v);
|
||||
Ok(())
|
||||
@@ -591,13 +605,20 @@ impl DiskLocation {
|
||||
&mut self,
|
||||
vid: VolumeId,
|
||||
only_empty: bool,
|
||||
only_garbage: bool,
|
||||
keep_remote_data: bool,
|
||||
) -> Result<(), VolumeError> {
|
||||
// Refuse before removing: a refused destroy must leave it mounted.
|
||||
if let Some(v) = self.volumes.get(&vid)
|
||||
&& v.is_compacting()
|
||||
{
|
||||
return Err(v.compacting_error());
|
||||
}
|
||||
if let Some(mut v) = self.volumes.remove(&vid) {
|
||||
crate::metrics::VOLUME_GAUGE
|
||||
.with_label_values(&[&v.collection, "volume"])
|
||||
.dec();
|
||||
v.destroy(only_empty, keep_remote_data)?;
|
||||
v.destroy(only_empty, only_garbage, keep_remote_data)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(VolumeError::NotFound)
|
||||
@@ -618,7 +639,7 @@ impl DiskLocation {
|
||||
crate::metrics::VOLUME_GAUGE
|
||||
.with_label_values(&[&v.collection, "volume"])
|
||||
.dec();
|
||||
if let Err(e) = v.destroy(false, false) {
|
||||
if let Err(e) = v.destroy(false, false, false) {
|
||||
warn!(volume_id = vid.0, error = %e, "delete collection: failed to destroy volume");
|
||||
}
|
||||
}
|
||||
@@ -669,8 +690,7 @@ impl DiskLocation {
|
||||
pub fn free_volume_count(&self) -> i32 {
|
||||
use crate::storage::erasure_coding::ec_shard::DATA_SHARDS_COUNT;
|
||||
let max = self.max_volume_count.load(Ordering::Relaxed);
|
||||
let free_count = (max as i64 - self.volumes.len() as i64)
|
||||
* DATA_SHARDS_COUNT as i64
|
||||
let free_count = (max as i64 - self.volumes.len() as i64) * DATA_SHARDS_COUNT as i64
|
||||
- self.ec_shard_count() as i64;
|
||||
let effective_free = free_count / DATA_SHARDS_COUNT as i64;
|
||||
if effective_free > 0 {
|
||||
@@ -773,22 +793,18 @@ impl DiskLocation {
|
||||
/// Mirrors `DiskLocation.HasEcxFileOnDisk` in
|
||||
/// `weed/storage/disk_location_ec.go`. Skips entries that are
|
||||
/// directories so a stray dir named `<collection>_<vid>.ecx` doesn't
|
||||
/// register as a present index file.
|
||||
/// register as a present index file. A 0-byte `.ecx` is a corrupt stub
|
||||
/// left by a failed EC distribute copy; it must not steer placement
|
||||
/// toward this disk, so it counts as absent (Go requires `Size() > 0`).
|
||||
pub fn has_ecx_file_on_disk(&self, collection: &str, vid: VolumeId) -> bool {
|
||||
let idx_base = volume_file_name(&self.idx_directory, collection, vid);
|
||||
let idx_path = format!("{}.ecx", idx_base);
|
||||
if let Ok(meta) = fs::metadata(&idx_path) {
|
||||
if !meta.is_dir() {
|
||||
return true;
|
||||
}
|
||||
if is_usable_ecx_file(&format!("{}.ecx", idx_base)) {
|
||||
return true;
|
||||
}
|
||||
if self.idx_directory != self.directory {
|
||||
let data_base = volume_file_name(&self.directory, collection, vid);
|
||||
let data_path = format!("{}.ecx", data_base);
|
||||
if let Ok(meta) = fs::metadata(&data_path) {
|
||||
if !meta.is_dir() {
|
||||
return true;
|
||||
}
|
||||
if is_usable_ecx_file(&format!("{}.ecx", data_base)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -799,6 +815,20 @@ impl DiskLocation {
|
||||
self.ec_volumes.remove(&vid)
|
||||
}
|
||||
|
||||
/// Drop the in-memory EC volume for vid and close its descriptors without
|
||||
/// deleting files, so a following unlink frees the inodes instead of
|
||||
/// leaving open fds serving the old bytes. Mirrors Go's unloadEcVolume.
|
||||
pub fn unload_ec_volume(&mut self, vid: VolumeId) {
|
||||
if let Some(mut ec_vol) = self.ec_volumes.remove(&vid) {
|
||||
for _ in 0..ec_vol.shard_count() {
|
||||
crate::metrics::VOLUME_GAUGE
|
||||
.with_label_values(&[&ec_vol.collection, "ec_shards"])
|
||||
.dec();
|
||||
}
|
||||
ec_vol.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Mount EC shards for a volume on this location.
|
||||
///
|
||||
/// `source_disk_type` is the source volume's disk type carried on the
|
||||
@@ -811,7 +841,7 @@ impl DiskLocation {
|
||||
&mut self,
|
||||
vid: VolumeId,
|
||||
collection: &str,
|
||||
shard_ids: &[u32],
|
||||
shard_ids: &[ShardId],
|
||||
source_disk_type: &str,
|
||||
) -> Result<(), VolumeError> {
|
||||
let idx_dir = self.idx_directory.clone();
|
||||
@@ -833,7 +863,7 @@ impl DiskLocation {
|
||||
&mut self,
|
||||
vid: VolumeId,
|
||||
collection: &str,
|
||||
shard_ids: &[u32],
|
||||
shard_ids: &[ShardId],
|
||||
idx_dir: &str,
|
||||
source_disk_type: &str,
|
||||
) -> Result<(), VolumeError> {
|
||||
@@ -845,14 +875,10 @@ impl DiskLocation {
|
||||
// propagate the error to the caller.
|
||||
let created = !self.ec_volumes.contains_key(&vid);
|
||||
if created {
|
||||
let ec_vol = EcVolume::new(&dir, idx_dir, collection, vid)
|
||||
.map_err(VolumeError::Io)?;
|
||||
let ec_vol = EcVolume::new(&dir, idx_dir, collection, vid).map_err(VolumeError::Io)?;
|
||||
self.ec_volumes.insert(vid, ec_vol);
|
||||
}
|
||||
let ec_vol = self
|
||||
.ec_volumes
|
||||
.get_mut(&vid)
|
||||
.expect("just inserted above");
|
||||
let ec_vol = self.ec_volumes.get_mut(&vid).expect("just inserted above");
|
||||
// When the orchestrator supplied a source disk type on the Mount
|
||||
// RPC, override the EC volume's disk type so heartbeats report
|
||||
// under the source volume's disk type (#9423). When the caller
|
||||
@@ -871,10 +897,10 @@ impl DiskLocation {
|
||||
// keep the existing registration (mirrors Go's AddEcVolumeShard
|
||||
// added=false) — re-adding would replace a serving fd and bump
|
||||
// the ec_shards gauge without growing the mounted count.
|
||||
if ec_vol.has_shard(shard_id as u8) {
|
||||
if ec_vol.has_shard(shard_id) {
|
||||
continue;
|
||||
}
|
||||
let mut shard = EcVolumeShard::new(&dir, collection, vid, shard_id as u8);
|
||||
let mut shard = EcVolumeShard::new(&dir, collection, vid, shard_id);
|
||||
shard.disk_type = ec_vol.disk_type.clone();
|
||||
if let Err(e) = ec_vol.add_shard(shard) {
|
||||
// The shard was dropped (its descriptors closed) inside the
|
||||
@@ -902,14 +928,14 @@ impl DiskLocation {
|
||||
/// caller passes a shard that lives on a sibling disk
|
||||
/// (cross-disk reconcile makes that the common case for the same
|
||||
/// `vid` after reconciliation).
|
||||
pub fn unmount_ec_shards(&mut self, vid: VolumeId, shard_ids: &[u32]) {
|
||||
pub fn unmount_ec_shards(&mut self, vid: VolumeId, shard_ids: &[ShardId]) {
|
||||
if let Some(ec_vol) = self.ec_volumes.get_mut(&vid) {
|
||||
let collection = ec_vol.collection.clone();
|
||||
for &shard_id in shard_ids {
|
||||
if !ec_vol.has_shard(shard_id as u8) {
|
||||
if !ec_vol.has_shard(shard_id) {
|
||||
continue;
|
||||
}
|
||||
ec_vol.remove_shard(shard_id as u8);
|
||||
let _ = ec_vol.remove_shard(shard_id);
|
||||
crate::metrics::VOLUME_GAUGE
|
||||
.with_label_values(&[&collection, "ec_shards"])
|
||||
.dec();
|
||||
@@ -969,7 +995,7 @@ impl DiskLocation {
|
||||
}
|
||||
entries.sort();
|
||||
|
||||
let mut same_volume_shards: Vec<(String, u32)> = Vec::new(); // (filename, shard_id)
|
||||
let mut same_volume_shards: Vec<(String, ShardId)> = Vec::new(); // (filename, shard_id)
|
||||
let mut prev_vid: Option<VolumeId> = None;
|
||||
let mut prev_collection: String = String::new();
|
||||
|
||||
@@ -1034,7 +1060,12 @@ impl DiskLocation {
|
||||
/// Validate + mount a (collection, vid) group when its `.ecx` is
|
||||
/// found. Mirrors `handleFoundEcxFile` in
|
||||
/// `weed/storage/disk_location_ec.go`.
|
||||
fn handle_found_ecx_file(&mut self, shards: &[(String, u32)], collection: &str, vid: VolumeId) {
|
||||
fn handle_found_ecx_file(
|
||||
&mut self,
|
||||
shards: &[(String, ShardId)],
|
||||
collection: &str,
|
||||
vid: VolumeId,
|
||||
) {
|
||||
let base = volume_file_name(&self.directory, collection, vid);
|
||||
let dat_path = format!("{}.dat", base);
|
||||
let dat_exists = check_dat_file_exists(&dat_path);
|
||||
@@ -1048,7 +1079,7 @@ impl DiskLocation {
|
||||
return;
|
||||
}
|
||||
|
||||
let shard_ids: Vec<u32> = shards.iter().map(|(_, sid)| *sid).collect();
|
||||
let shard_ids: Vec<ShardId> = shards.iter().map(|(_, sid)| *sid).collect();
|
||||
if let Err(e) = self.mount_ec_shards(vid, collection, &shard_ids, "") {
|
||||
// A mount failure (corrupt/locked .ecx, EMFILE, transient I/O) is
|
||||
// not proof the shards are disposable -- validate_ec_volume already
|
||||
@@ -1057,8 +1088,7 @@ impl DiskLocation {
|
||||
// delete on a load error.
|
||||
warn!(
|
||||
volume_id = vid.0,
|
||||
"Failed to load EC shards: {}; keeping files for retry",
|
||||
e,
|
||||
"Failed to load EC shards: {}; keeping files for retry", e,
|
||||
);
|
||||
self.unmount_ec_shards(vid, &shard_ids);
|
||||
}
|
||||
@@ -1071,7 +1101,7 @@ impl DiskLocation {
|
||||
/// distributed-EC shards waiting for cross-disk reconciliation.
|
||||
fn check_orphaned_shards(
|
||||
&self,
|
||||
shards: &[(String, u32)],
|
||||
shards: &[(String, ShardId)],
|
||||
collection: &str,
|
||||
vid: VolumeId,
|
||||
) -> bool {
|
||||
@@ -1107,7 +1137,7 @@ impl DiskLocation {
|
||||
|
||||
/// Close all volumes.
|
||||
pub fn close(&mut self) {
|
||||
for (_, v) in self.volumes.iter_mut() {
|
||||
for v in self.volumes.values_mut() {
|
||||
v.close();
|
||||
}
|
||||
self.volumes.clear();
|
||||
@@ -1127,20 +1157,60 @@ pub fn get_disk_stats(path: &str) -> (u64, u64) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return (0, 0),
|
||||
};
|
||||
unsafe {
|
||||
let mut stat: libc::statvfs = std::mem::zeroed();
|
||||
if libc::statvfs(c_path.as_ptr(), &mut stat) == 0 {
|
||||
let all = stat.f_blocks as u64 * stat.f_frsize as u64;
|
||||
let free = stat.f_bavail as u64 * stat.f_frsize as u64;
|
||||
return (all, free);
|
||||
}
|
||||
// SAFETY: `libc::statvfs` is plain data — integers and reserved
|
||||
// padding, no pointers and no restricted niches — so the all-zero
|
||||
// value is a valid one for the call to overwrite.
|
||||
let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
|
||||
// SAFETY: `c_path` is a live NUL-terminated `CString` that outlives
|
||||
// the call, and `&mut stat` is a live, aligned, exclusive pointer the
|
||||
// kernel only writes through; the fields are read below only after
|
||||
// the call reports success.
|
||||
if unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) } != 0 {
|
||||
return (0, 0);
|
||||
}
|
||||
(0, 0)
|
||||
let all = stat.f_blocks as u64 * stat.f_frsize as u64;
|
||||
let free = stat.f_bavail as u64 * stat.f_frsize as u64;
|
||||
(all, free)
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = path;
|
||||
(0, 0)
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
// Canonicalize so symlinks, `.`/`..` segments, and relative paths
|
||||
// resolve to the real location before querying. `\\?\`-prefixed
|
||||
// extended-length paths and UNC (`\\?\UNC\...`) are passed through
|
||||
// untouched: GetDiskFreeSpaceExW accepts them as-is.
|
||||
let canonical = match std::fs::canonicalize(path) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return (0, 0),
|
||||
};
|
||||
// UTF-16 with trailing NUL for the Win32 wide-string call.
|
||||
let mut wide: Vec<u16> = canonical.as_os_str().encode_wide().collect();
|
||||
// UNC directory names must end in a backslash for GetDiskFreeSpaceExW.
|
||||
if !wide.ends_with(&[0x5C]) {
|
||||
wide.push(0x5C);
|
||||
}
|
||||
wide.push(0);
|
||||
// SAFETY: `wide` is NUL-terminated; the out-params are valid u64
|
||||
// writes; the call has no other preconditions.
|
||||
unsafe {
|
||||
let mut free_available: u64 = 0;
|
||||
let mut total: u64 = 0;
|
||||
let ok = windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
|
||||
wide.as_ptr(),
|
||||
&mut free_available,
|
||||
&mut total,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if ok == 0 {
|
||||
return (0, 0);
|
||||
}
|
||||
return (total, free_available);
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
{
|
||||
compile_error!("get_disk_stats is implemented for unix and windows only");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1176,7 +1246,12 @@ fn rm_if_present(path: String) -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
fn ec_data_shards_from_vif(directory: &str, idx_directory: &str, collection: &str, vid: VolumeId) -> usize {
|
||||
fn ec_data_shards_from_vif(
|
||||
directory: &str,
|
||||
idx_directory: &str,
|
||||
collection: &str,
|
||||
vid: VolumeId,
|
||||
) -> usize {
|
||||
for dir in [directory, idx_directory] {
|
||||
let vif = format!("{}.vif", volume_file_name(dir, collection, vid));
|
||||
if let Some(ds) = fs::read_to_string(&vif)
|
||||
@@ -1184,10 +1259,9 @@ fn ec_data_shards_from_vif(directory: &str, idx_directory: &str, collection: &st
|
||||
.and_then(|s| serde_json::from_str::<VifVolumeInfo>(&s).ok())
|
||||
.and_then(|vi| vi.ec_shard_config)
|
||||
.map(|c| c.data_shards as usize)
|
||||
&& ds > 0
|
||||
{
|
||||
if ds > 0 {
|
||||
return ds;
|
||||
}
|
||||
return ds;
|
||||
}
|
||||
if directory == idx_directory {
|
||||
break;
|
||||
@@ -1223,7 +1297,7 @@ fn parse_collection_volume_id(base: &str) -> Option<(String, VolumeId)> {
|
||||
|
||||
/// `pub(crate)` re-export of [`parse_ec_shard_extension`] for the
|
||||
/// cross-disk reconcile in `store_ec_reconcile.rs`.
|
||||
pub(crate) fn is_ec_shard_extension(ext: &str) -> Option<u32> {
|
||||
pub(crate) fn is_ec_shard_extension(ext: &str) -> Option<ShardId> {
|
||||
parse_ec_shard_extension(ext)
|
||||
}
|
||||
|
||||
@@ -1237,7 +1311,7 @@ pub(crate) fn is_ec_shard_extension(ext: &str) -> Option<u32> {
|
||||
/// shardId > 255` guard. The 3-digit form (`.ec100`–`.ec255`) is
|
||||
/// retained so the parser can still recognise shards from custom
|
||||
/// 32+ ratios that fit in a u8 even though OSS only ships 10+4.
|
||||
fn parse_ec_shard_extension(ext: &str) -> Option<u32> {
|
||||
fn parse_ec_shard_extension(ext: &str) -> Option<ShardId> {
|
||||
let rest = ext.strip_prefix(".ec")?;
|
||||
if rest.len() < 2 || rest.len() > 3 {
|
||||
return None;
|
||||
@@ -1246,7 +1320,7 @@ fn parse_ec_shard_extension(ext: &str) -> Option<u32> {
|
||||
if id > 255 {
|
||||
return None;
|
||||
}
|
||||
Some(id)
|
||||
ShardId::try_from(id).ok()
|
||||
}
|
||||
|
||||
/// Robust check that a `.dat` with actual data exists. An empty `.dat`
|
||||
@@ -1308,7 +1382,10 @@ fn remove_empty_ec_dat_stub(volume_name: &str, idx_name: &str, vid: VolumeId) ->
|
||||
return false;
|
||||
}
|
||||
|
||||
warn!(volume_id = vid.0, "removing leftover empty .dat stub for EC volume");
|
||||
warn!(
|
||||
volume_id = vid.0,
|
||||
"removing leftover empty .dat stub for EC volume"
|
||||
);
|
||||
let _ = fs::remove_file(&dat_path);
|
||||
let _ = fs::remove_file(format!("{}.idx", idx_name));
|
||||
true
|
||||
@@ -1331,6 +1408,17 @@ mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// get_disk_stats must report real capacity for a real path on every
|
||||
/// platform (Windows included) — consumers treat total==0 as "unknown"
|
||||
/// and leave available_space at 0, which breaks volume assignment.
|
||||
#[test]
|
||||
fn test_get_disk_stats_reports_capacity_for_real_path() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let (total, free) = get_disk_stats(tmp.path().to_str().unwrap());
|
||||
assert!(total > 0, "expected total>0, got {total}");
|
||||
assert!(free > 0, "expected free>0, got {free}");
|
||||
}
|
||||
|
||||
/// When `-dir.idx` is configured the EC `.vif` may live in the idx
|
||||
/// directory; the sweep must look there too, not only the data dir.
|
||||
#[test]
|
||||
@@ -1353,7 +1441,11 @@ mod tests {
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
std::fs::write(format!("{}.vif", ibase), serde_json::to_string(&vif).unwrap()).unwrap();
|
||||
std::fs::write(
|
||||
format!("{}.vif", ibase),
|
||||
serde_json::to_string(&vif).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
remove_empty_ec_dat_stub(&vbase, &ibase, VolumeId(42)),
|
||||
@@ -1369,16 +1461,30 @@ mod tests {
|
||||
fn test_validate_ec_volume_partial_dat_next_to_full_shards_keeps() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let dir = tmp.path().to_str().unwrap();
|
||||
let loc = DiskLocation::new(dir, dir, 10, DiskType::HardDrive, MinFreeSpace::Percent(1.0), Vec::new()).unwrap();
|
||||
let loc = DiskLocation::new(
|
||||
dir,
|
||||
dir,
|
||||
10,
|
||||
DiskType::HardDrive,
|
||||
MinFreeSpace::Percent(1.0),
|
||||
Vec::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let base = volume_file_name(dir, "", VolumeId(70));
|
||||
let ds = crate::storage::erasure_coding::ec_shard::DATA_SHARDS_COUNT;
|
||||
let full = calculate_expected_shard_size(30 * 1024 * 1024, ds);
|
||||
for i in 0..ds {
|
||||
std::fs::File::create(format!("{}.ec{:02}", base, i)).unwrap().set_len(full as u64).unwrap();
|
||||
std::fs::File::create(format!("{}.ec{:02}", base, i))
|
||||
.unwrap()
|
||||
.set_len(full as u64)
|
||||
.unwrap();
|
||||
}
|
||||
// Partial .dat: bigger than a superblock so it is not swept as a stub,
|
||||
// but smaller than what these shards encode.
|
||||
std::fs::File::create(format!("{}.dat", base)).unwrap().set_len(5 * 1024 * 1024).unwrap();
|
||||
std::fs::File::create(format!("{}.dat", base))
|
||||
.unwrap()
|
||||
.set_len(5 * 1024 * 1024)
|
||||
.unwrap();
|
||||
assert!(
|
||||
loc.validate_ec_volume("", VolumeId(70)),
|
||||
"full-size shards beside a smaller (stale/partial) .dat must be kept",
|
||||
@@ -1392,15 +1498,29 @@ mod tests {
|
||||
fn test_validate_ec_volume_interrupted_encode_reclaims() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let dir = tmp.path().to_str().unwrap();
|
||||
let loc = DiskLocation::new(dir, dir, 10, DiskType::HardDrive, MinFreeSpace::Percent(1.0), Vec::new()).unwrap();
|
||||
let loc = DiskLocation::new(
|
||||
dir,
|
||||
dir,
|
||||
10,
|
||||
DiskType::HardDrive,
|
||||
MinFreeSpace::Percent(1.0),
|
||||
Vec::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let base = volume_file_name(dir, "", VolumeId(71));
|
||||
let ds = crate::storage::erasure_coding::ec_shard::DATA_SHARDS_COUNT;
|
||||
let dat_size = 30 * 1024 * 1024i64;
|
||||
std::fs::File::create(format!("{}.dat", base)).unwrap().set_len(dat_size as u64).unwrap();
|
||||
std::fs::File::create(format!("{}.dat", base))
|
||||
.unwrap()
|
||||
.set_len(dat_size as u64)
|
||||
.unwrap();
|
||||
let partial = calculate_expected_shard_size(dat_size, ds) / 3;
|
||||
assert!(partial > 0);
|
||||
for i in 0..ds {
|
||||
std::fs::File::create(format!("{}.ec{:02}", base, i)).unwrap().set_len(partial as u64).unwrap();
|
||||
std::fs::File::create(format!("{}.ec{:02}", base, i))
|
||||
.unwrap()
|
||||
.set_len(partial as u64)
|
||||
.unwrap();
|
||||
}
|
||||
assert!(
|
||||
!loc.validate_ec_volume("", VolumeId(71)),
|
||||
@@ -1445,7 +1565,11 @@ mod tests {
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
std::fs::write(format!("{}.vif", dbase), serde_json::to_string(&with_gen).unwrap()).unwrap();
|
||||
std::fs::write(
|
||||
format!("{}.vif", dbase),
|
||||
serde_json::to_string(&with_gen).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(loc.ec_generation_ts_ns("", vid), Some(4242));
|
||||
|
||||
// A .vif with no EC config reads as generation 0 (recovered/pre-upgrade live volume).
|
||||
@@ -1454,12 +1578,20 @@ mod tests {
|
||||
version: 3,
|
||||
..Default::default()
|
||||
};
|
||||
std::fs::write(format!("{}.vif", dbase), serde_json::to_string(&no_cfg).unwrap()).unwrap();
|
||||
std::fs::write(
|
||||
format!("{}.vif", dbase),
|
||||
serde_json::to_string(&no_cfg).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(loc.ec_generation_ts_ns("", vid), Some(0));
|
||||
|
||||
// idx-dir fallback: only the idx dir holds the .vif.
|
||||
std::fs::remove_file(format!("{}.vif", dbase)).unwrap();
|
||||
std::fs::write(format!("{}.vif", ibase), serde_json::to_string(&with_gen).unwrap()).unwrap();
|
||||
std::fs::write(
|
||||
format!("{}.vif", ibase),
|
||||
serde_json::to_string(&with_gen).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(loc.ec_generation_ts_ns("", vid), Some(4242));
|
||||
}
|
||||
|
||||
@@ -1499,16 +1631,8 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
loc.create_volume(
|
||||
VolumeId(1),
|
||||
"",
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
)
|
||||
.unwrap();
|
||||
loc.create_volume(VolumeId(1), NeedleMapKind::InMemory, &VolumeSpec::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(loc.volumes_len(), 1);
|
||||
assert!(loc.find_volume(VolumeId(1)).is_some());
|
||||
@@ -1532,24 +1656,15 @@ mod tests {
|
||||
Vec::new(),
|
||||
)
|
||||
.unwrap();
|
||||
loc.create_volume(
|
||||
VolumeId(1),
|
||||
"",
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
)
|
||||
.unwrap();
|
||||
loc.create_volume(VolumeId(1), NeedleMapKind::InMemory, &VolumeSpec::default())
|
||||
.unwrap();
|
||||
loc.create_volume(
|
||||
VolumeId(2),
|
||||
"test",
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec {
|
||||
collection: "test",
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
loc.close();
|
||||
@@ -1592,12 +1707,11 @@ mod tests {
|
||||
.unwrap();
|
||||
loc.create_volume(
|
||||
VolumeId(9),
|
||||
"good",
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec {
|
||||
collection: "good",
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
loc.close();
|
||||
@@ -1641,29 +1755,13 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
loc.create_volume(
|
||||
VolumeId(1),
|
||||
"",
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
)
|
||||
.unwrap();
|
||||
loc.create_volume(
|
||||
VolumeId(2),
|
||||
"",
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
)
|
||||
.unwrap();
|
||||
loc.create_volume(VolumeId(1), NeedleMapKind::InMemory, &VolumeSpec::default())
|
||||
.unwrap();
|
||||
loc.create_volume(VolumeId(2), NeedleMapKind::InMemory, &VolumeSpec::default())
|
||||
.unwrap();
|
||||
assert_eq!(loc.volumes_len(), 2);
|
||||
|
||||
loc.delete_volume(VolumeId(1), false, false).unwrap();
|
||||
loc.delete_volume(VolumeId(1), false, false, false).unwrap();
|
||||
assert_eq!(loc.volumes_len(), 1);
|
||||
assert!(loc.find_volume(VolumeId(1)).is_none());
|
||||
}
|
||||
@@ -1684,32 +1782,29 @@ mod tests {
|
||||
|
||||
loc.create_volume(
|
||||
VolumeId(1),
|
||||
"pics",
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec {
|
||||
collection: "pics",
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
loc.create_volume(
|
||||
VolumeId(2),
|
||||
"pics",
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec {
|
||||
collection: "pics",
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
loc.create_volume(
|
||||
VolumeId(3),
|
||||
"docs",
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec {
|
||||
collection: "docs",
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(loc.volumes_len(), 3);
|
||||
@@ -1719,6 +1814,34 @@ mod tests {
|
||||
assert!(loc.find_volume(VolumeId(3)).is_some());
|
||||
}
|
||||
|
||||
/// A 0-byte `.ecx` is the stub a failed EC distribute copy leaves behind.
|
||||
/// Go's HasEcxFileOnDisk requires Size() > 0 so the stub cannot pin
|
||||
/// placement to a disk that has no usable index.
|
||||
#[test]
|
||||
fn test_has_ecx_file_on_disk_ignores_zero_byte_stub() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let data = tmp.path().join("data");
|
||||
let idx = tmp.path().join("idx");
|
||||
fs::create_dir_all(&data).unwrap();
|
||||
fs::create_dir_all(&idx).unwrap();
|
||||
let loc = DiskLocation::new(
|
||||
data.to_str().unwrap(),
|
||||
idx.to_str().unwrap(),
|
||||
10,
|
||||
DiskType::HardDrive,
|
||||
MinFreeSpace::Percent(1.0),
|
||||
Vec::new(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
fs::write(idx.join("pics_7.ecx"), b"").unwrap();
|
||||
assert!(!loc.has_ecx_file_on_disk("pics", VolumeId(7)));
|
||||
|
||||
// A real index in the data dir still counts, stub or no stub.
|
||||
fs::write(data.join("pics_7.ecx"), [0u8; 16]).unwrap();
|
||||
assert!(loc.has_ecx_file_on_disk("pics", VolumeId(7)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disk_location_delete_collection_removes_ec_volumes() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
@@ -1735,6 +1858,8 @@ mod tests {
|
||||
|
||||
let shard_path = format!("{}/pics_7.ec00", dir);
|
||||
std::fs::write(&shard_path, b"ec-shard").unwrap();
|
||||
// An EC volume needs its .ecx to mount.
|
||||
std::fs::write(format!("{}/pics_7.ecx", dir), [0u8; 16]).unwrap();
|
||||
|
||||
loc.mount_ec_shards(VolumeId(7), "pics", &[0], "").unwrap();
|
||||
assert!(loc.has_ec_volume(VolumeId(7)));
|
||||
@@ -1772,7 +1897,9 @@ mod tests {
|
||||
// mount_ec_shards with source_disk_type="ssd" — simulating the
|
||||
// VolumeEcShardsMount RPC path.
|
||||
std::fs::write(format!("{}/pics_7.ec00", dir), b"ec-shard").unwrap();
|
||||
loc.mount_ec_shards(VolumeId(7), "pics", &[0], "ssd").unwrap();
|
||||
std::fs::write(format!("{}/pics_7.ecx", dir), [0u8; 16]).unwrap();
|
||||
loc.mount_ec_shards(VolumeId(7), "pics", &[0], "ssd")
|
||||
.unwrap();
|
||||
{
|
||||
let ec_vol = loc.find_ec_volume(VolumeId(7)).expect("ec volume mounted");
|
||||
assert_eq!(
|
||||
@@ -1789,7 +1916,9 @@ mod tests {
|
||||
std::fs::write(format!("{}/pics_7.ec01", dir), b"ec-shard").unwrap();
|
||||
loc.mount_ec_shards(VolumeId(7), "pics", &[1], "").unwrap();
|
||||
{
|
||||
let ec_vol = loc.find_ec_volume(VolumeId(7)).expect("ec volume still mounted");
|
||||
let ec_vol = loc
|
||||
.find_ec_volume(VolumeId(7))
|
||||
.expect("ec volume still mounted");
|
||||
assert_eq!(
|
||||
ec_vol.disk_type,
|
||||
DiskType::Ssd,
|
||||
@@ -1868,10 +1997,12 @@ mod tests {
|
||||
// A collection name unique to this test: the gauge is process-global
|
||||
// and sibling tests running in parallel touch other labels.
|
||||
std::fs::write(format!("{}/dupmount_11.ec00", dir), b"shard bytes").unwrap();
|
||||
std::fs::write(format!("{}/dupmount_11.ecx", dir), [0u8; 16]).unwrap();
|
||||
let gauge = crate::metrics::VOLUME_GAUGE.with_label_values(&["dupmount", "ec_shards"]);
|
||||
let before = gauge.get();
|
||||
|
||||
loc.mount_ec_shards(VolumeId(11), "dupmount", &[0], "").unwrap();
|
||||
loc.mount_ec_shards(VolumeId(11), "dupmount", &[0], "")
|
||||
.unwrap();
|
||||
loc.mount_ec_shards(VolumeId(11), "dupmount", &[0], "")
|
||||
.expect("a duplicate mount must succeed as a no-op");
|
||||
|
||||
@@ -1947,8 +2078,11 @@ mod tests {
|
||||
let path = format!("{}/{}_{}.ec{:02}", dir, collection, vid.0, sid);
|
||||
std::fs::write(&path, b"shard data nonempty").unwrap();
|
||||
}
|
||||
std::fs::write(format!("{}/{}_{}.ecx", dir, collection, vid.0), vec![0u8; 20])
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
format!("{}/{}_{}.ecx", dir, collection, vid.0),
|
||||
vec![0u8; 20],
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(format!("{}/{}_{}.ecj", dir, collection, vid.0), b"").unwrap();
|
||||
std::fs::write(
|
||||
format!("{}/{}_{}.vif", dir, collection, vid.0),
|
||||
|
||||
@@ -30,6 +30,7 @@ use crate::pb::volume_server_pb::{
|
||||
ChecksumAlgorithm, EcBitrotProtection, EcShardChecksums, EcShardConfig,
|
||||
};
|
||||
use crate::storage::erasure_coding::ec_shard::MAX_SHARD_COUNT;
|
||||
use crate::storage::io::read_exact_at;
|
||||
use crate::storage::needle::crc::CRC;
|
||||
|
||||
/// Canonical extension for the checksum sidecar. Generation 0 (legacy/fresh
|
||||
@@ -164,16 +165,16 @@ pub fn remove_bitrot_sidecars(base: &str) -> io::Result<()> {
|
||||
};
|
||||
let mut first_err: Option<io::Error> = None;
|
||||
let mut record = |res: io::Result<()>| {
|
||||
if let Err(e) = res {
|
||||
if first_err.is_none() {
|
||||
first_err = Some(e);
|
||||
}
|
||||
if let Err(e) = res
|
||||
&& first_err.is_none()
|
||||
{
|
||||
first_err = Some(e);
|
||||
}
|
||||
};
|
||||
record(rm(format!("{}{}", base, BITROT_SIDECAR_EXT).into()));
|
||||
let path = Path::new(base);
|
||||
if let (Some(parent), Some(fname)) = (path.parent(), path.file_name()) {
|
||||
let prefix = format!("{}{}.v", fname.to_string_lossy(), BITROT_SIDECAR_EXT);
|
||||
let prefix = format!("{}{}.v", fname.display(), BITROT_SIDECAR_EXT);
|
||||
match fs::read_dir(parent) {
|
||||
Ok(entries) => {
|
||||
for entry in entries.flatten() {
|
||||
@@ -203,7 +204,7 @@ pub fn new_encode_uuid() -> Vec<u8> {
|
||||
|
||||
/// Reports whether `block_size` is a power of two in [1 MiB, MAX_BITROT_BLOCK_SIZE].
|
||||
pub fn is_pow2_multiple_of_1mib(block_size: u32) -> bool {
|
||||
block_size >= (1 << 20) && block_size <= MAX_BITROT_BLOCK_SIZE && block_size.count_ones() == 1
|
||||
((1 << 20)..=MAX_BITROT_BLOCK_SIZE).contains(&block_size) && block_size.count_ones() == 1
|
||||
}
|
||||
|
||||
/// Returns ceil(covered_size / block_size).
|
||||
@@ -402,7 +403,7 @@ pub fn validate_manifest(
|
||||
total
|
||||
));
|
||||
}
|
||||
let mut seen = vec![false; MAX_SHARD_COUNT];
|
||||
let mut seen = [false; MAX_SHARD_COUNT];
|
||||
for s in &prot.shards {
|
||||
if s.shard_id >= total as u32 {
|
||||
return Err(format!(
|
||||
@@ -537,7 +538,7 @@ pub fn verify_shard_blocks(
|
||||
break;
|
||||
}
|
||||
let to_read = to_read as usize;
|
||||
read_full_at(f, &mut buf[..to_read], offset as u64)?;
|
||||
read_exact_at(f, &mut buf[..to_read], offset as u64)?;
|
||||
if CRC::new(&buf[..to_read]).0 != *want_crc {
|
||||
mismatched.push(i);
|
||||
}
|
||||
@@ -546,33 +547,6 @@ pub fn verify_shard_blocks(
|
||||
Ok(mismatched)
|
||||
}
|
||||
|
||||
/// Reads exactly `buf.len()` bytes from `f` at `offset`, erroring on early EOF.
|
||||
fn read_full_at(f: &File, buf: &mut [u8], offset: u64) -> io::Result<()> {
|
||||
let mut total = 0usize;
|
||||
while total < buf.len() {
|
||||
#[cfg(unix)]
|
||||
let n = {
|
||||
use std::os::unix::fs::FileExt;
|
||||
f.read_at(&mut buf[total..], offset + total as u64)?
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
let n = {
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
let mut fc = f.try_clone()?;
|
||||
fc.seek(SeekFrom::Start(offset + total as u64))?;
|
||||
fc.read(&mut buf[total..])?
|
||||
};
|
||||
if n == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"short read on shard block",
|
||||
));
|
||||
}
|
||||
total += n;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds the `EcShardConfig` proto for the given layout. The bitrot sidecar
|
||||
/// carries its own top-level encode_uuid, so the nested config leaves it empty.
|
||||
pub fn ec_shard_config(data_shards: u32, parity_shards: u32, block_size: i64) -> EcShardConfig {
|
||||
@@ -627,7 +601,10 @@ mod tests {
|
||||
save_bitrot_sidecar(path, &prot).unwrap();
|
||||
let bytes = std::fs::read(path).unwrap();
|
||||
let hex: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
|
||||
assert_eq!(hex, CANONICAL_HEX, "Rust .ecsum bytes drifted from the Go canonical form");
|
||||
assert_eq!(
|
||||
hex, CANONICAL_HEX,
|
||||
"Rust .ecsum bytes drifted from the Go canonical form"
|
||||
);
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
@@ -662,7 +639,11 @@ mod tests {
|
||||
format!("{}.ecsum.v1", base),
|
||||
format!("{}.ecsum.v7", base),
|
||||
] {
|
||||
assert!(!std::path::Path::new(&p).exists(), "{} should be removed", p);
|
||||
assert!(
|
||||
!std::path::Path::new(&p).exists(),
|
||||
"{} should be removed",
|
||||
p
|
||||
);
|
||||
}
|
||||
assert!(std::path::Path::new(&keep_shard).exists());
|
||||
assert!(std::path::Path::new(&keep_other_vid).exists());
|
||||
@@ -681,7 +662,9 @@ mod tests {
|
||||
assert!(!is_pow2_multiple_of_1mib(1 << 19)); // 512 KiB, too small
|
||||
assert!(!is_pow2_multiple_of_1mib(3 << 20)); // 3 MiB, not pow2
|
||||
assert!(!is_pow2_multiple_of_1mib(128 * 1024 * 1024)); // pow2 but > MAX_BITROT_BLOCK_SIZE
|
||||
assert!(!is_pow2_multiple_of_1mib(DEFAULT_BITROT_BLOCK_SIZE as u32 + 1));
|
||||
assert!(!is_pow2_multiple_of_1mib(
|
||||
DEFAULT_BITROT_BLOCK_SIZE as u32 + 1
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -735,12 +718,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_save_load_roundtrip() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let path = tmp
|
||||
.path()
|
||||
.join("vol.ecsum")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let path = tmp.path().join("vol.ecsum").to_str().unwrap().to_string();
|
||||
|
||||
let mut builder = ShardChecksumBuilder::new(DEFAULT_BITROT_BLOCK_SIZE as i64);
|
||||
builder.write(b"hello world");
|
||||
@@ -901,8 +879,7 @@ mod tests {
|
||||
assert_eq!(resolve_status(¬found, 0, 10, 4), BitrotStatus::Off);
|
||||
|
||||
// Integrity failure => Invalid.
|
||||
let bad: Result<EcBitrotProtection, BitrotLoadError> =
|
||||
Err(BitrotLoadError::BadMagic(0));
|
||||
let bad: Result<EcBitrotProtection, BitrotLoadError> = Err(BitrotLoadError::BadMagic(0));
|
||||
assert_eq!(resolve_status(&bad, 0, 10, 4), BitrotStatus::Invalid);
|
||||
|
||||
// Generation mismatch => Off.
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
//! Rebuilds the original .dat + .idx files from data shards (.ec00-.ec09)
|
||||
//! and the sorted index (.ecx) + deletion journal (.ecj).
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read, Write};
|
||||
|
||||
use crate::storage::erasure_coding::ec_shard::*;
|
||||
use crate::storage::erasure_coding::ec_volume::read_ecj_ids;
|
||||
use crate::storage::idx;
|
||||
use crate::storage::needle::needle::get_actual_size;
|
||||
use crate::storage::super_block::SUPER_BLOCK_SIZE;
|
||||
@@ -20,18 +22,21 @@ use crate::storage::volume::{fsync_dir, volume_file_name};
|
||||
/// `dir` is used both for reading `.ec00` and `.ecx`. For split-disk
|
||||
/// reconciled volumes call [`find_dat_file_size_with_dirs`] instead.
|
||||
pub fn find_dat_file_size(dir: &str, collection: &str, volume_id: VolumeId) -> io::Result<i64> {
|
||||
find_dat_file_size_with_dirs(dir, dir, collection, volume_id)
|
||||
let deleted = read_ecj_deletions(&[dir], collection, volume_id)?;
|
||||
find_dat_file_size_with_dirs(dir, dir, collection, volume_id, &deleted)
|
||||
}
|
||||
|
||||
/// Like [`find_dat_file_size`] but lets the caller pass separate dirs
|
||||
/// for `.ec00` (the data shard) and `.ecx` (the sealed index). This
|
||||
/// is the form needed when shards are split across data dirs and the
|
||||
/// `.ecx` lives on a sibling disk's idx dir (#9252).
|
||||
/// `.ecx` lives on a sibling disk's idx dir (#9252). Needles in `deleted`
|
||||
/// count as deleted.
|
||||
pub fn find_dat_file_size_with_dirs(
|
||||
ec00_dir: &str,
|
||||
ecx_dir: &str,
|
||||
collection: &str,
|
||||
volume_id: VolumeId,
|
||||
deleted: &HashSet<NeedleId>,
|
||||
) -> io::Result<i64> {
|
||||
let ec00_base = volume_file_name(ec00_dir, collection, volume_id);
|
||||
let ecx_base = volume_file_name(ecx_dir, collection, volume_id);
|
||||
@@ -53,9 +58,9 @@ pub fn find_dat_file_size_with_dirs(
|
||||
|
||||
for i in 0..entry_count {
|
||||
let start = i * NEEDLE_MAP_ENTRY_SIZE;
|
||||
let (_, offset, size) =
|
||||
let (key, offset, size) =
|
||||
idx_entry_from_bytes(&ecx_data[start..start + NEEDLE_MAP_ENTRY_SIZE]);
|
||||
if size.is_deleted() {
|
||||
if size.is_deleted() || deleted.contains(&key) {
|
||||
continue;
|
||||
}
|
||||
let entry_stop = offset.to_actual_offset() + get_actual_size(size, version);
|
||||
@@ -67,72 +72,87 @@ pub fn find_dat_file_size_with_dirs(
|
||||
Ok(dat_size)
|
||||
}
|
||||
|
||||
/// Reconstruct a .dat file from EC data shards.
|
||||
///
|
||||
/// Reads from .ec00-.ec09 and writes a new .dat file. All data shards
|
||||
/// must live in `dir`. For the cross-disk reconciled layout where
|
||||
/// shards are split across multiple data dirs of the same node, use
|
||||
/// [`write_dat_file_from_shards_with_dirs`] instead.
|
||||
pub fn write_dat_file_from_shards(
|
||||
dir: &str,
|
||||
/// Whether the `.ecx` in `ecx_dir` indexes a needle deleted neither there nor
|
||||
/// in `deleted`.
|
||||
pub fn has_live_needles(
|
||||
ecx_dir: &str,
|
||||
collection: &str,
|
||||
volume_id: VolumeId,
|
||||
dat_file_size: i64,
|
||||
encoded_dat_file_size: i64,
|
||||
data_shards: usize,
|
||||
large_block_size: usize,
|
||||
small_block_size: usize,
|
||||
) -> io::Result<()> {
|
||||
let dirs: Vec<String> = (0..data_shards).map(|_| dir.to_string()).collect();
|
||||
write_dat_file_from_shards_with_dirs(
|
||||
dir,
|
||||
collection,
|
||||
volume_id,
|
||||
dat_file_size,
|
||||
encoded_dat_file_size,
|
||||
data_shards,
|
||||
&dirs,
|
||||
large_block_size,
|
||||
small_block_size,
|
||||
)
|
||||
deleted: &HashSet<NeedleId>,
|
||||
) -> io::Result<bool> {
|
||||
let ecx_base = volume_file_name(ecx_dir, collection, volume_id);
|
||||
let ecx_data = std::fs::read(format!("{}.ecx", ecx_base))?;
|
||||
let (entries, _) = ecx_data.as_chunks::<NEEDLE_MAP_ENTRY_SIZE>();
|
||||
Ok(entries.iter().any(|entry| {
|
||||
let (key, _, size) = idx_entry_from_bytes(entry);
|
||||
!size.is_deleted() && !deleted.contains(&key)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Reconstruct a .dat file from EC data shards, taking the source
|
||||
/// directory for each shard separately.
|
||||
///
|
||||
/// `dat_dir` is where the produced `.dat` is written. `shard_dirs[i]`
|
||||
/// is the directory holding shard `i`. For the simple "all shards in
|
||||
/// one dir" case both can be the same value.
|
||||
/// Distinct needle ids journaled in the `.ecj` of any of `dirs`. Go folds the
|
||||
/// journal into the `.ecx` (RebuildEcxFile) before a decode; reading it leaves
|
||||
/// the sealed index untouched. Only NotFound means "no journal".
|
||||
pub fn read_ecj_deletions(
|
||||
dirs: &[&str],
|
||||
collection: &str,
|
||||
volume_id: VolumeId,
|
||||
) -> io::Result<HashSet<NeedleId>> {
|
||||
let mut ids = HashSet::new();
|
||||
for (i, dir) in dirs.iter().enumerate() {
|
||||
if dirs[..i].contains(dir) {
|
||||
continue;
|
||||
}
|
||||
let path = format!("{}.ecj", volume_file_name(dir, collection, volume_id));
|
||||
let file = match File::open(&path) {
|
||||
Ok(file) => file,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let len = file.metadata()?.len();
|
||||
read_ecj_ids(&file, len, &mut ids)?;
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/// What it takes to rebuild a volume's .dat from its EC data shards.
|
||||
///
|
||||
/// Mirrors Go's `WriteDatFile(baseFileName, datFileSize,
|
||||
/// encodedDatFileSize, shardFileNames)` shape — Go passes per-shard
|
||||
/// paths so a reconciled volume with shards split across disks of the
|
||||
/// same volume server can still be decoded back to a regular .dat
|
||||
/// (seaweedfs/seaweedfs#9252).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct DatRebuild<'a> {
|
||||
/// Where the produced `.dat` is written.
|
||||
pub dat_dir: &'a str,
|
||||
pub collection: &'a str,
|
||||
pub volume_id: VolumeId,
|
||||
/// The number of bytes to write, i.e. the live data extent from
|
||||
/// [`find_dat_file_size`].
|
||||
pub dat_file_size: i64,
|
||||
/// The .dat size at encode time, which fixed the shard block layout:
|
||||
/// deletions can move the live extent below the large-block row
|
||||
/// boundary, and deriving the layout from the shrunk extent would read
|
||||
/// the shards in the wrong block order. Zero when the .vif does not
|
||||
/// record the encode-time size; the layout is then inferred from the
|
||||
/// shard size.
|
||||
pub encoded_dat_file_size: i64,
|
||||
pub data_shards: usize,
|
||||
/// `shard_dirs[i]` is the directory holding shard `i`. `None` means every
|
||||
/// data shard sits in `dat_dir`.
|
||||
pub shard_dirs: Option<&'a [String]>,
|
||||
/// The volume's shard block layout, e.g. `EcVolume::large_block_size()`
|
||||
/// / `small_block_size()` from its .vif EC config.
|
||||
pub large_block_size: usize,
|
||||
pub small_block_size: usize,
|
||||
}
|
||||
|
||||
/// Reconstruct a .dat file from EC data shards.
|
||||
///
|
||||
/// `dat_file_size` is the number of bytes to write, i.e. the live data
|
||||
/// extent from [`find_dat_file_size`]. `encoded_dat_file_size` is the
|
||||
/// .dat size at encode time, which fixed the shard block layout:
|
||||
/// deletions can move the live extent below the large-block row
|
||||
/// boundary, and deriving the layout from the shrunk extent would read
|
||||
/// the shards in the wrong block order. Pass zero when the .vif does
|
||||
/// not record the encode-time size to infer the layout from the shard
|
||||
/// size. `large_block_size`/`small_block_size` are the volume's shard
|
||||
/// block layout, e.g. `EcVolume::large_block_size()` /
|
||||
/// `small_block_size()` from its .vif EC config.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn write_dat_file_from_shards_with_dirs(
|
||||
dat_dir: &str,
|
||||
collection: &str,
|
||||
volume_id: VolumeId,
|
||||
dat_file_size: i64,
|
||||
encoded_dat_file_size: i64,
|
||||
data_shards: usize,
|
||||
shard_dirs: &[String],
|
||||
large_block_size: usize,
|
||||
small_block_size: usize,
|
||||
) -> io::Result<()> {
|
||||
write_dat_file(
|
||||
/// Reads from .ec00-.ec09 and writes a new .dat file, from one directory or
|
||||
/// from the per-shard directories of a cross-disk reconciled volume.
|
||||
pub fn write_dat_file_from_shards(spec: &DatRebuild<'_>) -> io::Result<()> {
|
||||
let DatRebuild {
|
||||
dat_dir,
|
||||
collection,
|
||||
volume_id,
|
||||
@@ -142,21 +162,15 @@ pub fn write_dat_file_from_shards_with_dirs(
|
||||
shard_dirs,
|
||||
large_block_size,
|
||||
small_block_size,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn write_dat_file(
|
||||
dat_dir: &str,
|
||||
collection: &str,
|
||||
volume_id: VolumeId,
|
||||
dat_file_size: i64,
|
||||
encoded_dat_file_size: i64,
|
||||
data_shards: usize,
|
||||
shard_dirs: &[String],
|
||||
large_block_size: usize,
|
||||
small_block_size: usize,
|
||||
) -> io::Result<()> {
|
||||
} = *spec;
|
||||
let same_dir: Vec<String>;
|
||||
let shard_dirs: &[String] = match shard_dirs {
|
||||
Some(dirs) => dirs,
|
||||
None => {
|
||||
same_dir = vec![dat_dir.to_string(); data_shards];
|
||||
&same_dir
|
||||
}
|
||||
};
|
||||
if data_shards == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
@@ -233,10 +247,10 @@ fn write_dat_file(
|
||||
|
||||
// Read large blocks
|
||||
while encoded_remaining >= large_row_size && remaining > 0 {
|
||||
for i in 0..data_shards {
|
||||
for (i, shard) in shards[..data_shards].iter().enumerate() {
|
||||
let to_write = large_block_size.min(remaining as usize);
|
||||
let mut buf = vec![0u8; to_write];
|
||||
let n = shards[i].read_at(&mut buf, shard_offset)?;
|
||||
let n = shard.read_at(&mut buf, shard_offset)?;
|
||||
if n != to_write {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
@@ -255,10 +269,10 @@ fn write_dat_file(
|
||||
|
||||
// Read small blocks
|
||||
while remaining > 0 {
|
||||
for i in 0..data_shards {
|
||||
for (i, shard) in shards[..data_shards].iter().enumerate() {
|
||||
let to_write = small_block_size.min(remaining as usize);
|
||||
let mut buf = vec![0u8; to_write];
|
||||
let n = shards[i].read_at(&mut buf, shard_offset)?;
|
||||
let n = shard.read_at(&mut buf, shard_offset)?;
|
||||
if n != to_write {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
@@ -300,54 +314,61 @@ fn write_dat_file(
|
||||
|
||||
/// Write .idx file from .ecx index + .ecj deletion journal.
|
||||
///
|
||||
/// Copies sorted .ecx entries to .idx, then appends tombstones for
|
||||
/// deleted needles from .ecj.
|
||||
/// See [`write_idx_file_from_ec_index_with_dirs`]; everything lives in `dir`.
|
||||
pub fn write_idx_file_from_ec_index(
|
||||
dir: &str,
|
||||
collection: &str,
|
||||
volume_id: VolumeId,
|
||||
) -> io::Result<()> {
|
||||
let base = volume_file_name(dir, collection, volume_id);
|
||||
let ecx_path = format!("{}.ecx", base);
|
||||
let ecj_path = format!("{}.ecj", base);
|
||||
let idx_path = format!("{}.idx", base);
|
||||
let deleted = read_ecj_deletions(&[dir], collection, volume_id)?;
|
||||
let dat_file_size = find_dat_file_size_with_dirs(dir, dir, collection, volume_id, &deleted)?;
|
||||
write_idx_file_from_ec_index_with_dirs(dir, dir, collection, volume_id, &deleted, dat_file_size)
|
||||
}
|
||||
|
||||
/// Write the `.idx` for a `.dat` decoded to `dat_file_size` bytes, from the
|
||||
/// `.ecx` in `ecx_dir`, into `idx_dir`.
|
||||
///
|
||||
/// Copies the `.ecx` rows, then appends one tombstone per row whose needle is
|
||||
/// in `deleted`. A deleted needle at or past `dat_file_size` was cut from the
|
||||
/// `.dat`, so its row is dropped: a row pointing past the end of the `.dat`
|
||||
/// makes the volume load read-only.
|
||||
pub fn write_idx_file_from_ec_index_with_dirs(
|
||||
ecx_dir: &str,
|
||||
idx_dir: &str,
|
||||
collection: &str,
|
||||
volume_id: VolumeId,
|
||||
deleted: &HashSet<NeedleId>,
|
||||
dat_file_size: i64,
|
||||
) -> io::Result<()> {
|
||||
let ecx_path = format!("{}.ecx", volume_file_name(ecx_dir, collection, volume_id));
|
||||
let idx_path = format!("{}.idx", volume_file_name(idx_dir, collection, volume_id));
|
||||
// Write to a temp file and atomically rename into place, so a crash
|
||||
// mid-write never leaves a partial .idx at the final name beside the
|
||||
// source shards.
|
||||
let tmp_path = format!("{}.tmp", idx_path);
|
||||
|
||||
let write_result = (|| -> io::Result<()> {
|
||||
// Copy .ecx to the temp .idx
|
||||
std::fs::copy(&ecx_path, &tmp_path)?;
|
||||
|
||||
// Append deletions from .ecj as tombstones. Read the journal directly
|
||||
// and treat only NotFound as "no journal": Path::exists would also
|
||||
// swallow a permission/IO error and silently skip deletions, which
|
||||
// would resurrect deleted needles as live.
|
||||
let mut idx_file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.append(true)
|
||||
.open(&tmp_path)?;
|
||||
match std::fs::read(&ecj_path) {
|
||||
Ok(ecj_data) => {
|
||||
let count = ecj_data.len() / NEEDLE_ID_SIZE;
|
||||
for i in 0..count {
|
||||
let start = i * NEEDLE_ID_SIZE;
|
||||
let needle_id = NeedleId::from_bytes(&ecj_data[start..start + NEEDLE_ID_SIZE]);
|
||||
idx::write_index_entry(
|
||||
&mut idx_file,
|
||||
needle_id,
|
||||
Offset::default(),
|
||||
TOMBSTONE_FILE_SIZE,
|
||||
)?;
|
||||
}
|
||||
let mut ecx_file = File::open(&ecx_path)?;
|
||||
let mut idx_file = io::BufWriter::new(File::create(&tmp_path)?);
|
||||
let mut tombstoned = Vec::new();
|
||||
idx::walk_index_file(&mut ecx_file, 0, |key, offset, size| {
|
||||
let is_deleted = size.is_deleted() || deleted.contains(&key);
|
||||
if is_deleted && offset.to_actual_offset() >= dat_file_size {
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(e),
|
||||
idx::write_index_entry(&mut idx_file, key, offset, size)?;
|
||||
if !size.is_deleted() && deleted.contains(&key) {
|
||||
tombstoned.push(key);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
for key in tombstoned {
|
||||
idx::write_index_entry(&mut idx_file, key, Offset::default(), TOMBSTONE_FILE_SIZE)?;
|
||||
}
|
||||
|
||||
// fsync, rename, then fsync the dir so the decoded .idx is durable and
|
||||
// atomically published before the caller deletes the source shards.
|
||||
let idx_file = idx_file.into_inner().map_err(|e| e.into_error())?;
|
||||
idx_file.sync_all()?;
|
||||
drop(idx_file);
|
||||
// Windows rename does not replace an existing file on every version;
|
||||
@@ -372,7 +393,7 @@ mod tests {
|
||||
use crate::storage::erasure_coding::ec_encoder;
|
||||
use crate::storage::needle::needle::Needle;
|
||||
use crate::storage::needle_map::NeedleMapKind;
|
||||
use crate::storage::volume::Volume;
|
||||
use crate::storage::volume::{Volume, VolumeSpec};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
@@ -384,13 +405,9 @@ mod tests {
|
||||
let mut v = Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -430,16 +447,17 @@ mod tests {
|
||||
std::fs::remove_file(format!("{}/1.idx", dir)).unwrap();
|
||||
|
||||
// Reconstruct from EC shards
|
||||
write_dat_file_from_shards(
|
||||
dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
original_dat_size as i64,
|
||||
original_dat_size as i64,
|
||||
write_dat_file_from_shards(&DatRebuild {
|
||||
dat_dir: dir,
|
||||
collection: "",
|
||||
volume_id: VolumeId(1),
|
||||
dat_file_size: original_dat_size as i64,
|
||||
encoded_dat_file_size: original_dat_size as i64,
|
||||
data_shards,
|
||||
block_size as usize,
|
||||
block_size as usize,
|
||||
)
|
||||
shard_dirs: None,
|
||||
large_block_size: block_size as usize,
|
||||
small_block_size: block_size as usize,
|
||||
})
|
||||
.unwrap();
|
||||
write_idx_file_from_ec_index(dir, "", VolumeId(1)).unwrap();
|
||||
|
||||
@@ -459,13 +477,9 @@ mod tests {
|
||||
let v2 = Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -485,29 +499,29 @@ mod tests {
|
||||
let dir = tmp.path().to_str().unwrap();
|
||||
// No shard files exist, so de-striping must fail and publish nothing:
|
||||
// neither the final .dat nor a partial .dat.tmp may remain.
|
||||
let res = write_dat_file_from_shards(
|
||||
dir,
|
||||
"",
|
||||
VolumeId(7),
|
||||
100,
|
||||
100,
|
||||
10,
|
||||
ERASURE_CODING_LARGE_BLOCK_SIZE,
|
||||
ERASURE_CODING_SMALL_BLOCK_SIZE,
|
||||
);
|
||||
let res = write_dat_file_from_shards(&DatRebuild {
|
||||
dat_dir: dir,
|
||||
collection: "",
|
||||
volume_id: VolumeId(7),
|
||||
dat_file_size: 100,
|
||||
encoded_dat_file_size: 100,
|
||||
data_shards: 10,
|
||||
shard_dirs: None,
|
||||
large_block_size: ERASURE_CODING_LARGE_BLOCK_SIZE,
|
||||
small_block_size: ERASURE_CODING_SMALL_BLOCK_SIZE,
|
||||
});
|
||||
assert!(res.is_err());
|
||||
assert!(!std::path::Path::new(&format!("{}/7.dat", dir)).exists());
|
||||
assert!(!std::path::Path::new(&format!("{}/7.dat.tmp", dir)).exists());
|
||||
}
|
||||
|
||||
|
||||
// Decoding when .vif does not record the encode-time size: the layout is
|
||||
// inferred from the shard size, except when that is an exact large-block
|
||||
// multiple and the live extent reaches the ambiguous region.
|
||||
#[test]
|
||||
fn test_write_dat_file_fallback_layout() {
|
||||
use crate::storage::erasure_coding::ec_bitrot::{
|
||||
ShardChecksumBuilder, DEFAULT_BITROT_BLOCK_SIZE,
|
||||
DEFAULT_BITROT_BLOCK_SIZE, ShardChecksumBuilder,
|
||||
};
|
||||
use reed_solomon_erasure::galois_8::ReedSolomon;
|
||||
|
||||
@@ -545,11 +559,13 @@ mod tests {
|
||||
&rs,
|
||||
&mut shards,
|
||||
&mut builders,
|
||||
data_shards,
|
||||
parity_shards,
|
||||
SMALL,
|
||||
LARGE,
|
||||
SMALL,
|
||||
ec_encoder::EcEncodeLayout {
|
||||
data_shards,
|
||||
parity_shards,
|
||||
buffer_size: SMALL,
|
||||
large_block_size: LARGE,
|
||||
small_block_size: SMALL,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
for shard in &mut shards {
|
||||
@@ -567,7 +583,17 @@ mod tests {
|
||||
-> io::Result<Vec<u8>> {
|
||||
let out = format!("{}/{}", dir, sub);
|
||||
std::fs::create_dir_all(&out).unwrap();
|
||||
write_dat_file(&out, "", VolumeId(1), live, encoded, 10, shard_dirs, LARGE, SMALL)?;
|
||||
write_dat_file_from_shards(&DatRebuild {
|
||||
dat_dir: &out,
|
||||
collection: "",
|
||||
volume_id: VolumeId(1),
|
||||
dat_file_size: live,
|
||||
encoded_dat_file_size: encoded,
|
||||
data_shards: 10,
|
||||
shard_dirs: Some(shard_dirs),
|
||||
large_block_size: LARGE,
|
||||
small_block_size: SMALL,
|
||||
})?;
|
||||
Ok(std::fs::read(format!("{}/1.dat", out)).unwrap())
|
||||
};
|
||||
|
||||
@@ -581,14 +607,20 @@ mod tests {
|
||||
// each shard exactly one large block, indistinguishable from one large row
|
||||
let (dir, shard_dirs, _) = encode("ambig1", large_row_size - 1);
|
||||
let err = decode_to(&dir, "out", large_row_size / 2, 0, &shard_dirs).unwrap_err();
|
||||
assert!(err.to_string().contains("does not identify the block layout"));
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("does not identify the block layout")
|
||||
);
|
||||
|
||||
// two-row equivalent: decoding within the agreed prefix still works
|
||||
let (dir, shard_dirs, original) = encode("ambig2", 2 * large_row_size - 1);
|
||||
let decoded = decode_to(&dir, "outa", large_row_size, 0, &shard_dirs).unwrap();
|
||||
assert_eq!(&original[..large_row_size as usize], &decoded[..]);
|
||||
let err = decode_to(&dir, "outb", large_row_size + 1, 0, &shard_dirs).unwrap_err();
|
||||
assert!(err.to_string().contains("does not identify the block layout"));
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("does not identify the block layout")
|
||||
);
|
||||
}
|
||||
|
||||
// Decoding after deletions moved the live extent below the large-block row
|
||||
@@ -597,7 +629,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_write_dat_file_after_tail_deletion() {
|
||||
use crate::storage::erasure_coding::ec_bitrot::{
|
||||
ShardChecksumBuilder, DEFAULT_BITROT_BLOCK_SIZE,
|
||||
DEFAULT_BITROT_BLOCK_SIZE, ShardChecksumBuilder,
|
||||
};
|
||||
use reed_solomon_erasure::galois_8::ReedSolomon;
|
||||
|
||||
@@ -637,11 +669,13 @@ mod tests {
|
||||
&rs,
|
||||
&mut shards,
|
||||
&mut builders,
|
||||
data_shards,
|
||||
parity_shards,
|
||||
SMALL,
|
||||
LARGE,
|
||||
SMALL,
|
||||
ec_encoder::EcEncodeLayout {
|
||||
data_shards,
|
||||
parity_shards,
|
||||
buffer_size: SMALL,
|
||||
large_block_size: LARGE,
|
||||
small_block_size: SMALL,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
for shard in &mut shards {
|
||||
@@ -657,17 +691,17 @@ mod tests {
|
||||
std::fs::create_dir(&out_dir).unwrap();
|
||||
let out = out_dir.to_str().unwrap();
|
||||
let decode = |live_size: i64, encoded_size: i64| -> Vec<u8> {
|
||||
write_dat_file(
|
||||
out,
|
||||
"",
|
||||
VolumeId(1),
|
||||
live_size,
|
||||
encoded_size,
|
||||
write_dat_file_from_shards(&DatRebuild {
|
||||
dat_dir: out,
|
||||
collection: "",
|
||||
volume_id: VolumeId(1),
|
||||
dat_file_size: live_size,
|
||||
encoded_dat_file_size: encoded_size,
|
||||
data_shards,
|
||||
&shard_dirs,
|
||||
LARGE,
|
||||
SMALL,
|
||||
)
|
||||
shard_dirs: Some(&shard_dirs),
|
||||
large_block_size: LARGE,
|
||||
small_block_size: SMALL,
|
||||
})
|
||||
.unwrap();
|
||||
let path = format!("{}/1.dat", out);
|
||||
let decoded = std::fs::read(&path).unwrap();
|
||||
@@ -702,17 +736,58 @@ mod tests {
|
||||
assert_ne!(&original[..(large_row_size / 2) as usize], &control[..]);
|
||||
|
||||
// the live extent can never exceed the encode-time size
|
||||
assert!(write_dat_file(
|
||||
out,
|
||||
"",
|
||||
VolumeId(1),
|
||||
dat_size + 1,
|
||||
dat_size,
|
||||
data_shards,
|
||||
&shard_dirs,
|
||||
LARGE,
|
||||
SMALL,
|
||||
)
|
||||
.is_err());
|
||||
assert!(
|
||||
write_dat_file_from_shards(&DatRebuild {
|
||||
dat_dir: out,
|
||||
collection: "",
|
||||
volume_id: VolumeId(1),
|
||||
dat_file_size: dat_size + 1,
|
||||
encoded_dat_file_size: dat_size,
|
||||
data_shards,
|
||||
shard_dirs: Some(&shard_dirs),
|
||||
large_block_size: LARGE,
|
||||
small_block_size: SMALL,
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
/// A journal many chunks long that repeats a few ids reads back as those
|
||||
/// ids, from each dir once, whatever its length.
|
||||
#[test]
|
||||
fn test_read_ecj_deletions_collects_distinct_ids_across_dirs() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let data = tmp.path().join("data");
|
||||
let idx = tmp.path().join("idx");
|
||||
let missing = tmp.path().join("missing");
|
||||
std::fs::create_dir_all(&data).unwrap();
|
||||
std::fs::create_dir_all(&idx).unwrap();
|
||||
let (data, idx, missing) = (
|
||||
data.to_str().unwrap(),
|
||||
idx.to_str().unwrap(),
|
||||
missing.to_str().unwrap(),
|
||||
);
|
||||
|
||||
let entry = |id: u64| {
|
||||
let mut buf = [0u8; NEEDLE_ID_SIZE];
|
||||
NeedleId(id).to_bytes(&mut buf);
|
||||
buf
|
||||
};
|
||||
// Past two load chunks of three repeating ids, then an id only in the
|
||||
// last chunk and a torn trailing record.
|
||||
let mut ecj = Vec::new();
|
||||
while ecj.len() <= 2 * (1 << 20) {
|
||||
for id in [1, 2, 3] {
|
||||
ecj.extend_from_slice(&entry(id));
|
||||
}
|
||||
}
|
||||
ecj.extend_from_slice(&entry(7));
|
||||
ecj.extend_from_slice(&entry(8)[..3]);
|
||||
std::fs::write(format!("{idx}/1.ecj"), &ecj).unwrap();
|
||||
std::fs::write(format!("{data}/1.ecj"), entry(9)).unwrap();
|
||||
|
||||
let ids = read_ecj_deletions(&[data, idx, idx, missing], "", VolumeId(1)).unwrap();
|
||||
let expected: HashSet<NeedleId> = [1, 2, 3, 7, 9].into_iter().map(NeedleId).collect();
|
||||
assert_eq!(ids, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,12 @@
|
||||
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
#[cfg(not(unix))]
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
use reed_solomon_erasure::galois_8::ReedSolomon;
|
||||
|
||||
use crate::pb::volume_server_pb::{
|
||||
ChecksumAlgorithm, EcBitrotProtection, EcShardChecksums,
|
||||
};
|
||||
use crate::pb::volume_server_pb::{ChecksumAlgorithm, EcBitrotProtection, EcShardChecksums};
|
||||
use crate::storage::erasure_coding::ec_bitrot::{
|
||||
self, ShardChecksumBuilder, DEFAULT_BITROT_BLOCK_SIZE,
|
||||
self, DEFAULT_BITROT_BLOCK_SIZE, ShardChecksumBuilder,
|
||||
};
|
||||
use crate::storage::erasure_coding::ec_shard::*;
|
||||
use crate::storage::idx;
|
||||
@@ -50,7 +46,7 @@ pub fn write_ec_files(
|
||||
let dat_size = dat_file.metadata()?.len() as i64;
|
||||
|
||||
let rs = ReedSolomon::new(data_shards, parity_shards)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("reed-solomon init: {:?}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("reed-solomon init: {:?}", e)))?;
|
||||
|
||||
// Create shard files
|
||||
let total_shards = data_shards + parity_shards;
|
||||
@@ -77,11 +73,13 @@ pub fn write_ec_files(
|
||||
&rs,
|
||||
&mut shards,
|
||||
&mut builders,
|
||||
data_shards,
|
||||
parity_shards,
|
||||
ENCODE_BUFFER_SIZE,
|
||||
block_size as usize,
|
||||
block_size as usize,
|
||||
EcEncodeLayout {
|
||||
data_shards,
|
||||
parity_shards,
|
||||
buffer_size: ENCODE_BUFFER_SIZE,
|
||||
large_block_size: block_size as usize,
|
||||
small_block_size: block_size as usize,
|
||||
},
|
||||
)?;
|
||||
|
||||
// Close all shards
|
||||
@@ -162,7 +160,7 @@ pub fn rebuild_ec_files(
|
||||
}
|
||||
|
||||
let rs = ReedSolomon::new(data_shards, parity_shards)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("reed-solomon init: {:?}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("reed-solomon init: {:?}", e)))?;
|
||||
|
||||
let total_shards = data_shards + parity_shards;
|
||||
let mut shards: Vec<EcVolumeShard> = (0..total_shards as u8)
|
||||
@@ -175,7 +173,7 @@ pub fn rebuild_ec_files(
|
||||
let mut shard_size = 0;
|
||||
for (i, shard) in shards.iter_mut().enumerate() {
|
||||
if !missing_shard_ids.contains(&(i as u32)) {
|
||||
if let Ok(_) = shard.open() {
|
||||
if shard.open().is_ok() {
|
||||
let size = shard.file_size();
|
||||
if size > shard_size {
|
||||
shard_size = size;
|
||||
@@ -185,7 +183,7 @@ pub fn rebuild_ec_files(
|
||||
let mut found = false;
|
||||
for &other_dir in additional_dirs {
|
||||
let mut alt = EcVolumeShard::new(other_dir, collection, volume_id, i as u8);
|
||||
if let Ok(_) = alt.open() {
|
||||
if alt.open().is_ok() {
|
||||
let size = alt.file_size();
|
||||
if size > shard_size {
|
||||
shard_size = size;
|
||||
@@ -251,12 +249,8 @@ pub fn rebuild_ec_files(
|
||||
}
|
||||
|
||||
// Reconstruct missing shards
|
||||
rs.reconstruct(&mut buffers).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("reed-solomon reconstruct: {:?}", e),
|
||||
)
|
||||
})?;
|
||||
rs.reconstruct(&mut buffers)
|
||||
.map_err(|e| io::Error::other(format!("reed-solomon reconstruct: {:?}", e)))?;
|
||||
|
||||
// Write recovered data into the missing shards
|
||||
for i in missing_shard_ids {
|
||||
@@ -296,7 +290,7 @@ pub fn verify_ec_shards(
|
||||
parity_shards: usize,
|
||||
) -> io::Result<(Vec<u32>, Vec<String>)> {
|
||||
let rs = ReedSolomon::new(data_shards, parity_shards)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("reed-solomon init: {:?}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("reed-solomon init: {:?}", e)))?;
|
||||
|
||||
let total_shards = data_shards + parity_shards;
|
||||
let mut shards: Vec<Option<EcVolumeShard>> = (0..total_shards)
|
||||
@@ -378,27 +372,27 @@ pub fn verify_ec_shards(
|
||||
if !read_failed {
|
||||
// Need to convert Vec<Vec<u8>> to &[&[u8]] for rs.verify
|
||||
let slice_ptrs: Vec<&[u8]> = buffers.iter().map(|v| v.as_slice()).collect();
|
||||
if let Ok(is_valid) = rs.verify(&slice_ptrs) {
|
||||
if !is_valid {
|
||||
// Reed-Solomon verification failed. We cannot easily pinpoint which shard
|
||||
// is corrupted without recalculating parities or syndromes, so we just
|
||||
// log that this batch has corruption. Wait, we can test each parity shard!
|
||||
// Let's re-encode from the first `data_shards` and compare to the actual `parity_shards`.
|
||||
if let Ok(is_valid) = rs.verify(&slice_ptrs)
|
||||
&& !is_valid
|
||||
{
|
||||
// Reed-Solomon verification failed. We cannot easily pinpoint which shard
|
||||
// is corrupted without recalculating parities or syndromes, so we just
|
||||
// log that this batch has corruption. Wait, we can test each parity shard!
|
||||
// Let's re-encode from the first `data_shards` and compare to the actual `parity_shards`.
|
||||
|
||||
let mut verify_buffers = buffers.clone();
|
||||
// Clear the parity parts
|
||||
for i in data_shards..total_shards {
|
||||
verify_buffers[i].fill(0);
|
||||
}
|
||||
if rs.encode(&mut verify_buffers).is_ok() {
|
||||
for i in 0..total_shards {
|
||||
if buffers[i] != verify_buffers[i] {
|
||||
broken_shards.insert(i as u32);
|
||||
details.push(format!(
|
||||
"parity mismatch on shard {} at offset {}",
|
||||
i, offset
|
||||
));
|
||||
}
|
||||
let mut verify_buffers = buffers.clone();
|
||||
// Clear the parity parts
|
||||
for buf in &mut verify_buffers[data_shards..total_shards] {
|
||||
buf.fill(0);
|
||||
}
|
||||
if rs.encode(&mut verify_buffers).is_ok() {
|
||||
for i in 0..total_shards {
|
||||
if buffers[i] != verify_buffers[i] {
|
||||
broken_shards.insert(i as u32);
|
||||
details.push(format!(
|
||||
"parity mismatch on shard {} at offset {}",
|
||||
i, offset
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,22 +425,23 @@ pub(crate) fn write_sorted_ecx_from_idx(idx_path: &str, ecx_path: &str) -> io::R
|
||||
|
||||
// Read all idx entries
|
||||
let mut idx_file = File::open(idx_path)?;
|
||||
let mut entries: Vec<(NeedleId, Offset, Size)> = Vec::new();
|
||||
|
||||
let mut last: std::collections::HashMap<NeedleId, (Offset, Size)> =
|
||||
std::collections::HashMap::new();
|
||||
idx::walk_index_file(&mut idx_file, 0, |key, offset, size| {
|
||||
entries.push((key, offset, size));
|
||||
last.insert(key, (offset, size));
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Sort by NeedleId, then by actual offset so later entries come last
|
||||
entries.sort_by_key(|&(key, offset, _)| (key, offset.to_actual_offset()));
|
||||
|
||||
// Remove duplicates (keep last/latest entry for each key).
|
||||
// dedup_by_key keeps the first in each run, so we reverse first,
|
||||
// dedup, then reverse back.
|
||||
entries.reverse();
|
||||
entries.dedup_by_key(|entry| entry.0);
|
||||
entries.reverse();
|
||||
let mut entries: Vec<(NeedleId, Offset, Size)> = last
|
||||
.into_iter()
|
||||
.filter_map(|(key, (offset, size))| {
|
||||
if size.is_deleted() || offset.is_zero() {
|
||||
None
|
||||
} else {
|
||||
Some((key, offset, size))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
entries.sort_by_key(|&(key, _o, _s)| key);
|
||||
|
||||
// Write sorted entries to .ecx
|
||||
let mut ecx_file = File::create(ecx_path)?;
|
||||
@@ -490,7 +485,7 @@ pub fn rebuild_ecx_file(
|
||||
.collect();
|
||||
|
||||
for (i, shard) in shards.iter_mut().enumerate() {
|
||||
if let Err(_) = shard.open() {
|
||||
if shard.open().is_err() {
|
||||
let mut found = false;
|
||||
for &other_dir in additional_dirs {
|
||||
let mut alt = EcVolumeShard::new(other_dir, collection, volume_id, i as u8);
|
||||
@@ -507,7 +502,7 @@ pub fn rebuild_ecx_file(
|
||||
}
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("cannot open data shard for ecx rebuild"),
|
||||
"cannot open data shard for ecx rebuild".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -515,7 +510,7 @@ pub fn rebuild_ecx_file(
|
||||
|
||||
// Determine total logical data size from shard sizes
|
||||
let shard_size = shards.iter().map(|s| s.file_size()).max().unwrap_or(0);
|
||||
let total_data_size = shard_size as i64 * data_shards as i64;
|
||||
let total_data_size = shard_size * data_shards as i64;
|
||||
// The volume's shard block layout: the .vif-recorded uniform block size,
|
||||
// or the legacy two-tier sizes when 0. The row count comes from the shard
|
||||
// length; -1 disambiguates a legacy shard that is an exact large-block
|
||||
@@ -538,7 +533,7 @@ pub fn rebuild_ecx_file(
|
||||
let locate_shard_size = if dat_file_size > 0 {
|
||||
dat_file_size / data_shards as i64
|
||||
} else {
|
||||
(shard_size as i64 - 1).max(0)
|
||||
(shard_size - 1).max(0)
|
||||
};
|
||||
|
||||
// Read version from superblock (first byte of logical data)
|
||||
@@ -587,7 +582,8 @@ pub fn rebuild_ecx_file(
|
||||
}
|
||||
|
||||
let cookie = Cookie::from_bytes(&header_buf[..COOKIE_SIZE]);
|
||||
let needle_id = NeedleId::from_bytes(&header_buf[COOKIE_SIZE..COOKIE_SIZE + NEEDLE_ID_SIZE]);
|
||||
let needle_id =
|
||||
NeedleId::from_bytes(&header_buf[COOKIE_SIZE..COOKIE_SIZE + NEEDLE_ID_SIZE]);
|
||||
let size = Size::from_bytes(&header_buf[COOKIE_SIZE + NEEDLE_ID_SIZE..header_size]);
|
||||
|
||||
// Validate: stop if we hit zero cookie+id (end of data)
|
||||
@@ -640,7 +636,6 @@ pub fn rebuild_ecx_file(
|
||||
/// Read bytes from EC data shards at a logical offset in the .dat file,
|
||||
/// resolving the shard/offset through the volume's block layout via
|
||||
/// locate_data — the same mapping the read path uses.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn read_from_data_shards(
|
||||
shards: &[EcVolumeShard],
|
||||
buf: &mut [u8],
|
||||
@@ -707,30 +702,50 @@ fn read_from_data_shards(
|
||||
/// the uniform block is.
|
||||
const ENCODE_BUFFER_SIZE: usize = 256 * 1024;
|
||||
|
||||
/// Shape of one encode run: the Reed-Solomon split and the block sizes that
|
||||
/// fix where every byte of the .dat lands in the shards. Mirrors Go's
|
||||
/// `ECContext`. `buffer_size` must divide both block sizes.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct EcEncodeLayout {
|
||||
pub(crate) data_shards: usize,
|
||||
pub(crate) parity_shards: usize,
|
||||
/// Bytes of each shard's block handled per sub-batch; bounds memory at
|
||||
/// `total_shards * buffer_size` however large the blocks are.
|
||||
pub(crate) buffer_size: usize,
|
||||
pub(crate) large_block_size: usize,
|
||||
pub(crate) small_block_size: usize,
|
||||
}
|
||||
|
||||
/// Encode the .dat file data into shard files.
|
||||
///
|
||||
/// Uses a two-phase approach matching Go's ec_encoder.go:
|
||||
/// 1. Process as many large blocks as possible
|
||||
/// 2. Process remaining data with small blocks
|
||||
///
|
||||
/// `buffer_size` must divide both block sizes.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn encode_dat_file(
|
||||
dat_file: &File,
|
||||
dat_size: i64,
|
||||
rs: &ReedSolomon,
|
||||
shards: &mut [EcVolumeShard],
|
||||
builders: &mut [ShardChecksumBuilder],
|
||||
data_shards: usize,
|
||||
parity_shards: usize,
|
||||
buffer_size: usize,
|
||||
large_block_size: usize,
|
||||
small_block_size: usize,
|
||||
layout: EcEncodeLayout,
|
||||
) -> io::Result<()> {
|
||||
let EcEncodeLayout {
|
||||
data_shards,
|
||||
parity_shards,
|
||||
buffer_size,
|
||||
large_block_size,
|
||||
small_block_size,
|
||||
} = layout;
|
||||
let total_shards = data_shards + parity_shards;
|
||||
let mut buffers: Vec<Vec<u8>> = (0..total_shards)
|
||||
.map(|_| vec![0u8; buffer_size])
|
||||
.collect();
|
||||
let mut buffers: Vec<Vec<u8>> = (0..total_shards).map(|_| vec![0u8; buffer_size]).collect();
|
||||
let mut run = EncodeRun {
|
||||
dat_file,
|
||||
rs,
|
||||
buffers: &mut buffers,
|
||||
shards,
|
||||
builders,
|
||||
data_shards,
|
||||
};
|
||||
|
||||
let mut remaining = dat_size;
|
||||
let mut offset: u64 = 0;
|
||||
@@ -739,16 +754,7 @@ pub(crate) fn encode_dat_file(
|
||||
let large_row_size = large_block_size * data_shards;
|
||||
|
||||
while remaining >= large_row_size as i64 {
|
||||
encode_data(
|
||||
dat_file,
|
||||
offset,
|
||||
large_block_size,
|
||||
rs,
|
||||
&mut buffers,
|
||||
shards,
|
||||
builders,
|
||||
data_shards,
|
||||
)?;
|
||||
run.encode_row(offset, large_block_size)?;
|
||||
offset += large_row_size as u64;
|
||||
remaining -= large_row_size as i64;
|
||||
}
|
||||
@@ -758,16 +764,7 @@ pub(crate) fn encode_dat_file(
|
||||
|
||||
while remaining > 0 {
|
||||
let to_process = remaining.min(small_row_size as i64);
|
||||
encode_data(
|
||||
dat_file,
|
||||
offset,
|
||||
small_block_size,
|
||||
rs,
|
||||
&mut buffers,
|
||||
shards,
|
||||
builders,
|
||||
data_shards,
|
||||
)?;
|
||||
run.encode_row(offset, small_block_size)?;
|
||||
offset += to_process as u64;
|
||||
remaining -= to_process;
|
||||
}
|
||||
@@ -775,108 +772,66 @@ pub(crate) fn encode_dat_file(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encode one row of blocks, streaming it in ENCODE_BUFFER_SIZE sub-batches so
|
||||
/// arbitrarily large blocks never require block-sized allocations. Mirrors
|
||||
/// Go's encodeData.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn encode_data(
|
||||
dat_file: &File,
|
||||
row_offset: u64,
|
||||
block_size: usize,
|
||||
rs: &ReedSolomon,
|
||||
buffers: &mut [Vec<u8>],
|
||||
shards: &mut [EcVolumeShard],
|
||||
builders: &mut [ShardChecksumBuilder],
|
||||
/// Everything one encode run streams through: the source .dat, the codec, a
|
||||
/// buffer per shard, and the per-shard file and checksum sinks.
|
||||
struct EncodeRun<'a> {
|
||||
dat_file: &'a File,
|
||||
rs: &'a ReedSolomon,
|
||||
buffers: &'a mut [Vec<u8>],
|
||||
shards: &'a mut [EcVolumeShard],
|
||||
builders: &'a mut [ShardChecksumBuilder],
|
||||
data_shards: usize,
|
||||
) -> io::Result<()> {
|
||||
let buffer_size = buffers[0].len();
|
||||
if block_size % buffer_size != 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"unexpected block size {} buffer size {}",
|
||||
block_size, buffer_size
|
||||
),
|
||||
));
|
||||
}
|
||||
let batch_count = block_size / buffer_size;
|
||||
for b in 0..batch_count {
|
||||
encode_one_batch(
|
||||
dat_file,
|
||||
row_offset + (b * buffer_size) as u64,
|
||||
block_size,
|
||||
rs,
|
||||
buffers,
|
||||
shards,
|
||||
builders,
|
||||
data_shards,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encode one sub-batch: the same buffer-sized slice of every shard's block in
|
||||
/// this row. Mirrors Go's encodeDataOneBatch.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn encode_one_batch(
|
||||
dat_file: &File,
|
||||
offset: u64,
|
||||
block_size: usize,
|
||||
rs: &ReedSolomon,
|
||||
buffers: &mut [Vec<u8>],
|
||||
shards: &mut [EcVolumeShard],
|
||||
builders: &mut [ShardChecksumBuilder],
|
||||
data_shards: usize,
|
||||
) -> io::Result<()> {
|
||||
// Read data shards from the .dat file, zero-filling past EOF — the buffers
|
||||
// are reused across batches, so the tail must be cleared explicitly.
|
||||
for i in 0..data_shards {
|
||||
let read_offset = offset + (i * block_size) as u64;
|
||||
let n = read_at_most(dat_file, &mut buffers[i], read_offset)?;
|
||||
for b in buffers[i][n..].iter_mut() {
|
||||
*b = 0;
|
||||
impl EncodeRun<'_> {
|
||||
/// Encode one row of blocks, streaming it in ENCODE_BUFFER_SIZE sub-batches
|
||||
/// so arbitrarily large blocks never require block-sized allocations.
|
||||
/// Mirrors Go's encodeData.
|
||||
fn encode_row(&mut self, row_offset: u64, block_size: usize) -> io::Result<()> {
|
||||
let buffer_size = self.buffers[0].len();
|
||||
if !block_size.is_multiple_of(buffer_size) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"unexpected block size {} buffer size {}",
|
||||
block_size, buffer_size
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Encode parity shards
|
||||
rs.encode(&mut *buffers).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("reed-solomon encode: {:?}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Write all shard buffers to files and feed the same bytes to each
|
||||
// shard's bitrot checksum builder, keeping covered_size == on-disk length.
|
||||
for (i, buf) in buffers.iter().enumerate() {
|
||||
shards[i].write_all(buf)?;
|
||||
builders[i].write(buf);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read into `buf` at `offset` until it is full or EOF; returns bytes read.
|
||||
fn read_at_most(dat_file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
|
||||
let mut n = 0;
|
||||
while n < buf.len() {
|
||||
#[cfg(unix)]
|
||||
let r = {
|
||||
use std::os::unix::fs::FileExt;
|
||||
dat_file.read_at(&mut buf[n..], offset + n as u64)?
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
let r = {
|
||||
let mut f = dat_file.try_clone()?;
|
||||
f.seek(SeekFrom::Start(offset + n as u64))?;
|
||||
f.read(&mut buf[n..])?
|
||||
};
|
||||
if r == 0 {
|
||||
break;
|
||||
let batch_count = block_size / buffer_size;
|
||||
for b in 0..batch_count {
|
||||
self.encode_one_batch(row_offset + (b * buffer_size) as u64, block_size)?;
|
||||
}
|
||||
n += r;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encode one sub-batch: the same buffer-sized slice of every shard's block
|
||||
/// in this row. Mirrors Go's encodeDataOneBatch.
|
||||
fn encode_one_batch(&mut self, offset: u64, block_size: usize) -> io::Result<()> {
|
||||
// Read data shards from the .dat file, zero-filling past EOF — the
|
||||
// buffers are reused across batches, so the tail must be cleared
|
||||
// explicitly.
|
||||
for (i, buf) in self.buffers[..self.data_shards].iter_mut().enumerate() {
|
||||
let read_offset = offset + (i * block_size) as u64;
|
||||
let n = crate::storage::io::read_full_at(self.dat_file, buf, read_offset)?;
|
||||
buf[n..].fill(0);
|
||||
}
|
||||
|
||||
// Encode parity shards
|
||||
self.rs
|
||||
.encode(&mut *self.buffers)
|
||||
.map_err(|e| io::Error::other(format!("reed-solomon encode: {:?}", e)))?;
|
||||
|
||||
// Write all shard buffers to files and feed the same bytes to each
|
||||
// shard's bitrot checksum builder, keeping covered_size == on-disk
|
||||
// length.
|
||||
for (i, buf) in self.buffers.iter().enumerate() {
|
||||
self.shards[i].write_all(buf)?;
|
||||
self.builders[i].write(buf);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -884,7 +839,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::storage::needle::needle::Needle;
|
||||
use crate::storage::needle_map::NeedleMapKind;
|
||||
use crate::storage::volume::Volume;
|
||||
use crate::storage::volume::{Volume, VolumeSpec};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
@@ -896,13 +851,9 @@ mod tests {
|
||||
let mut v = Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -947,13 +898,9 @@ mod tests {
|
||||
let mut v = Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
for i in 1..=n {
|
||||
@@ -1026,13 +973,9 @@ mod tests {
|
||||
let mut v = Volume::new(
|
||||
&dir,
|
||||
&dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
for i in 1..=20 {
|
||||
@@ -1064,7 +1007,10 @@ mod tests {
|
||||
let victim = format!("{}/1.ec03", dir);
|
||||
let full = std::fs::metadata(&victim).unwrap().len();
|
||||
assert!(full > 0, "encoded shard should be non-empty");
|
||||
let f = std::fs::OpenOptions::new().write(true).open(&victim).unwrap();
|
||||
let f = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&victim)
|
||||
.unwrap();
|
||||
f.set_len(full / 2).unwrap();
|
||||
drop(f);
|
||||
|
||||
@@ -1218,19 +1164,15 @@ mod tests {
|
||||
#[test]
|
||||
fn test_rebuild_ecx_file_uniform_layout() {
|
||||
use crate::storage::needle_map::NeedleMapKind;
|
||||
use crate::storage::volume::Volume;
|
||||
use crate::storage::volume::{Volume, VolumeSpec};
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let dir = tmp.path().to_str().unwrap().to_string();
|
||||
let mut v = Volume::new(
|
||||
&dir,
|
||||
&dir,
|
||||
"",
|
||||
VolumeId(2),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
for i in 1u64..=12 {
|
||||
@@ -1260,7 +1202,10 @@ mod tests {
|
||||
|
||||
rebuild_ecx_file(&dir, "", VolumeId(2), 10, block_size, 0, &[]).unwrap();
|
||||
let rebuilt = std::fs::read(&ecx_path).unwrap();
|
||||
assert_eq!(canonical, rebuilt, "rebuilt .ecx must match the encode-time .ecx");
|
||||
assert_eq!(
|
||||
canonical, rebuilt,
|
||||
"rebuilt .ecx must match the encode-time .ecx"
|
||||
);
|
||||
}
|
||||
|
||||
// A truncated data shard must FAIL the .ecx rebuild, not publish the
|
||||
@@ -1268,19 +1213,15 @@ mod tests {
|
||||
#[test]
|
||||
fn test_rebuild_ecx_file_fails_on_truncated_shard() {
|
||||
use crate::storage::needle_map::NeedleMapKind;
|
||||
use crate::storage::volume::Volume;
|
||||
use crate::storage::volume::{Volume, VolumeSpec};
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let dir = tmp.path().to_str().unwrap().to_string();
|
||||
let mut v = Volume::new(
|
||||
&dir,
|
||||
&dir,
|
||||
"",
|
||||
VolumeId(3),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
for i in 1u64..=12 {
|
||||
@@ -1378,13 +1319,9 @@ mod tests {
|
||||
let mut v = Volume::new(
|
||||
dat_dir,
|
||||
idx_dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -1462,13 +1399,9 @@ mod tests {
|
||||
let mut v = Volume::new(
|
||||
dat_dir,
|
||||
idx_dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -1500,13 +1433,9 @@ mod tests {
|
||||
let mut v = Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
"",
|
||||
vid,
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
for i in 1..=8 {
|
||||
@@ -1597,4 +1526,86 @@ mod tests {
|
||||
details
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_drops_tombstone_last_wins() {
|
||||
use crate::storage::idx;
|
||||
use crate::storage::types::{NeedleId, Offset, Size, TOMBSTONE_FILE_SIZE};
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let dir = tmp.path().to_str().unwrap();
|
||||
let idx_path = format!("{}/t.idx", dir);
|
||||
let ecx_path = format!("{}/t.ecx", dir);
|
||||
let key = NeedleId(12345);
|
||||
{
|
||||
let mut f = std::fs::File::create(&idx_path).unwrap();
|
||||
idx::write_index_entry(&mut f, key, Offset::from_actual_offset(1024), Size(100))
|
||||
.unwrap();
|
||||
idx::write_index_entry(&mut f, key, Offset::default(), TOMBSTONE_FILE_SIZE).unwrap();
|
||||
}
|
||||
super::write_sorted_ecx_from_idx(&idx_path, &ecx_path).unwrap();
|
||||
let mut found = false;
|
||||
{
|
||||
let mut f = std::fs::File::open(&ecx_path).unwrap();
|
||||
idx::walk_index_file(&mut f, 0, |k, _o, _s| {
|
||||
if k == key {
|
||||
found = true;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
assert!(!found, "tombstoned key must not appear in .ecx");
|
||||
let idx2 = format!("{}/t2.idx", dir);
|
||||
let ecx2 = format!("{}/t2.ecx", dir);
|
||||
{
|
||||
let mut f = std::fs::File::create(&idx2).unwrap();
|
||||
idx::write_index_entry(&mut f, key, Offset::default(), TOMBSTONE_FILE_SIZE).unwrap();
|
||||
idx::write_index_entry(&mut f, key, Offset::from_actual_offset(2048), Size(200))
|
||||
.unwrap();
|
||||
}
|
||||
super::write_sorted_ecx_from_idx(&idx2, &ecx2).unwrap();
|
||||
let mut found2 = false;
|
||||
{
|
||||
let mut f = std::fs::File::open(&ecx2).unwrap();
|
||||
idx::walk_index_file(&mut f, 0, |k, o, s| {
|
||||
if k == key {
|
||||
found2 = true;
|
||||
assert_eq!(o.to_actual_offset(), 2048);
|
||||
assert_eq!(s, Size(200));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
assert!(found2, "re-created key must appear live");
|
||||
// Zero offset with non-negative size is also a deletion: Go
|
||||
// readNeedleMap (`if !offset.IsZero() && !size.IsDeleted() { Set }
|
||||
// else { Delete }`) and CompactNeedleMap::load_from_idx both treat
|
||||
// it as deleted. Encode must drop it too, or the .ecx live-map
|
||||
// mismatches replay.
|
||||
let idx3 = format!("{}/t3.idx", dir);
|
||||
let ecx3 = format!("{}/t3.ecx", dir);
|
||||
{
|
||||
let mut f = std::fs::File::create(&idx3).unwrap();
|
||||
idx::write_index_entry(&mut f, key, Offset::from_actual_offset(1024), Size(100))
|
||||
.unwrap();
|
||||
idx::write_index_entry(&mut f, key, Offset::default(), Size(0)).unwrap();
|
||||
}
|
||||
super::write_sorted_ecx_from_idx(&idx3, &ecx3).unwrap();
|
||||
let mut found3 = false;
|
||||
{
|
||||
let mut f = std::fs::File::open(&ecx3).unwrap();
|
||||
idx::walk_index_file(&mut f, 0, |k, _o, _s| {
|
||||
if k == key {
|
||||
found3 = true;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
assert!(
|
||||
!found3,
|
||||
"zero-offset row must not appear in .ecx even with non-negative size"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,20 @@ pub const ERASURE_CODING_SMALL_BLOCK_SIZE: usize = 1024 * 1024; // 1MB
|
||||
|
||||
pub type ShardId = u8;
|
||||
|
||||
/// Validate a wire shard id. `ShardId` is `u8` but only 0..MAX_SHARD_COUNT are valid.
|
||||
/// Rejects 256 (would truncate to 0 and delete .ec00) and 270 (would alias 14).
|
||||
pub fn shard_id_try_from(v: u32) -> Result<ShardId, String> {
|
||||
if v < MAX_SHARD_COUNT as u32 {
|
||||
Ok(v as ShardId)
|
||||
} else {
|
||||
Err(format!(
|
||||
"invalid shard id {} (max {})",
|
||||
v,
|
||||
MAX_SHARD_COUNT - 1
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// A single erasure-coded shard file.
|
||||
pub struct EcVolumeShard {
|
||||
pub volume_id: VolumeId,
|
||||
@@ -73,28 +87,14 @@ impl EcVolumeShard {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read data at a specific offset.
|
||||
/// Read data at a specific offset, filling `buf` unless the shard ends first.
|
||||
pub fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
|
||||
let file = self
|
||||
.ecd_file
|
||||
.as_ref()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "shard file not open"))?;
|
||||
.ok_or_else(|| io::Error::other("shard file not open"))?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::FileExt;
|
||||
file.read_at(buf, offset)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
// File::read_at is unix-only; fall back to seek + read.
|
||||
// We need a mutable reference for seek/read, so clone the handle.
|
||||
let mut f = file.try_clone()?;
|
||||
f.seek(SeekFrom::Start(offset))?;
|
||||
f.read(buf)
|
||||
}
|
||||
crate::storage::io::read_full_at(file, buf, offset)
|
||||
}
|
||||
|
||||
/// Write data to the shard file (appends).
|
||||
@@ -102,7 +102,7 @@ impl EcVolumeShard {
|
||||
let file = self
|
||||
.ecd_file
|
||||
.as_mut()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "shard file not open"))?;
|
||||
.ok_or_else(|| io::Error::other("shard file not open"))?;
|
||||
file.write_all(data)?;
|
||||
self.ecd_file_size += data.len() as i64;
|
||||
Ok(())
|
||||
@@ -123,7 +123,7 @@ impl EcVolumeShard {
|
||||
pub fn try_clone_file(&self) -> io::Result<File> {
|
||||
self.ecd_file
|
||||
.as_ref()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "shard file not open"))?
|
||||
.ok_or_else(|| io::Error::other("shard file not open"))?
|
||||
.try_clone()
|
||||
}
|
||||
|
||||
@@ -195,6 +195,104 @@ impl ShardBits {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the generation of a 2PC-staged `<base>.v<N>` file: `None` means the
|
||||
/// name is not a generation file of `base`.
|
||||
pub fn ec_file_generation(name: &str, base: &str) -> Option<u32> {
|
||||
let suffix = name.strip_prefix(&format!("{}.v", base))?;
|
||||
match suffix.parse::<u32>() {
|
||||
Ok(g) if g > 0 => Some(g),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes 2PC generation files staged under `base`:
|
||||
/// `<base>.ecNN.v<N>`, `<base>.ecx.v<N>`, `<base>.ecj.v<N>`, `<base>.ecsum.v<N>`
|
||||
/// and `<base>.vif.v<N>`. `generations_older_than == 0` removes every
|
||||
/// generation; otherwise only generations strictly below it. Returns the
|
||||
/// first real removal failure. Mirrors Go's `RemoveEcGenerationFiles`.
|
||||
pub fn remove_ec_generation_files(base: &str, generations_older_than: u32) -> io::Result<()> {
|
||||
let path = std::path::Path::new(base);
|
||||
let (Some(parent), Some(fname)) = (path.parent(), path.file_name()) else {
|
||||
return Ok(());
|
||||
};
|
||||
let ec_prefix = format!("{}.ec", fname.to_string_lossy());
|
||||
let vif_name = format!("{}.vif", fname.to_string_lossy());
|
||||
let mut first_err: Option<io::Error> = None;
|
||||
let mut record = |res: io::Result<()>| {
|
||||
if let Err(e) = res
|
||||
&& first_err.is_none()
|
||||
{
|
||||
first_err = Some(e);
|
||||
}
|
||||
};
|
||||
match fs::read_dir(parent) {
|
||||
Ok(entries) => {
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(e) => {
|
||||
// A skipped entry means an incomplete sweep; report it
|
||||
// instead of pretending the cleanup finished.
|
||||
record(Err(e));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
let Some((artifact, _)) = name.rsplit_once(".v") else {
|
||||
continue;
|
||||
};
|
||||
if artifact != vif_name && !artifact.starts_with(&ec_prefix) {
|
||||
continue;
|
||||
}
|
||||
let Some(generation) = ec_file_generation(&name, artifact) else {
|
||||
continue;
|
||||
};
|
||||
if generations_older_than > 0 && generation >= generations_older_than {
|
||||
continue;
|
||||
}
|
||||
record(match fs::remove_file(entry.path()) {
|
||||
Err(e) if e.kind() != io::ErrorKind::NotFound => Err(e),
|
||||
_ => Ok(()),
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) if e.kind() != io::ErrorKind::NotFound => record(Err(e)),
|
||||
Err(_) => {}
|
||||
}
|
||||
match first_err {
|
||||
Some(e) => Err(e),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes every staged generation `<shard_file>.v<N>` of one shard file.
|
||||
/// Returns true when at least one generation file was removed.
|
||||
pub fn remove_ec_shard_generations(shard_file: &str) -> io::Result<bool> {
|
||||
let path = std::path::Path::new(shard_file);
|
||||
let (Some(parent), Some(fname)) = (path.parent(), path.file_name()) else {
|
||||
return Ok(false);
|
||||
};
|
||||
let fname = fname.to_string_lossy().into_owned();
|
||||
let mut removed = false;
|
||||
match fs::read_dir(parent) {
|
||||
Ok(entries) => {
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if ec_file_generation(&name, &fname).is_some() {
|
||||
match fs::remove_file(entry.path()) {
|
||||
Err(e) if e.kind() != io::ErrorKind::NotFound => return Err(e),
|
||||
_ => removed = true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) if e.kind() != io::ErrorKind::NotFound => return Err(e),
|
||||
Err(_) => {}
|
||||
}
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -251,4 +349,42 @@ mod tests {
|
||||
let shard = EcVolumeShard::new("/data", "", VolumeId(7), 13);
|
||||
assert_eq!(shard.file_name(), "/data/7.ec13");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shard_id_try_from_u32_rejects_overflow() {
|
||||
use super::{MAX_SHARD_COUNT, shard_id_try_from};
|
||||
assert_eq!(shard_id_try_from(0).unwrap(), 0u8);
|
||||
assert_eq!(shard_id_try_from(14).unwrap(), 14u8);
|
||||
assert_eq!(shard_id_try_from(31).unwrap(), 31u8);
|
||||
assert!(shard_id_try_from(32).is_err());
|
||||
assert!(shard_id_try_from(256).is_err());
|
||||
assert!(shard_id_try_from(270).is_err());
|
||||
assert!(shard_id_try_from(u32::MAX).is_err());
|
||||
assert_eq!(MAX_SHARD_COUNT, 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shard_batch_validation_is_atomic_rejects_without_partial_prefix() {
|
||||
use super::shard_id_try_from;
|
||||
// The mount/unmount handlers pre-validate the ENTIRE req.shard_ids into
|
||||
// a Vec<ShardId> BEFORE acquiring the write lock or mutating any EC
|
||||
// state. This test pins the validation half of that contract at the
|
||||
// unit level: a batch like [0, 32] must fail as a whole, so by
|
||||
// construction no validated prefix (e.g. shard 0) is ever applied.
|
||||
// The handler-level tests below assert the no-state-change half.
|
||||
let batch = vec![0u32, 32u32];
|
||||
let validated: Result<Vec<_>, _> =
|
||||
batch.iter().map(|&sid| shard_id_try_from(sid)).collect();
|
||||
assert!(
|
||||
validated.is_err(),
|
||||
"batch {:?} must be rejected as a whole",
|
||||
batch
|
||||
);
|
||||
// A fully-valid batch still validates cleanly.
|
||||
let ok: Result<Vec<_>, _> = [0u32, 1u32, 13u32]
|
||||
.iter()
|
||||
.map(|&sid| shard_id_try_from(sid))
|
||||
.collect();
|
||||
assert_eq!(ok.unwrap(), vec![0u8, 1u8, 13u8]);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ pub mod ec_shard;
|
||||
pub mod ec_volume;
|
||||
|
||||
pub use ec_shard::{
|
||||
EcVolumeShard, ShardId, DATA_SHARDS_COUNT, MAX_SHARD_COUNT, MIN_TOTAL_DISKS,
|
||||
PARITY_SHARDS_COUNT, TOTAL_SHARDS_COUNT,
|
||||
DATA_SHARDS_COUNT, EcVolumeShard, MAX_SHARD_COUNT, MIN_TOTAL_DISKS, PARITY_SHARDS_COUNT,
|
||||
ShardId, TOTAL_SHARDS_COUNT,
|
||||
};
|
||||
pub use ec_volume::EcVolume;
|
||||
|
||||
@@ -21,12 +21,26 @@ where
|
||||
let mut buf = vec![0u8; NEEDLE_MAP_ENTRY_SIZE * ROWS_TO_READ];
|
||||
|
||||
loop {
|
||||
let count = match reader.read(&mut buf) {
|
||||
Ok(0) => return Ok(()),
|
||||
Ok(n) => n,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(()),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
// Fill the batch before decoding: `read` may return a count that is
|
||||
// not a multiple of the entry size, and a split entry would misalign
|
||||
// every later row. Go is immune: `ReadAt` fills or errors.
|
||||
let mut count = 0;
|
||||
let mut eof = false;
|
||||
while count < buf.len() {
|
||||
match reader.read(&mut buf[count..]) {
|
||||
Ok(0) => {
|
||||
eof = true;
|
||||
break;
|
||||
}
|
||||
Ok(n) => count += n,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => {
|
||||
eof = true;
|
||||
break;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
let mut i = 0;
|
||||
while i + NEEDLE_MAP_ENTRY_SIZE <= count {
|
||||
@@ -34,6 +48,11 @@ where
|
||||
f(key, offset, size)?;
|
||||
i += NEEDLE_MAP_ENTRY_SIZE;
|
||||
}
|
||||
|
||||
// A trailing partial entry at EOF is ignored, as Go does on `io.EOF`.
|
||||
if eof {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +76,7 @@ pub fn check_index_file<R: Read + Seek>(
|
||||
errs.push(format!("walk index file: {}", e));
|
||||
}
|
||||
|
||||
entries.sort_by(|a, b| a.2.cmp(&b.2).then(a.3 .0.cmp(&b.3 .0)));
|
||||
entries.sort_by(|a, b| a.2.cmp(&b.2).then(a.3.0.cmp(&b.3.0)));
|
||||
|
||||
// Offset-0 logical tombstones (remote-tier deletes) occupy no physical extent,
|
||||
// so they cannot overlap anything — exclude them from the overlap check. They
|
||||
@@ -177,6 +196,111 @@ mod tests {
|
||||
data
|
||||
}
|
||||
|
||||
/// Reader that hands back at most `chunk` bytes per `read`. 7 is coprime
|
||||
/// with the 17-byte entry size, so nearly every read ends mid-entry. With
|
||||
/// `interrupts`, every other call fails with `ErrorKind::Interrupted`.
|
||||
struct ShortReader {
|
||||
inner: Cursor<Vec<u8>>,
|
||||
chunk: usize,
|
||||
interrupts: bool,
|
||||
interrupt_next: bool,
|
||||
}
|
||||
|
||||
impl ShortReader {
|
||||
fn new(data: Vec<u8>, interrupts: bool) -> Self {
|
||||
ShortReader {
|
||||
inner: Cursor::new(data),
|
||||
chunk: 7,
|
||||
interrupts,
|
||||
interrupt_next: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for ShortReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if self.interrupt_next {
|
||||
self.interrupt_next = false;
|
||||
return Err(io::Error::from(io::ErrorKind::Interrupted));
|
||||
}
|
||||
self.interrupt_next = self.interrupts;
|
||||
let n = buf.len().min(self.chunk);
|
||||
self.inner.read(&mut buf[..n])
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for ShortReader {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.inner.seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_all<R: Read + Seek>(reader: &mut R, start_from: u64) -> Vec<(NeedleId, i64, Size)> {
|
||||
let mut collected = Vec::new();
|
||||
walk_index_file(reader, start_from, |key, offset, size| {
|
||||
collected.push((key, offset.to_actual_offset(), size));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
collected
|
||||
}
|
||||
|
||||
/// More than one ROWS_TO_READ batch, so the walk crosses a buffer refill.
|
||||
fn many_entries() -> Vec<(NeedleId, Offset, Size)> {
|
||||
(0..(ROWS_TO_READ as u64 * 2 + 37))
|
||||
.map(|i| {
|
||||
(
|
||||
NeedleId(i * 7 + 1),
|
||||
Offset::from_actual_offset(i as i64 * 128),
|
||||
Size(i as i32 + 1),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_walk_index_file_short_reads_keep_alignment() {
|
||||
let data = idx_bytes(&many_entries());
|
||||
let expected = walk_all(&mut Cursor::new(data.clone()), 0);
|
||||
assert_eq!(expected.len(), ROWS_TO_READ * 2 + 37);
|
||||
|
||||
let mut short = ShortReader::new(data, false);
|
||||
assert_eq!(walk_all(&mut short, 0), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_walk_index_file_retries_interrupted_reads() {
|
||||
let data = idx_bytes(&many_entries());
|
||||
let expected = walk_all(&mut Cursor::new(data.clone()), 0);
|
||||
|
||||
let mut short = ShortReader::new(data, true);
|
||||
assert_eq!(walk_all(&mut short, 0), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_walk_index_file_short_reads_start_from() {
|
||||
let data = idx_bytes(&many_entries());
|
||||
let expected = walk_all(&mut Cursor::new(data.clone()), 0);
|
||||
|
||||
let start = ROWS_TO_READ as u64 + 5;
|
||||
let mut short = ShortReader::new(data, false);
|
||||
assert_eq!(walk_all(&mut short, start), expected[start as usize..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_walk_index_file_ignores_trailing_partial_entry() {
|
||||
// A torn final entry is dropped without an error, as Go does on io.EOF.
|
||||
let entries = many_entries();
|
||||
let mut data = idx_bytes(&entries);
|
||||
data.extend_from_slice(&[0xAB; NEEDLE_MAP_ENTRY_SIZE - 1]);
|
||||
|
||||
let expected = walk_all(&mut Cursor::new(idx_bytes(&entries)), 0);
|
||||
assert_eq!(walk_all(&mut Cursor::new(data.clone()), 0), expected);
|
||||
|
||||
let mut short = ShortReader::new(data, false);
|
||||
assert_eq!(walk_all(&mut short, 0), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_index_file_clean() {
|
||||
let data = idx_bytes(&[
|
||||
@@ -213,7 +337,11 @@ mod tests {
|
||||
let size = data.len() as i64;
|
||||
let (count, errs) = check_index_file(&mut Cursor::new(data), size, Version(3));
|
||||
assert_eq!(count, 2, "tombstone row is still counted: {:?}", errs);
|
||||
assert!(errs.is_empty(), "offset-0 tombstone must not overlap: {:?}", errs);
|
||||
assert!(
|
||||
errs.is_empty(),
|
||||
"offset-0 tombstone must not overlap: {:?}",
|
||||
errs
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
//! Positional file reads.
|
||||
//!
|
||||
//! Every read here is "these bytes at this offset", never "the next bytes".
|
||||
//! The handles are shared — `.dat` and `.idx` descriptors are borrowed from
|
||||
//! [`file_pool`](super::needle_map::file_pool), a mounted EC shard's handle is
|
||||
//! duplicated into a scrub plan — so no caller may rely on a file position.
|
||||
//!
|
||||
//! On unix that is `pread(2)` through `std::os::unix::fs::FileExt`. On Windows
|
||||
//! it is `seek_read`, which passes the offset through `OVERLAPPED`, so the read
|
||||
//! itself is independent of the current cursor.
|
||||
//!
|
||||
//! What these helpers replace is `try_clone()` + `seek()` + `read()`. A
|
||||
//! duplicated handle shares one kernel file offset with the original, so that
|
||||
//! sequence is two syscalls against state another thread can move in between:
|
||||
//! the seek positions the offset, a concurrent reader or an append moves it,
|
||||
//! and the read returns bytes from somewhere else entirely. `seek_read` carries
|
||||
//! its own offset in a single call, so there is no window.
|
||||
//!
|
||||
//! `seek_read` does still advance the cursor as a side effect — Windows updates
|
||||
//! the file pointer even for an `OVERLAPPED` read — which nothing here relies
|
||||
//! on. A caller that genuinely needs a private position must open the file
|
||||
//! again rather than duplicate a handle; see `Volume::dat_scan_plan` in
|
||||
//! [`storage::volume`](super::volume).
|
||||
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
|
||||
/// Reads exactly `buf.len()` bytes from `file` starting at `offset`.
|
||||
///
|
||||
/// Fails with [`io::ErrorKind::UnexpectedEof`] if the file ends first.
|
||||
pub(crate) fn read_exact_at(file: &File, buf: &mut [u8], offset: u64) -> io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::FileExt;
|
||||
file.read_exact_at(buf, offset)?;
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if read_full_at(file, buf, offset)? < buf.len() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"unexpected EOF in seek_read",
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
{
|
||||
compile_error!("Platform not supported: only unix and windows are supported");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads up to `buf.len()` bytes from `file` starting at `offset`, returning
|
||||
/// how many were read.
|
||||
///
|
||||
/// A short read — including `0` at or past end of file — is not an error; use
|
||||
/// [`read_exact_at`] when the whole buffer must be filled.
|
||||
pub(crate) fn read_at(file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::FileExt;
|
||||
file.read_at(buf, offset)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::fs::FileExt;
|
||||
file.seek_read(buf, offset)
|
||||
}
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
{
|
||||
compile_error!("Platform not supported: only unix and windows are supported");
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads into `buf` at `offset` until it is full or the file ends, retrying
|
||||
/// interrupted reads; returns how many bytes were read.
|
||||
///
|
||||
/// Unlike [`read_at`], a count below `buf.len()` always means end of file.
|
||||
pub(crate) fn read_full_at(file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
|
||||
fill_at(|b, at| read_at(file, b, at), buf, offset)
|
||||
}
|
||||
|
||||
fn fill_at(
|
||||
mut read: impl FnMut(&mut [u8], u64) -> io::Result<usize>,
|
||||
buf: &mut [u8],
|
||||
offset: u64,
|
||||
) -> io::Result<usize> {
|
||||
let mut filled = 0;
|
||||
while filled < buf.len() {
|
||||
match read(&mut buf[filled..], offset + filled as u64) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => filled += n,
|
||||
Err(err) if err.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Ok(filled)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{fill_at, read_at, read_exact_at, read_full_at};
|
||||
use std::io::{ErrorKind, Write};
|
||||
|
||||
fn temp_file(bytes: &[u8]) -> tempfile::NamedTempFile {
|
||||
let mut f = tempfile::NamedTempFile::new().expect("temp file");
|
||||
f.write_all(bytes).expect("write");
|
||||
f.flush().expect("flush");
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_exact_at_fills_the_whole_buffer() {
|
||||
let f = temp_file(b"0123456789");
|
||||
let mut buf = [0u8; 10];
|
||||
read_exact_at(f.as_file(), &mut buf, 0).expect("read");
|
||||
assert_eq!(&buf, b"0123456789");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_exact_at_reads_from_the_offset() {
|
||||
let f = temp_file(b"0123456789");
|
||||
let mut buf = [0u8; 4];
|
||||
read_exact_at(f.as_file(), &mut buf, 3).expect("read");
|
||||
assert_eq!(&buf, b"3456");
|
||||
|
||||
// The helper is positional: a second read at a lower offset sees the
|
||||
// bytes at that offset, not wherever the first read left a cursor.
|
||||
let mut again = [0u8; 4];
|
||||
read_exact_at(f.as_file(), &mut again, 1).expect("read");
|
||||
assert_eq!(&again, b"1234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_exact_at_short_file_is_unexpected_eof() {
|
||||
let f = temp_file(b"0123");
|
||||
let mut buf = [0u8; 8];
|
||||
let err = read_exact_at(f.as_file(), &mut buf, 0).expect_err("short file");
|
||||
assert_eq!(err.kind(), ErrorKind::UnexpectedEof);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_at_allows_a_short_read_at_eof() {
|
||||
let f = temp_file(b"0123456789");
|
||||
let mut buf = [0u8; 8];
|
||||
|
||||
let n = read_at(f.as_file(), &mut buf, 6).expect("read");
|
||||
assert_eq!(n, 4);
|
||||
assert_eq!(&buf[..n], b"6789");
|
||||
|
||||
// Entirely past the end is zero bytes, not an error.
|
||||
let n = read_at(f.as_file(), &mut buf, 10).expect("read");
|
||||
assert_eq!(n, 0);
|
||||
}
|
||||
|
||||
/// A source that returns at most `chunk` bytes per call and fails with
|
||||
/// `Interrupted` on its first call, like a network mount under a signal.
|
||||
fn chunked(src: &[u8], chunk: usize) -> impl FnMut(&mut [u8], u64) -> std::io::Result<usize> {
|
||||
let mut interrupted = false;
|
||||
move |buf, at| {
|
||||
if !interrupted {
|
||||
interrupted = true;
|
||||
return Err(ErrorKind::Interrupted.into());
|
||||
}
|
||||
let at = (at as usize).min(src.len());
|
||||
let n = buf.len().min(chunk).min(src.len() - at);
|
||||
buf[..n].copy_from_slice(&src[at..at + n]);
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_at_fills_across_short_and_interrupted_reads() {
|
||||
let src: Vec<u8> = (0..=255).collect();
|
||||
let mut buf = [0u8; 100];
|
||||
let n = fill_at(chunked(&src, 7), &mut buf, 50).expect("read");
|
||||
assert_eq!(n, buf.len());
|
||||
assert_eq!(&buf[..], &src[50..150]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_at_stops_at_end_of_source() {
|
||||
let src: Vec<u8> = (0..=255).collect();
|
||||
let mut buf = [0u8; 100];
|
||||
let n = fill_at(chunked(&src, 7), &mut buf, 200).expect("read");
|
||||
assert_eq!(n, 56);
|
||||
assert_eq!(&buf[..n], &src[200..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_at_propagates_other_errors() {
|
||||
let mut buf = [0u8; 8];
|
||||
let err = fill_at(|_, _| Err(ErrorKind::PermissionDenied.into()), &mut buf, 0)
|
||||
.expect_err("error");
|
||||
assert_eq!(err.kind(), ErrorKind::PermissionDenied);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_full_at_returns_the_short_count_only_at_eof() {
|
||||
let f = temp_file(b"0123456789");
|
||||
let mut buf = [0u8; 8];
|
||||
assert_eq!(read_full_at(f.as_file(), &mut buf, 0).expect("read"), 8);
|
||||
assert_eq!(&buf, b"01234567");
|
||||
assert_eq!(read_full_at(f.as_file(), &mut buf, 6).expect("read"), 4);
|
||||
assert_eq!(&buf[..4], b"6789");
|
||||
assert_eq!(read_full_at(f.as_file(), &mut buf, 10).expect("read"), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
//! Consecutive storage-media error tracking shared by `Volume` and
|
||||
//! `EcVolume`. Mirrors Go's `weed/storage/io_error.go`.
|
||||
|
||||
use std::io;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
|
||||
/// Consecutive storage-media errors allowed before the volume is quarantined.
|
||||
pub(crate) const IO_ERROR_TOLERANCE: i32 = 3;
|
||||
|
||||
/// Returns true for I/O errors that indicate faulty storage media, not
|
||||
/// transient/network failures. On Unix this is EIO; on Windows it covers
|
||||
/// ERROR_CRC and ERROR_IO_DEVICE, which the kernel returns for failing disks.
|
||||
pub(crate) fn is_storage_io_error(e: &io::Error) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
e.raw_os_error() == Some(libc::EIO)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
const ERROR_CRC: i32 = 23;
|
||||
const ERROR_IO_DEVICE: i32 = 1117;
|
||||
return e.raw_os_error() == Some(ERROR_CRC) || e.raw_os_error() == Some(ERROR_IO_DEVICE);
|
||||
}
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Consecutive storage-media error state for one volume. `quarantined` is
|
||||
/// sticky: once set it survives later successful I/O and is lifted only by
|
||||
/// `reset_io_error_state`.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct IoErrorTracker {
|
||||
last: Mutex<Option<String>>,
|
||||
/// The consecutive error count in the low 32 bits and, in the high 32,
|
||||
/// how many times it has been cleared. They share one word so that
|
||||
/// `record_success_at` updates both in one step: reads record their
|
||||
/// outcomes here without the volume's write lock.
|
||||
streak: AtomicU64,
|
||||
quarantined: AtomicBool,
|
||||
}
|
||||
|
||||
const STREAK_COUNT_BITS: u64 = 0xffff_ffff;
|
||||
|
||||
fn streak_count(streak: u64) -> i32 {
|
||||
(streak & STREAK_COUNT_BITS) as i32
|
||||
}
|
||||
|
||||
/// `streak` with its count cleared and one more clear on record.
|
||||
fn streak_cleared(streak: u64) -> u64 {
|
||||
(streak >> 32).wrapping_add(1) << 32
|
||||
}
|
||||
|
||||
/// A point in the error streak, taken where a write landed whose success
|
||||
/// is only recorded later. See `IoErrorTracker::record_success_at`.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct StreakMark(u64);
|
||||
|
||||
impl IoErrorTracker {
|
||||
/// `Some(e)` records a failure, `None` a success. Only storage-media
|
||||
/// failures count; every other outcome clears the count and last error.
|
||||
pub(crate) fn check_read_write_error(&self, err: Option<&io::Error>) {
|
||||
if let Some(e) = err
|
||||
&& is_storage_io_error(e)
|
||||
{
|
||||
self.streak.fetch_add(1, Ordering::Relaxed);
|
||||
if let Ok(mut guard) = self.last.lock() {
|
||||
*guard = Some(e.to_string());
|
||||
}
|
||||
crate::metrics::STORAGE_IO_ERROR_COUNTER.inc();
|
||||
return;
|
||||
}
|
||||
self.clear_count();
|
||||
self.clear_last();
|
||||
}
|
||||
|
||||
fn clear_count(&self) {
|
||||
self.update_streak(|streak| Some(streak_cleared(streak)));
|
||||
}
|
||||
|
||||
fn clear_last(&self) {
|
||||
if let Ok(mut guard) = self.last.lock()
|
||||
&& guard.is_some()
|
||||
{
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply `f` to the streak atomically; `None` leaves it as it is.
|
||||
/// Returns the streak `f` produced, if any.
|
||||
fn update_streak(&self, mut f: impl FnMut(u64) -> Option<u64>) -> Option<u64> {
|
||||
let mut updated = None;
|
||||
let _ = self
|
||||
.streak
|
||||
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |streak| {
|
||||
updated = f(streak);
|
||||
updated
|
||||
});
|
||||
updated
|
||||
}
|
||||
|
||||
pub(crate) fn mark(&self) -> StreakMark {
|
||||
StreakMark(self.streak.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// Record a success as if it had come at `mark`: the errors counted
|
||||
/// before the mark are cleared and the ones counted since still stand,
|
||||
/// as they would had each outcome been recorded in order. A streak
|
||||
/// cleared since the mark is left as it is.
|
||||
pub(crate) fn record_success_at(&self, mark: StreakMark) {
|
||||
let before = streak_count(mark.0);
|
||||
let updated = self.update_streak(|streak| {
|
||||
if streak >> 32 != mark.0 >> 32 {
|
||||
return None;
|
||||
}
|
||||
if streak_count(streak) <= before {
|
||||
return Some(streak_cleared(streak));
|
||||
}
|
||||
Some(streak - before as u64)
|
||||
});
|
||||
// The last error stays when one counted since the mark is left.
|
||||
if updated.is_some_and(|streak| streak_count(streak) == 0) {
|
||||
self.clear_last();
|
||||
}
|
||||
}
|
||||
|
||||
/// The last recorded error, the consecutive count, and the quarantine flag.
|
||||
pub(crate) fn get_io_error_state(&self) -> (Option<String>, i32, bool) {
|
||||
let err = self.last.lock().ok().and_then(|g| g.clone());
|
||||
let count = self.count();
|
||||
let quarantined = self.quarantined.load(Ordering::Relaxed);
|
||||
(err, count, quarantined)
|
||||
}
|
||||
|
||||
fn count(&self) -> i32 {
|
||||
streak_count(self.streak.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
pub(crate) fn should_quarantine(&self) -> bool {
|
||||
self.quarantined.load(Ordering::Relaxed) || self.count() >= IO_ERROR_TOLERANCE
|
||||
}
|
||||
|
||||
pub(crate) fn mark_io_quarantined(&self) {
|
||||
self.quarantined.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn reset_io_error_state(&self) {
|
||||
self.clear_count();
|
||||
self.quarantined.store(false, Ordering::Relaxed);
|
||||
if let Ok(mut guard) = self.last.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_last_io_error_for_test(&self, err: Option<&str>) {
|
||||
if let Ok(mut guard) = self.last.lock() {
|
||||
*guard = err.map(|value| value.to_string());
|
||||
}
|
||||
if err.is_some() {
|
||||
self.update_streak(|streak| {
|
||||
Some((streak & !STREAK_COUNT_BITS) | IO_ERROR_TOLERANCE as u64)
|
||||
});
|
||||
} else {
|
||||
self.clear_count();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The tracker only reacts to errors `is_storage_io_error` recognises, which is
|
||||
// nothing at all on a platform that is neither Unix nor Windows.
|
||||
#[cfg(all(test, any(unix, windows)))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// An OS error the platform reports for failing storage media.
|
||||
#[cfg(unix)]
|
||||
fn media_error() -> io::Error {
|
||||
io::Error::from_raw_os_error(libc::EIO)
|
||||
}
|
||||
|
||||
/// An OS error the platform reports for failing storage media.
|
||||
#[cfg(windows)]
|
||||
fn media_error() -> io::Error {
|
||||
const ERROR_IO_DEVICE: i32 = 1117;
|
||||
io::Error::from_raw_os_error(ERROR_IO_DEVICE)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_read_write_error_counts_consecutive_media_errors() {
|
||||
let tracker = IoErrorTracker::default();
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
|
||||
let (last, count, quarantined) = tracker.get_io_error_state();
|
||||
assert_eq!(last, Some(media_error().to_string()));
|
||||
assert_eq!(count, 2);
|
||||
assert!(!quarantined);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_clears_the_count_and_the_last_error() {
|
||||
let tracker = IoErrorTracker::default();
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
tracker.check_read_write_error(None);
|
||||
|
||||
assert_eq!(tracker.get_io_error_state(), (None, 0, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_media_error_clears_the_count() {
|
||||
let tracker = IoErrorTracker::default();
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
tracker.check_read_write_error(Some(&io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"no such file",
|
||||
)));
|
||||
|
||||
assert_eq!(tracker.get_io_error_state(), (None, 0, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_quarantine_only_once_the_tolerance_is_reached() {
|
||||
let tracker = IoErrorTracker::default();
|
||||
for _ in 1..IO_ERROR_TOLERANCE {
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
assert!(!tracker.should_quarantine());
|
||||
}
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
assert!(tracker.should_quarantine());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quarantine_survives_later_successful_io() {
|
||||
let tracker = IoErrorTracker::default();
|
||||
tracker.mark_io_quarantined();
|
||||
tracker.check_read_write_error(None);
|
||||
|
||||
assert_eq!(tracker.get_io_error_state(), (None, 0, true));
|
||||
assert!(tracker.should_quarantine());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_at_a_mark_keeps_only_the_errors_after_it() {
|
||||
let tracker = IoErrorTracker::default();
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
let mark = tracker.mark();
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
tracker.record_success_at(mark);
|
||||
|
||||
assert_eq!(
|
||||
tracker.get_io_error_state(),
|
||||
(Some(media_error().to_string()), 1, false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_at_a_mark_with_nothing_after_it_clears_the_streak() {
|
||||
let tracker = IoErrorTracker::default();
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
let mark = tracker.mark();
|
||||
tracker.record_success_at(mark);
|
||||
|
||||
assert_eq!(tracker.get_io_error_state(), (None, 0, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_at_a_mark_leaves_a_streak_cleared_since() {
|
||||
let tracker = IoErrorTracker::default();
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
let mark = tracker.mark();
|
||||
tracker.check_read_write_error(None);
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
tracker.record_success_at(mark);
|
||||
|
||||
assert_eq!(
|
||||
tracker.get_io_error_state(),
|
||||
(Some(media_error().to_string()), 1, false)
|
||||
);
|
||||
}
|
||||
|
||||
/// Reads update the tracker without the volume's write lock, so a
|
||||
/// success replayed at a mark must not lose the errors they record
|
||||
/// while it runs.
|
||||
#[test]
|
||||
fn success_at_a_mark_keeps_concurrent_errors() {
|
||||
use std::sync::{Arc, Barrier};
|
||||
const READERS: i32 = 4;
|
||||
const ERRORS: i32 = 200;
|
||||
|
||||
for _ in 0..500 {
|
||||
let tracker = Arc::new(IoErrorTracker::default());
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
let mark = tracker.mark();
|
||||
let start = Arc::new(Barrier::new(READERS as usize + 1));
|
||||
let readers: Vec<_> = (0..READERS)
|
||||
.map(|_| {
|
||||
let (tracker, start) = (tracker.clone(), start.clone());
|
||||
std::thread::spawn(move || {
|
||||
start.wait();
|
||||
for _ in 0..ERRORS {
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
start.wait();
|
||||
while tracker.get_io_error_state().1 < 2 + READERS * ERRORS / 2 {
|
||||
std::hint::spin_loop();
|
||||
}
|
||||
tracker.record_success_at(mark);
|
||||
for reader in readers {
|
||||
reader.join().unwrap();
|
||||
}
|
||||
// The two errors before the mark are cleared; every error the
|
||||
// readers recorded after it stands.
|
||||
assert_eq!(tracker.get_io_error_state().1, READERS * ERRORS);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_io_error_state_lifts_the_quarantine() {
|
||||
let tracker = IoErrorTracker::default();
|
||||
tracker.check_read_write_error(Some(&media_error()));
|
||||
tracker.mark_io_quarantined();
|
||||
tracker.reset_io_error_state();
|
||||
|
||||
assert_eq!(tracker.get_io_error_state(), (None, 0, false));
|
||||
assert!(!tracker.should_quarantine());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_helper_arms_a_sustained_error() {
|
||||
let tracker = IoErrorTracker::default();
|
||||
tracker.set_last_io_error_for_test(Some("input/output error"));
|
||||
assert!(tracker.should_quarantine());
|
||||
assert_eq!(
|
||||
tracker.get_io_error_state(),
|
||||
(
|
||||
Some("input/output error".to_string()),
|
||||
IO_ERROR_TOLERANCE,
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
tracker.set_last_io_error_for_test(None);
|
||||
assert_eq!(tracker.get_io_error_state(), (None, 0, false));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod disk_location;
|
||||
pub mod erasure_coding;
|
||||
pub mod idx;
|
||||
pub(crate) mod io;
|
||||
pub(crate) mod io_error;
|
||||
pub mod needle;
|
||||
pub mod needle_map;
|
||||
pub mod store;
|
||||
|
||||
@@ -21,7 +21,7 @@ impl CRC {
|
||||
/// Legacy `.Value()` function — deprecated in Go but needed for backward compat check.
|
||||
/// Formula: (crc >> 15 | crc << 17) + 0xa282ead8
|
||||
pub fn legacy_value(&self) -> u32 {
|
||||
(self.0 >> 15 | self.0 << 17).wrapping_add(0xa282ead8)
|
||||
self.0.rotate_right(15).wrapping_add(0xa282ead8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,9 @@ mod tests {
|
||||
fn test_crc_legacy_value() {
|
||||
let crc = CRC(0x12345678);
|
||||
let v = crc.legacy_value();
|
||||
let expected = (0x12345678u32 >> 15 | 0x12345678u32 << 17).wrapping_add(0xa282ead8);
|
||||
// (0x12345678 >> 15 | 0x12345678 << 17) + 0xa282ead8, worked out by hand so
|
||||
// the test checks the rotate rather than restating it.
|
||||
let expected = 0x4f730f40_u32;
|
||||
assert_eq!(v, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
pub mod crc;
|
||||
#[expect(
|
||||
clippy::module_inception,
|
||||
reason = "needle/needle.rs mirrors the Go package layout"
|
||||
)]
|
||||
pub mod needle;
|
||||
pub mod ttl;
|
||||
|
||||
|
||||
@@ -198,8 +198,8 @@ impl Needle {
|
||||
/// the data payload from disk at all, matching Go's `ReadNeedleMeta`.
|
||||
pub fn read_paged_meta(
|
||||
&mut self,
|
||||
header_bytes: &[u8], // first 20 bytes: NEEDLE_HEADER_SIZE + DATA_SIZE_SIZE
|
||||
meta_bytes: &[u8], // tail: non-data body metadata + checksum + timestamp + padding
|
||||
header_bytes: &[u8], // first 20 bytes: NEEDLE_HEADER_SIZE + DATA_SIZE_SIZE
|
||||
meta_bytes: &[u8], // tail: non-data body metadata + checksum + timestamp + padding
|
||||
offset: i64,
|
||||
expected_size: Size,
|
||||
version: Version,
|
||||
@@ -560,7 +560,7 @@ impl Needle {
|
||||
|
||||
// Padding to 8-byte alignment
|
||||
let padding = padding_length(self.size, version).0 as usize;
|
||||
buf.extend(std::iter::repeat(0u8).take(padding));
|
||||
buf.extend(std::iter::repeat_n(0u8, padding));
|
||||
|
||||
buf
|
||||
}
|
||||
@@ -581,23 +581,19 @@ impl Needle {
|
||||
// ============================================================================
|
||||
|
||||
/// Compute padding to align needle to NEEDLE_PADDING_SIZE (8 bytes).
|
||||
///
|
||||
/// The sum is formed in i64: a size read from a corrupt header can sit near
|
||||
/// `i32::MAX`, and adding the header, checksum and timestamp widths to it in
|
||||
/// i32 would overflow (a panic with overflow checks, a wrapped padding
|
||||
/// without). The result is at most NEEDLE_PADDING_SIZE, so it fits `Size`.
|
||||
pub fn padding_length(needle_size: Size, version: Version) -> Size {
|
||||
if version == VERSION_3 {
|
||||
Size(
|
||||
NEEDLE_PADDING_SIZE as i32
|
||||
- ((NEEDLE_HEADER_SIZE as i32
|
||||
+ needle_size.0
|
||||
+ NEEDLE_CHECKSUM_SIZE as i32
|
||||
+ TIMESTAMP_SIZE as i32)
|
||||
% NEEDLE_PADDING_SIZE as i32),
|
||||
)
|
||||
let fixed = if version == VERSION_3 {
|
||||
NEEDLE_HEADER_SIZE + NEEDLE_CHECKSUM_SIZE + TIMESTAMP_SIZE
|
||||
} else {
|
||||
Size(
|
||||
NEEDLE_PADDING_SIZE as i32
|
||||
- ((NEEDLE_HEADER_SIZE as i32 + needle_size.0 + NEEDLE_CHECKSUM_SIZE as i32)
|
||||
% NEEDLE_PADDING_SIZE as i32),
|
||||
)
|
||||
}
|
||||
NEEDLE_HEADER_SIZE + NEEDLE_CHECKSUM_SIZE
|
||||
};
|
||||
let unpadded = fixed as i64 + needle_size.0 as i64;
|
||||
Size((NEEDLE_PADDING_SIZE as i64 - unpadded % NEEDLE_PADDING_SIZE as i64) as i32)
|
||||
}
|
||||
|
||||
/// Body length = Size + Checksum + [Timestamp] + Padding.
|
||||
@@ -619,6 +615,30 @@ pub fn get_actual_size(size: Size, version: Version) -> i64 {
|
||||
NEEDLE_HEADER_SIZE as i64 + needle_body_length(size, version)
|
||||
}
|
||||
|
||||
/// Validate a wire-supplied needle body size before any `as usize` cast.
|
||||
/// Rejects negative/deleted sizes and bodies larger than the gRPC max message.
|
||||
/// Size(0) is allowed: empty/anomalous entries and tombstones read as size 0
|
||||
/// (actual_size = header+checksum+pad > 0, safe alloc, no wrap).
|
||||
/// Transport cap only: storage paths must NOT use this cap — see volume.rs
|
||||
/// guards (a >1GiB stored needle from a high-limit cluster must remain
|
||||
/// readable/compaction-safe). Keep `get_actual_size` unchanged (it
|
||||
/// intentionally returns negative for deleted index entries).
|
||||
pub fn validate_wire_size(size: Size) -> Result<(), String> {
|
||||
if size.0 < 0 {
|
||||
return Err(format!("invalid needle size {}", size.0));
|
||||
}
|
||||
// Keep in sync with canonical `GRPC_MAX_MESSAGE_SIZE` in server/grpc_client.rs:10
|
||||
// (duplicated here to avoid a storage->server import and prevent drift).
|
||||
const WIRE_MAX_NEEDLE_SIZE: i32 = 1 << 30;
|
||||
if size.0 > WIRE_MAX_NEEDLE_SIZE {
|
||||
return Err(format!(
|
||||
"needle size {} exceeds max {}",
|
||||
size.0, WIRE_MAX_NEEDLE_SIZE
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read 5 bytes as a u64 (big-endian, zero-padded high bytes).
|
||||
fn bytes_to_u64_5(bytes: &[u8]) -> u64 {
|
||||
assert!(bytes.len() >= 5);
|
||||
@@ -729,6 +749,14 @@ pub fn parse_needle_id_cookie(s: &str) -> Result<(NeedleId, Cookie), String> {
|
||||
(s, None)
|
||||
};
|
||||
|
||||
// Every length check and the split below are in BYTES, so a multi-byte
|
||||
// character would let `split` land inside one and panic the slice. Hex is
|
||||
// ASCII by definition; reject anything else up front, as Go's ParseUint
|
||||
// does a step later.
|
||||
if !hex_part.is_ascii() {
|
||||
return Err("KeyHash must be ASCII hex.".to_string());
|
||||
}
|
||||
|
||||
// Go: len(key_hash_string) <= CookieSize*2 => error (must be > 8 hex chars)
|
||||
if hex_part.len() <= COOKIE_SIZE * 2 {
|
||||
return Err("KeyHash is too short.".to_string());
|
||||
@@ -770,7 +798,9 @@ pub fn parse_needle_id_cookie(s: &str) -> Result<(NeedleId, Cookie), String> {
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum NeedleError {
|
||||
#[error("size mismatch at offset {offset}: found id={id} size={found:?}, expected size={expected:?}")]
|
||||
#[error(
|
||||
"size mismatch at offset {offset}: found id={id} size={found:?}, expected size={expected:?}"
|
||||
)]
|
||||
SizeMismatch {
|
||||
offset: i64,
|
||||
id: NeedleId,
|
||||
@@ -806,6 +836,30 @@ pub enum NeedleError {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A fid whose hex part carries multi-byte UTF-8 must be rejected, not
|
||||
/// panic. `split` is a byte offset into `hex_part`; before the ASCII guard
|
||||
/// `&hex_part[..split]` could land inside a character. `GET /3,ééééa` is
|
||||
/// nine bytes, so it passes the length checks and splits at byte 1 —
|
||||
/// halfway through the first `é`. Go's `ParseUint` just errors.
|
||||
#[test]
|
||||
fn parse_needle_id_cookie_rejects_non_ascii_instead_of_panicking() {
|
||||
for s in ["ééééa", "ééééaaaaa", "0123456é9abc", "ééééa_1"] {
|
||||
assert!(
|
||||
parse_needle_id_cookie(s).is_err(),
|
||||
"non-ASCII fid {:?} must be an error",
|
||||
s
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The ASCII guard must not change any accepted input.
|
||||
#[test]
|
||||
fn parse_needle_id_cookie_still_accepts_ascii_hex() {
|
||||
let (id, cookie) = parse_needle_id_cookie("01637037d6").unwrap();
|
||||
assert_eq!(id, NeedleId(0x01));
|
||||
assert_eq!(cookie, Cookie(0x637037d6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_header() {
|
||||
let mut buf = [0u8; NEEDLE_HEADER_SIZE];
|
||||
@@ -824,11 +878,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_needle_write_read_round_trip_v3() {
|
||||
let mut n = Needle::default();
|
||||
n.cookie = Cookie(42);
|
||||
n.id = NeedleId(100);
|
||||
n.data = b"hello world".to_vec();
|
||||
n.flags = 0;
|
||||
let mut n = Needle {
|
||||
cookie: Cookie(42),
|
||||
id: NeedleId(100),
|
||||
data: b"hello world".to_vec(),
|
||||
flags: 0,
|
||||
..Needle::default()
|
||||
};
|
||||
n.set_has_name();
|
||||
n.name = b"test.txt".to_vec();
|
||||
n.name_size = 8;
|
||||
@@ -867,11 +923,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_needle_write_read_round_trip_v2() {
|
||||
let mut n = Needle::default();
|
||||
n.cookie = Cookie(77);
|
||||
n.id = NeedleId(200);
|
||||
n.data = b"data v2".to_vec();
|
||||
n.flags = 0;
|
||||
let mut n = Needle {
|
||||
cookie: Cookie(77),
|
||||
id: NeedleId(200),
|
||||
data: b"data v2".to_vec(),
|
||||
flags: 0,
|
||||
..Needle::default()
|
||||
};
|
||||
|
||||
let bytes = n.write_bytes(VERSION_2);
|
||||
let expected_size = get_actual_size(n.size, VERSION_2);
|
||||
@@ -886,10 +944,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_read_bytes_meta_only_handles_tombstone_v3() {
|
||||
let mut tombstone = Needle::default();
|
||||
tombstone.cookie = Cookie(0x1234abcd);
|
||||
tombstone.id = NeedleId(300);
|
||||
tombstone.append_at_ns = 999_999;
|
||||
let mut tombstone = Needle {
|
||||
cookie: Cookie(0x1234abcd),
|
||||
id: NeedleId(300),
|
||||
append_at_ns: 999_999,
|
||||
..Needle::default()
|
||||
};
|
||||
|
||||
let bytes = tombstone.write_bytes(VERSION_3);
|
||||
|
||||
@@ -917,6 +977,21 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padding_length_does_not_overflow_on_a_corrupt_size() {
|
||||
// A header read from a corrupt or truncated file can carry any i32
|
||||
// size. The scanners bound it against the bytes left before sizing a
|
||||
// buffer, but on a volume with more than 2 GiB left a size near
|
||||
// i32::MAX passes that bound, so the padding arithmetic itself must
|
||||
// not overflow. Overflow checks are on in test builds, so an i32 sum
|
||||
// here would panic rather than wrap.
|
||||
for version in [VERSION_2, VERSION_3] {
|
||||
let padding = padding_length(Size(i32::MAX), version).0 as i64;
|
||||
assert!((1..=NEEDLE_PADDING_SIZE as i64).contains(&padding));
|
||||
assert_eq!(get_actual_size(Size(i32::MAX), version) % 8, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_id_parse() {
|
||||
let fid = FileId::parse("3,01637037d6").unwrap();
|
||||
@@ -961,4 +1036,16 @@ mod tests {
|
||||
assert_eq!(fid.key, NeedleId(0x123));
|
||||
assert_eq!(fid.cookie, Cookie(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_wire_size_boundaries() {
|
||||
assert!(validate_wire_size(Size(-100)).is_err());
|
||||
assert!(validate_wire_size(Size(-1)).is_err());
|
||||
assert!(validate_wire_size(Size(0)).is_ok());
|
||||
assert!(validate_wire_size(Size(1024)).is_ok());
|
||||
assert!(validate_wire_size(Size(1)).is_ok());
|
||||
assert!(validate_wire_size(Size(1 << 30)).is_ok());
|
||||
assert!(validate_wire_size(Size((1 << 30) + 1)).is_err());
|
||||
assert!(validate_wire_size(Size(i32::MAX)).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +80,14 @@ impl TTL {
|
||||
if s.is_empty() {
|
||||
return Ok(TTL::EMPTY);
|
||||
}
|
||||
// The unit is read as the last BYTE and the count as everything before
|
||||
// it, so a trailing multi-byte character would split inside itself and
|
||||
// panic. A TTL is digits plus a one-letter unit; reject the rest.
|
||||
if !s.is_ascii() {
|
||||
return Err(format!("invalid TTL {:?}: must be ASCII", s));
|
||||
}
|
||||
let last_byte = s.as_bytes()[s.len() - 1];
|
||||
let (num_str, unit_byte) = if last_byte >= b'0' && last_byte <= b'9' {
|
||||
let (num_str, unit_byte) = if last_byte.is_ascii_digit() {
|
||||
// All digits — default to minutes (matching Go)
|
||||
(s, b'm')
|
||||
} else {
|
||||
@@ -144,40 +150,73 @@ fn fit_ttl_count(count: u32, unit: u8) -> TTL {
|
||||
const MINUTE_SECS: u64 = 60;
|
||||
|
||||
// First pass: try exact fits from largest to smallest
|
||||
if seconds % YEAR_SECS == 0 && seconds / YEAR_SECS < 256 {
|
||||
return TTL { count: (seconds / YEAR_SECS) as u8, unit: TTL_UNIT_YEAR };
|
||||
if seconds.is_multiple_of(YEAR_SECS) && seconds / YEAR_SECS < 256 {
|
||||
return TTL {
|
||||
count: (seconds / YEAR_SECS) as u8,
|
||||
unit: TTL_UNIT_YEAR,
|
||||
};
|
||||
}
|
||||
if seconds % MONTH_SECS == 0 && seconds / MONTH_SECS < 256 {
|
||||
return TTL { count: (seconds / MONTH_SECS) as u8, unit: TTL_UNIT_MONTH };
|
||||
if seconds.is_multiple_of(MONTH_SECS) && seconds / MONTH_SECS < 256 {
|
||||
return TTL {
|
||||
count: (seconds / MONTH_SECS) as u8,
|
||||
unit: TTL_UNIT_MONTH,
|
||||
};
|
||||
}
|
||||
if seconds % WEEK_SECS == 0 && seconds / WEEK_SECS < 256 {
|
||||
return TTL { count: (seconds / WEEK_SECS) as u8, unit: TTL_UNIT_WEEK };
|
||||
if seconds.is_multiple_of(WEEK_SECS) && seconds / WEEK_SECS < 256 {
|
||||
return TTL {
|
||||
count: (seconds / WEEK_SECS) as u8,
|
||||
unit: TTL_UNIT_WEEK,
|
||||
};
|
||||
}
|
||||
if seconds % DAY_SECS == 0 && seconds / DAY_SECS < 256 {
|
||||
return TTL { count: (seconds / DAY_SECS) as u8, unit: TTL_UNIT_DAY };
|
||||
if seconds.is_multiple_of(DAY_SECS) && seconds / DAY_SECS < 256 {
|
||||
return TTL {
|
||||
count: (seconds / DAY_SECS) as u8,
|
||||
unit: TTL_UNIT_DAY,
|
||||
};
|
||||
}
|
||||
if seconds % HOUR_SECS == 0 && seconds / HOUR_SECS < 256 {
|
||||
return TTL { count: (seconds / HOUR_SECS) as u8, unit: TTL_UNIT_HOUR };
|
||||
if seconds.is_multiple_of(HOUR_SECS) && seconds / HOUR_SECS < 256 {
|
||||
return TTL {
|
||||
count: (seconds / HOUR_SECS) as u8,
|
||||
unit: TTL_UNIT_HOUR,
|
||||
};
|
||||
}
|
||||
// Minutes: truncating division
|
||||
if seconds / MINUTE_SECS < 256 {
|
||||
return TTL { count: (seconds / MINUTE_SECS) as u8, unit: TTL_UNIT_MINUTE };
|
||||
return TTL {
|
||||
count: (seconds / MINUTE_SECS) as u8,
|
||||
unit: TTL_UNIT_MINUTE,
|
||||
};
|
||||
}
|
||||
// Second pass: truncating division from smallest to largest
|
||||
if seconds / HOUR_SECS < 256 {
|
||||
return TTL { count: (seconds / HOUR_SECS) as u8, unit: TTL_UNIT_HOUR };
|
||||
return TTL {
|
||||
count: (seconds / HOUR_SECS) as u8,
|
||||
unit: TTL_UNIT_HOUR,
|
||||
};
|
||||
}
|
||||
if seconds / DAY_SECS < 256 {
|
||||
return TTL { count: (seconds / DAY_SECS) as u8, unit: TTL_UNIT_DAY };
|
||||
return TTL {
|
||||
count: (seconds / DAY_SECS) as u8,
|
||||
unit: TTL_UNIT_DAY,
|
||||
};
|
||||
}
|
||||
if seconds / WEEK_SECS < 256 {
|
||||
return TTL { count: (seconds / WEEK_SECS) as u8, unit: TTL_UNIT_WEEK };
|
||||
return TTL {
|
||||
count: (seconds / WEEK_SECS) as u8,
|
||||
unit: TTL_UNIT_WEEK,
|
||||
};
|
||||
}
|
||||
if seconds / MONTH_SECS < 256 {
|
||||
return TTL { count: (seconds / MONTH_SECS) as u8, unit: TTL_UNIT_MONTH };
|
||||
return TTL {
|
||||
count: (seconds / MONTH_SECS) as u8,
|
||||
unit: TTL_UNIT_MONTH,
|
||||
};
|
||||
}
|
||||
if seconds / YEAR_SECS < 256 {
|
||||
return TTL { count: (seconds / YEAR_SECS) as u8, unit: TTL_UNIT_YEAR };
|
||||
return TTL {
|
||||
count: (seconds / YEAR_SECS) as u8,
|
||||
unit: TTL_UNIT_YEAR,
|
||||
};
|
||||
}
|
||||
TTL::EMPTY
|
||||
}
|
||||
@@ -207,6 +246,16 @@ impl fmt::Display for TTL {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `?ttl=5%C3%A9` must be an error, not a panic. The unit is taken as the
|
||||
/// last *byte*, so a trailing multi-byte character made `&s[..s.len()-1]`
|
||||
/// split inside it.
|
||||
#[test]
|
||||
fn ttl_read_rejects_non_ascii_instead_of_panicking() {
|
||||
for s in ["5é", "é", "3🦀", "12é"] {
|
||||
assert!(TTL::read(s).is_err(), "non-ASCII TTL {:?} must error", s);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ttl_parse() {
|
||||
let ttl = TTL::read("3m").unwrap();
|
||||
@@ -225,7 +274,13 @@ mod tests {
|
||||
// 24h normalizes to 1d via fitTtlCount
|
||||
let ttl = TTL::read("24h").unwrap();
|
||||
assert_eq!(ttl.to_seconds(), 86400);
|
||||
assert_eq!(ttl, TTL { count: 1, unit: TTL_UNIT_DAY });
|
||||
assert_eq!(
|
||||
ttl,
|
||||
TTL {
|
||||
count: 1,
|
||||
unit: TTL_UNIT_DAY
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -271,12 +326,24 @@ mod tests {
|
||||
fn test_ttl_overflow_normalizes() {
|
||||
// Go's ReadTTL calls fitTtlCount: 300m = 18000s = 5h (exact fit)
|
||||
let ttl = TTL::read("300m").unwrap();
|
||||
assert_eq!(ttl, TTL { count: 5, unit: TTL_UNIT_HOUR });
|
||||
assert_eq!(
|
||||
ttl,
|
||||
TTL {
|
||||
count: 5,
|
||||
unit: TTL_UNIT_HOUR
|
||||
}
|
||||
);
|
||||
|
||||
// 256h = 921600s. Doesn't fit in hours (256 >= 256), doesn't fit exact in days.
|
||||
// Second pass: 921600/86400 = 10 (truncated) < 256 -> 10d
|
||||
let ttl = TTL::read("256h").unwrap();
|
||||
assert_eq!(ttl, TTL { count: 10, unit: TTL_UNIT_DAY });
|
||||
assert_eq!(
|
||||
ttl,
|
||||
TTL {
|
||||
count: 10,
|
||||
unit: TTL_UNIT_DAY
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -284,19 +351,49 @@ mod tests {
|
||||
// Go's ReadTTL calls fitTtlCount which normalizes to coarsest unit.
|
||||
// 120m -> 2h, 7d -> 1w, 24h -> 1d.
|
||||
let ttl = TTL::read("120m").unwrap();
|
||||
assert_eq!(ttl, TTL { count: 2, unit: TTL_UNIT_HOUR });
|
||||
assert_eq!(
|
||||
ttl,
|
||||
TTL {
|
||||
count: 2,
|
||||
unit: TTL_UNIT_HOUR
|
||||
}
|
||||
);
|
||||
|
||||
let ttl = TTL::read("7d").unwrap();
|
||||
assert_eq!(ttl, TTL { count: 1, unit: TTL_UNIT_WEEK });
|
||||
assert_eq!(
|
||||
ttl,
|
||||
TTL {
|
||||
count: 1,
|
||||
unit: TTL_UNIT_WEEK
|
||||
}
|
||||
);
|
||||
|
||||
let ttl = TTL::read("24h").unwrap();
|
||||
assert_eq!(ttl, TTL { count: 1, unit: TTL_UNIT_DAY });
|
||||
assert_eq!(
|
||||
ttl,
|
||||
TTL {
|
||||
count: 1,
|
||||
unit: TTL_UNIT_DAY
|
||||
}
|
||||
);
|
||||
|
||||
// Values that don't simplify stay as-is
|
||||
let ttl = TTL::read("5d").unwrap();
|
||||
assert_eq!(ttl, TTL { count: 5, unit: TTL_UNIT_DAY });
|
||||
assert_eq!(
|
||||
ttl,
|
||||
TTL {
|
||||
count: 5,
|
||||
unit: TTL_UNIT_DAY
|
||||
}
|
||||
);
|
||||
|
||||
let ttl = TTL::read("3m").unwrap();
|
||||
assert_eq!(ttl, TTL { count: 3, unit: TTL_UNIT_MINUTE });
|
||||
assert_eq!(
|
||||
ttl,
|
||||
TTL {
|
||||
count: 3,
|
||||
unit: TTL_UNIT_MINUTE
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,12 +97,13 @@ impl NeedleMapMetric {
|
||||
self.file_byte_count
|
||||
.fetch_add(new_size.0 as u64, Ordering::Relaxed);
|
||||
// Go: if oldSize > 0 && oldSize.IsValid() { LogDeletionCounter(oldSize) }
|
||||
if let Some(old_val) = old {
|
||||
if old_val.size.0 > 0 && old_val.size.is_valid() {
|
||||
self.deletion_count.fetch_add(1, Ordering::Relaxed);
|
||||
self.deletion_byte_count
|
||||
.fetch_add(old_val.size.0 as u64, Ordering::Relaxed);
|
||||
}
|
||||
if let Some(old_val) = old
|
||||
&& old_val.size.0 > 0
|
||||
&& old_val.size.is_valid()
|
||||
{
|
||||
self.deletion_count.fetch_add(1, Ordering::Relaxed);
|
||||
self.deletion_byte_count
|
||||
.fetch_add(old_val.size.0 as u64, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +226,12 @@ pub struct CompactNeedleMap {
|
||||
idx_file_offset: u64,
|
||||
}
|
||||
|
||||
impl Default for CompactNeedleMap {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CompactNeedleMap {
|
||||
/// Create a new empty in-memory map.
|
||||
pub fn new() -> Self {
|
||||
@@ -240,6 +247,8 @@ impl CompactNeedleMap {
|
||||
pub fn load_from_idx<R: Read + Seek>(reader: &mut R, version: Version) -> io::Result<Self> {
|
||||
let mut nm = CompactNeedleMap::new();
|
||||
idx::walk_index_file(reader, 0, |key, offset, size| {
|
||||
// A read-only load attaches no writer, so this is its only size.
|
||||
nm.idx_file_offset += NEEDLE_MAP_ENTRY_SIZE as u64;
|
||||
nm.metric.maybe_set_max_needle_end(offset, size, version);
|
||||
if offset.is_zero() || size.is_deleted() {
|
||||
nm.delete_from_map(key);
|
||||
@@ -410,9 +419,9 @@ impl CompactNeedleMap {
|
||||
}
|
||||
|
||||
/// Visit all entries in ascending order by needle ID.
|
||||
pub fn ascending_visit<F>(&self, f: F) -> Result<(), String>
|
||||
pub fn ascending_visit<F, E>(&self, f: F) -> Result<(), E>
|
||||
where
|
||||
F: FnMut(NeedleId, &NeedleValue) -> Result<(), String>,
|
||||
F: FnMut(NeedleId, &NeedleValue) -> Result<(), E>,
|
||||
{
|
||||
self.map.ascending_visit(f)
|
||||
}
|
||||
@@ -465,9 +474,9 @@ impl RedbNeedleMap {
|
||||
/// loses at most the writes since the last checkpoint from redb, and
|
||||
/// the next load replays them from .idx.
|
||||
fn begin_write_no_fsync(db: &Database) -> io::Result<redb::WriteTransaction> {
|
||||
let mut txn = db.begin_write().map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb begin_write: {}", e))
|
||||
})?;
|
||||
let mut txn = db
|
||||
.begin_write()
|
||||
.map_err(|e| io::Error::other(format!("redb begin_write: {}", e)))?;
|
||||
let _ = txn.set_durability(Durability::None);
|
||||
Ok(txn)
|
||||
}
|
||||
@@ -501,7 +510,7 @@ impl RedbNeedleMap {
|
||||
pub fn checkpoint(&mut self, sync_idx: bool) -> io::Result<()> {
|
||||
let txn = self.begin_checkpoint(sync_idx)?;
|
||||
txn.commit()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redb commit: {}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("redb commit: {}", e)))?;
|
||||
self.writes_since_checkpoint = 0;
|
||||
Ok(())
|
||||
}
|
||||
@@ -516,17 +525,17 @@ impl RedbNeedleMap {
|
||||
if sync_idx {
|
||||
self.sync()?;
|
||||
}
|
||||
let mut txn = self.db_or_err()?.begin_write().map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb begin_write: {}", e))
|
||||
})?;
|
||||
let mut txn = self
|
||||
.db_or_err()?
|
||||
.begin_write()
|
||||
.map_err(|e| io::Error::other(format!("redb begin_write: {}", e)))?;
|
||||
txn.set_quick_repair(true);
|
||||
if self.idx_file.is_some() {
|
||||
let mut meta = txn.open_table(META_TABLE).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb open meta: {}", e))
|
||||
})?;
|
||||
meta.insert(META_IDX_SIZE, self.idx_file_offset).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb insert meta: {}", e))
|
||||
})?;
|
||||
let mut meta = txn
|
||||
.open_table(META_TABLE)
|
||||
.map_err(|e| io::Error::other(format!("redb open meta: {}", e)))?;
|
||||
meta.insert(META_IDX_SIZE, self.idx_file_offset)
|
||||
.map_err(|e| io::Error::other(format!("redb insert meta: {}", e)))?;
|
||||
}
|
||||
Ok(txn)
|
||||
}
|
||||
@@ -538,22 +547,20 @@ impl RedbNeedleMap {
|
||||
let db = Database::builder()
|
||||
.set_cache_size(cache_bytes)
|
||||
.create(db_path)
|
||||
.map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb create error: {}", e))
|
||||
})?;
|
||||
.map_err(|e| io::Error::other(format!("redb create error: {}", e)))?;
|
||||
|
||||
// Ensure tables exist
|
||||
let txn = Self::begin_write_no_fsync(&db)?;
|
||||
{
|
||||
let _table = txn.open_table(NEEDLE_TABLE).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb open_table: {}", e))
|
||||
})?;
|
||||
let _meta = txn.open_table(META_TABLE).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb open_table meta: {}", e))
|
||||
})?;
|
||||
let _table = txn
|
||||
.open_table(NEEDLE_TABLE)
|
||||
.map_err(|e| io::Error::other(format!("redb open_table: {}", e)))?;
|
||||
let _meta = txn
|
||||
.open_table(META_TABLE)
|
||||
.map_err(|e| io::Error::other(format!("redb open_table meta: {}", e)))?;
|
||||
}
|
||||
txn.commit()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redb commit: {}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("redb commit: {}", e)))?;
|
||||
|
||||
Ok(RedbNeedleMap {
|
||||
db: Some(db),
|
||||
@@ -572,16 +579,14 @@ impl RedbNeedleMap {
|
||||
fn save_idx_size_meta(&self, idx_size: u64) -> io::Result<()> {
|
||||
let txn = Self::begin_write_no_fsync(self.db_or_err()?)?;
|
||||
{
|
||||
let mut meta = txn.open_table(META_TABLE).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb open meta: {}", e))
|
||||
})?;
|
||||
meta.insert(META_IDX_SIZE, idx_size).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb insert meta: {}", e))
|
||||
})?;
|
||||
let mut meta = txn
|
||||
.open_table(META_TABLE)
|
||||
.map_err(|e| io::Error::other(format!("redb open meta: {}", e)))?;
|
||||
meta.insert(META_IDX_SIZE, idx_size)
|
||||
.map_err(|e| io::Error::other(format!("redb insert meta: {}", e)))?;
|
||||
}
|
||||
txn.commit().map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb commit meta: {}", e))
|
||||
})?;
|
||||
txn.commit()
|
||||
.map_err(|e| io::Error::other(format!("redb commit meta: {}", e)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -590,22 +595,26 @@ impl RedbNeedleMap {
|
||||
let txn = self
|
||||
.db_or_err()?
|
||||
.begin_read()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redb begin_read: {}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("redb begin_read: {}", e)))?;
|
||||
let meta = txn
|
||||
.open_table(META_TABLE)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redb open meta: {}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("redb open meta: {}", e)))?;
|
||||
// experimental-api-5 drops inherent ReadOnlyTable::get ('static guard).
|
||||
// ReadableTable::get guard borrows `meta`; bind the match so the
|
||||
// temporary Result is dropped before `meta`.
|
||||
let result = match meta.get(META_IDX_SIZE) {
|
||||
// ReadableTable::get guard borrows `meta`; edition 2024 drops the tail
|
||||
// expression's temporaries before `meta`, so no extra binding is needed.
|
||||
match meta.get(META_IDX_SIZE) {
|
||||
Ok(Some(guard)) => Ok(Some(guard.value())),
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("redb get meta: {}", e),
|
||||
)),
|
||||
};
|
||||
result
|
||||
Err(e) => Err(io::Error::other(format!("redb get meta: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test-only read of META `idx_size` through the live handle. See
|
||||
/// [`test_support::live_meta_idx_size`] for why durability tests use
|
||||
/// this instead of copying the open `.rdb`.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn live_meta_idx_size(&self) -> Option<u64> {
|
||||
self.read_idx_size_meta().unwrap()
|
||||
}
|
||||
|
||||
/// Load from an .idx file, reusing an existing .rdb if it is consistent.
|
||||
@@ -648,7 +657,7 @@ impl RedbNeedleMap {
|
||||
let db = Database::builder()
|
||||
.set_cache_size(cache_bytes)
|
||||
.open(db_path)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redb open: {}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("redb open: {}", e)))?;
|
||||
|
||||
let mut nm = RedbNeedleMap {
|
||||
db: Some(db),
|
||||
@@ -663,14 +672,11 @@ impl RedbNeedleMap {
|
||||
|
||||
let stored_idx_size = nm
|
||||
.read_idx_size_meta()?
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "no idx_size in redb meta"))?;
|
||||
.ok_or_else(|| io::Error::other("no idx_size in redb meta"))?;
|
||||
|
||||
if stored_idx_size > idx_size {
|
||||
// .idx shrank — corrupted or truncated, need full rebuild
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"idx file smaller than stored size",
|
||||
));
|
||||
return Err(io::Error::other("idx file smaller than stored size"));
|
||||
}
|
||||
|
||||
// Counters come from the whole .idx history, never from the table,
|
||||
@@ -683,40 +689,37 @@ impl RedbNeedleMap {
|
||||
let start_entry = stored_idx_size / NEEDLE_MAP_ENTRY_SIZE as u64;
|
||||
let txn = Self::begin_write_no_fsync(nm.db.as_ref().unwrap())?;
|
||||
{
|
||||
let mut table = txn.open_table(NEEDLE_TABLE).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb open_table: {}", e))
|
||||
})?;
|
||||
let mut table = txn
|
||||
.open_table(NEEDLE_TABLE)
|
||||
.map_err(|e| io::Error::other(format!("redb open_table: {}", e)))?;
|
||||
idx::walk_index_file(reader, start_entry, |key, offset, size| {
|
||||
let key_u64: u64 = key.into();
|
||||
if offset.is_zero() || size.is_deleted() {
|
||||
// Delete: store a tombstone (negative size, original
|
||||
// offset) over a live value; already deleted is a no-op.
|
||||
if let Ok(Some(old)) = nm.get_via_table(&table, key_u64) {
|
||||
if old.size.is_valid() {
|
||||
let deleted_nv = NeedleValue {
|
||||
offset: old.offset,
|
||||
size: Size(-(old.size.0)),
|
||||
};
|
||||
let packed = pack_needle_value(&deleted_nv);
|
||||
table.insert(key_u64, packed.as_slice()).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("redb insert: {}", e),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
if let Ok(Some(old)) = nm.get_via_table(&table, key_u64)
|
||||
&& old.size.is_valid()
|
||||
{
|
||||
let deleted_nv = NeedleValue {
|
||||
offset: old.offset,
|
||||
size: Size(-(old.size.0)),
|
||||
};
|
||||
let packed = pack_needle_value(&deleted_nv);
|
||||
table
|
||||
.insert(key_u64, packed.as_slice())
|
||||
.map_err(|e| io::Error::other(format!("redb insert: {}", e)))?;
|
||||
}
|
||||
} else {
|
||||
let packed = pack_needle_value(&NeedleValue { offset, size });
|
||||
table.insert(key_u64, packed.as_slice()).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb insert: {}", e))
|
||||
})?;
|
||||
table
|
||||
.insert(key_u64, packed.as_slice())
|
||||
.map_err(|e| io::Error::other(format!("redb insert: {}", e)))?;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
txn.commit()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redb commit: {}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("redb commit: {}", e)))?;
|
||||
|
||||
nm.save_idx_size_meta(idx_size)?;
|
||||
}
|
||||
@@ -734,10 +737,7 @@ impl RedbNeedleMap {
|
||||
match table.get(key_u64) {
|
||||
Ok(Some(guard)) => Ok(packed_to_needle_value(guard.value())),
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("redb get: {}", e),
|
||||
)),
|
||||
Err(e) => Err(io::Error::other(format!("redb get: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,13 +790,13 @@ impl RedbNeedleMap {
|
||||
|
||||
let txn = Self::begin_write_no_fsync(nm.db.as_ref().unwrap())?;
|
||||
{
|
||||
let mut table = txn.open_table(NEEDLE_TABLE).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb open_table: {}", e))
|
||||
})?;
|
||||
let mut table = txn
|
||||
.open_table(NEEDLE_TABLE)
|
||||
.map_err(|e| io::Error::other(format!("redb open_table: {}", e)))?;
|
||||
if !unlinked {
|
||||
table.retain(|_, _| false).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb retain: {}", e))
|
||||
})?;
|
||||
table
|
||||
.retain(|_, _| false)
|
||||
.map_err(|e| io::Error::other(format!("redb retain: {}", e)))?;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "redb-experimental-cursor"))]
|
||||
@@ -804,30 +804,33 @@ impl RedbNeedleMap {
|
||||
for (key, nv) in &entries {
|
||||
let key_u64: u64 = (*key).into();
|
||||
let packed = pack_needle_value(nv);
|
||||
table.insert(key_u64, packed.as_slice()).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb insert: {}", e))
|
||||
})?;
|
||||
table
|
||||
.insert(key_u64, packed.as_slice())
|
||||
.map_err(|e| io::Error::other(format!("redb insert: {}", e)))?;
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "redb-experimental-cursor")]
|
||||
{
|
||||
let mut cursor = table
|
||||
.upper_bound_mut(Bound::<u64>::Unbounded)
|
||||
.map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("redb upper_bound_mut: {}", e),
|
||||
)
|
||||
})?;
|
||||
let mut cursor =
|
||||
table
|
||||
.upper_bound_mut(Bound::<u64>::Unbounded)
|
||||
.map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("redb upper_bound_mut: {}", e),
|
||||
)
|
||||
})?;
|
||||
for (key, nv) in &entries {
|
||||
let key_u64: u64 = (*key).into();
|
||||
let packed = pack_needle_value(nv);
|
||||
cursor.insert_before(key_u64, packed.as_slice()).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("redb insert_before: {}", e),
|
||||
)
|
||||
})?;
|
||||
cursor
|
||||
.insert_before(key_u64, packed.as_slice())
|
||||
.map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("redb insert_before: {}", e),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
cursor.close().map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb cursor close: {}", e))
|
||||
@@ -835,7 +838,7 @@ impl RedbNeedleMap {
|
||||
}
|
||||
}
|
||||
txn.commit()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redb commit: {}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("redb commit: {}", e)))?;
|
||||
|
||||
nm.save_idx_size_meta(idx_size)?;
|
||||
Ok(())
|
||||
@@ -901,23 +904,16 @@ impl RedbNeedleMap {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.truncate_idx_to_offset();
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("redb open_table: {}", e),
|
||||
));
|
||||
return Err(io::Error::other(format!("redb open_table: {}", e)));
|
||||
}
|
||||
};
|
||||
let result = match table.insert(key_u64, packed.as_slice()) {
|
||||
match table.insert(key_u64, packed.as_slice()) {
|
||||
Ok(prev) => prev.and_then(|g| packed_to_needle_value(g.value())),
|
||||
Err(e) => {
|
||||
self.truncate_idx_to_offset();
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("redb insert: {}", e),
|
||||
));
|
||||
return Err(io::Error::other(format!("redb insert: {}", e)));
|
||||
}
|
||||
};
|
||||
result
|
||||
}
|
||||
};
|
||||
match txn.commit() {
|
||||
Ok(()) => old,
|
||||
@@ -925,8 +921,7 @@ impl RedbNeedleMap {
|
||||
// Transaction rolled back, database still usable:
|
||||
// truncate the orphan .idx row.
|
||||
self.truncate_idx_to_offset();
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
return Err(io::Error::other(
|
||||
"redb commit: Transaction was poisoned by a panic",
|
||||
));
|
||||
}
|
||||
@@ -935,12 +930,9 @@ impl RedbNeedleMap {
|
||||
// visible and redb refuses further writes. Keep
|
||||
// the .idx row (do NOT truncate) and reopen from
|
||||
// .idx to repair redb's internal state.
|
||||
let err = io::Error::new(io::ErrorKind::Other, format!("redb commit: {}", e));
|
||||
let err = io::Error::other(format!("redb commit: {}", e));
|
||||
if let Err(reopen_err) = self.reopen_from_idx() {
|
||||
tracing::warn!(
|
||||
"redb reopen after put commit error failed: {}",
|
||||
reopen_err
|
||||
);
|
||||
tracing::warn!("redb reopen after put commit error failed: {}", reopen_err);
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
@@ -968,39 +960,32 @@ impl RedbNeedleMap {
|
||||
let txn = self
|
||||
.db_or_err()?
|
||||
.begin_read()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redb begin_read: {}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("redb begin_read: {}", e)))?;
|
||||
let table = txn
|
||||
.open_table(NEEDLE_TABLE)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redb open_table: {}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("redb open_table: {}", e)))?;
|
||||
// experimental-api-5 drops inherent ReadOnlyTable::get ('static guard).
|
||||
// ReadableTable::get guard borrows `table`; bind the match so the
|
||||
// temporary Result is dropped before `table`.
|
||||
let result = match table.get(key_u64) {
|
||||
// ReadableTable::get guard borrows `table`; edition 2024 drops the tail
|
||||
// expression's temporaries before `table`, so no extra binding is needed.
|
||||
match table.get(key_u64) {
|
||||
Ok(Some(guard)) => Ok(packed_to_needle_value(guard.value())),
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("redb get: {}", e),
|
||||
)),
|
||||
};
|
||||
result
|
||||
Err(e) => Err(io::Error::other(format!("redb get: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a needle as deleted. Appends tombstone to .idx file, negates size in redb.
|
||||
pub fn delete(&mut self, key: NeedleId, offset: Offset) -> io::Result<Option<Size>> {
|
||||
let key_u64: u64 = key.into();
|
||||
let txn = Self::begin_write_no_fsync(self.db_or_err()?)?;
|
||||
let mut table = txn.open_table(NEEDLE_TABLE).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb open_table: {}", e))
|
||||
})?;
|
||||
let mut table = txn
|
||||
.open_table(NEEDLE_TABLE)
|
||||
.map_err(|e| io::Error::other(format!("redb open_table: {}", e)))?;
|
||||
let old = match table.get(key_u64) {
|
||||
Ok(Some(guard)) => packed_to_needle_value(guard.value()),
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("redb get: {}", e),
|
||||
));
|
||||
return Err(io::Error::other(format!("redb get: {}", e)));
|
||||
}
|
||||
};
|
||||
let Some(old) = old.filter(|nv| nv.size.is_valid()) else {
|
||||
@@ -1021,10 +1006,7 @@ impl RedbNeedleMap {
|
||||
drop(table);
|
||||
if let Err(e) = insert_res {
|
||||
self.truncate_idx_to_offset();
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("redb insert: {}", e),
|
||||
));
|
||||
return Err(io::Error::other(format!("redb insert: {}", e)));
|
||||
}
|
||||
match txn.commit() {
|
||||
Ok(()) => {}
|
||||
@@ -1032,8 +1014,7 @@ impl RedbNeedleMap {
|
||||
// Transaction rolled back, database still usable:
|
||||
// truncate the orphan .idx row.
|
||||
self.truncate_idx_to_offset();
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
return Err(io::Error::other(
|
||||
"redb commit: Transaction was poisoned by a panic",
|
||||
));
|
||||
}
|
||||
@@ -1042,7 +1023,7 @@ impl RedbNeedleMap {
|
||||
// and redb refuses further writes. Keep the .idx row
|
||||
// (do NOT truncate) and reopen from .idx to repair
|
||||
// redb's internal state.
|
||||
let err = io::Error::new(io::ErrorKind::Other, format!("redb commit: {}", e));
|
||||
let err = io::Error::other(format!("redb commit: {}", e));
|
||||
if let Err(reopen_err) = self.reopen_from_idx() {
|
||||
tracing::warn!(
|
||||
"redb reopen after delete commit error failed: {}",
|
||||
@@ -1105,10 +1086,10 @@ impl RedbNeedleMap {
|
||||
/// after the orphan, `idx_file_offset` advances past it, and a later
|
||||
/// checkpoint records an offset that makes the reload skip the orphan.
|
||||
fn truncate_idx_to_offset(&mut self) {
|
||||
if let Some(ref mut idx_file) = self.idx_file {
|
||||
if let Err(e) = idx_file.truncate_to(self.idx_file_offset) {
|
||||
tracing::warn!("failed to truncate orphan .idx row: {}", e);
|
||||
}
|
||||
if let Some(ref mut idx_file) = self.idx_file
|
||||
&& let Err(e) = idx_file.truncate_to(self.idx_file_offset)
|
||||
{
|
||||
tracing::warn!("failed to truncate orphan .idx row: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1146,18 +1127,12 @@ impl RedbNeedleMap {
|
||||
let read_file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.open(&idx_path)
|
||||
.map_err(|e| {
|
||||
io::Error::other(format!("reopen: open .idx {}: {}", idx_path, e))
|
||||
})?;
|
||||
.map_err(|e| io::Error::other(format!("reopen: open .idx {}: {}", idx_path, e)))?;
|
||||
let actual_idx_size = read_file.metadata()?.len();
|
||||
let mut reader = io::BufReader::new(read_file);
|
||||
|
||||
let reopened = Self::load_from_idx(
|
||||
&self.rdb_path,
|
||||
&mut reader,
|
||||
self.version,
|
||||
self.cache_bytes,
|
||||
)?;
|
||||
let reopened =
|
||||
Self::load_from_idx(&self.rdb_path, &mut reader, self.version, self.cache_bytes)?;
|
||||
|
||||
// Preserve the append writer and the paths/version/cache; adopt the
|
||||
// repaired database, metrics, and idx_file_offset from the reload.
|
||||
@@ -1198,10 +1173,10 @@ impl RedbNeedleMap {
|
||||
let txn = self
|
||||
.db_or_err()?
|
||||
.begin_read()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redb begin_read: {}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("redb begin_read: {}", e)))?;
|
||||
let table = txn
|
||||
.open_table(NEEDLE_TABLE)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redb open_table: {}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("redb open_table: {}", e)))?;
|
||||
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
@@ -1212,18 +1187,17 @@ impl RedbNeedleMap {
|
||||
// redb iterates in key order (u64 ascending)
|
||||
let iter = table
|
||||
.iter()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redb iter: {}", e)))?;
|
||||
.map_err(|e| io::Error::other(format!("redb iter: {}", e)))?;
|
||||
|
||||
for entry in iter {
|
||||
let (key_guard, val_guard) = entry.map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("redb iter next: {}", e))
|
||||
})?;
|
||||
let (key_guard, val_guard) =
|
||||
entry.map_err(|e| io::Error::other(format!("redb iter next: {}", e)))?;
|
||||
let key_u64: u64 = key_guard.value();
|
||||
let bytes: &[u8] = val_guard.value();
|
||||
if let Some(nv) = packed_to_needle_value(bytes) {
|
||||
if nv.size.is_valid() {
|
||||
idx::write_index_entry(&mut file, NeedleId(key_u64), nv.offset, nv.size)?;
|
||||
}
|
||||
if let Some(nv) = packed_to_needle_value(bytes)
|
||||
&& nv.size.is_valid()
|
||||
{
|
||||
idx::write_index_entry(&mut file, NeedleId(key_u64), nv.offset, nv.size)?;
|
||||
}
|
||||
}
|
||||
file.sync_all()?;
|
||||
@@ -1231,9 +1205,10 @@ impl RedbNeedleMap {
|
||||
}
|
||||
|
||||
/// Visit all entries in ascending order by needle ID.
|
||||
pub fn ascending_visit<F>(&self, mut f: F) -> Result<(), String>
|
||||
pub fn ascending_visit<F, E>(&self, mut f: F) -> Result<(), E>
|
||||
where
|
||||
F: FnMut(NeedleId, &NeedleValue) -> Result<(), String>,
|
||||
F: FnMut(NeedleId, &NeedleValue) -> Result<(), E>,
|
||||
E: From<String>,
|
||||
{
|
||||
let txn = self
|
||||
.db_or_err()
|
||||
@@ -1413,6 +1388,18 @@ impl NeedleMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Skew the live file count away from what the `.idx` holds, so tests
|
||||
/// can build a volume whose reported count disagrees with a reload.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn add_file_count_for_test(&self, delta: i64) {
|
||||
let metric = match self {
|
||||
NeedleMap::InMemory(nm) => &nm.metric,
|
||||
NeedleMap::Redb(nm) => &nm.metric,
|
||||
NeedleMap::SortedFile(_) => panic!("sorted-file needle maps are read-only"),
|
||||
};
|
||||
metric.file_count.fetch_add(delta, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Largest (offset + actual size) seen during the load walk; 0 if the
|
||||
/// map is empty. Used at volume load to detect .idx entries that
|
||||
/// reference past the end of .dat (issue #8928) without a second scan.
|
||||
@@ -1471,9 +1458,10 @@ impl NeedleMap {
|
||||
}
|
||||
|
||||
/// Visit all entries in ascending order by needle ID.
|
||||
pub fn ascending_visit<F>(&self, f: F) -> Result<(), String>
|
||||
pub fn ascending_visit<F, E>(&self, f: F) -> Result<(), E>
|
||||
where
|
||||
F: FnMut(NeedleId, &NeedleValue) -> Result<(), String>,
|
||||
F: FnMut(NeedleId, &NeedleValue) -> Result<(), E>,
|
||||
E: From<String>,
|
||||
{
|
||||
match self {
|
||||
NeedleMap::InMemory(nm) => nm.ascending_visit(f),
|
||||
@@ -1494,7 +1482,7 @@ impl NeedleMap {
|
||||
// The visitor never fails, so neither can this.
|
||||
let _ = nm.ascending_visit(|id, nv| {
|
||||
entries.push((id, *nv));
|
||||
Ok(())
|
||||
Ok::<(), std::convert::Infallible>(())
|
||||
});
|
||||
Ok(entries)
|
||||
}
|
||||
@@ -1512,22 +1500,30 @@ impl NeedleMap {
|
||||
pub(crate) mod test_support {
|
||||
use super::*;
|
||||
|
||||
/// The `.idx` size recorded in the durable state of the `.rdb` at
|
||||
/// `rdb_path`, read from a copy taken while the map may still be open:
|
||||
/// exactly what a crash would leave behind. `None` when nothing durable
|
||||
/// has been recorded yet.
|
||||
pub(crate) fn durable_idx_size(rdb_path: &Path) -> Option<u64> {
|
||||
let copy = rdb_path.with_extension("crash-copy.rdb");
|
||||
std::fs::copy(rdb_path, ©).unwrap();
|
||||
let db = Database::open(©).unwrap();
|
||||
let txn = db.begin_read().unwrap();
|
||||
let meta = txn.open_table(META_TABLE).ok()?;
|
||||
let size = meta.get(META_IDX_SIZE).unwrap().map(|g| g.value());
|
||||
drop(meta);
|
||||
drop(txn);
|
||||
drop(db);
|
||||
let _ = std::fs::remove_file(©);
|
||||
size
|
||||
/// The `.idx` size in the map's META table, read through the live
|
||||
/// handle.
|
||||
///
|
||||
/// The load path records the `.idx` size with `Durability::None`, and
|
||||
/// every `put`/`delete` also commits non-durably, so before the first
|
||||
/// checkpoint this is the load-time value (`Some(0)` for a fresh map) —
|
||||
/// NOT the crash-durable `None` a copy of the open `.rdb` would show.
|
||||
/// A live read is the only portable observation: redb 4.2.0 takes an
|
||||
/// exclusive whole-file lock, which is advisory on Unix but mandatory
|
||||
/// on Windows, so copying the open `.rdb` fails there with OS error 33.
|
||||
///
|
||||
/// It still pins the property under test: the only *durable* META
|
||||
/// writer is `checkpoint`, so any value other than the load-time one
|
||||
/// proves a checkpoint recorded progress — and the post-checkpoint
|
||||
/// value equals the durable one, because checkpoints commit with
|
||||
/// `Durability::Immediate`. What is lost vs the old copy: strict crash
|
||||
/// fidelity — a hard crash pre-checkpoint would leave META absent
|
||||
/// rather than `Some(0)` (loader-equivalent outcomes: full rebuild vs
|
||||
/// replay-from-0, both correct). A clean close+reopen cannot recover
|
||||
/// that distinction either: dropping the `Database` flushes pending
|
||||
/// non-durable commits, so a reopened handle reads `Some(0)` just like
|
||||
/// the live one.
|
||||
pub(crate) fn live_meta_idx_size(nm: &RedbNeedleMap) -> Option<u64> {
|
||||
nm.live_meta_idx_size()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1682,6 +1678,7 @@ mod tests {
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(&idx_path)
|
||||
.unwrap();
|
||||
let idx_size = idx_file.metadata().unwrap().len();
|
||||
@@ -1857,7 +1854,7 @@ mod tests {
|
||||
let mut live = 0u64;
|
||||
nm.ascending_visit(|_, _| {
|
||||
live += 1;
|
||||
Ok(())
|
||||
Ok::<(), String>(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(live, N - 1);
|
||||
@@ -2070,7 +2067,7 @@ mod tests {
|
||||
let mut visited = Vec::new();
|
||||
nm.ascending_visit(|id, nv| {
|
||||
visited.push((id, nv.size));
|
||||
Ok(())
|
||||
Ok::<(), String>(())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -2168,8 +2165,14 @@ mod tests {
|
||||
// server opens one redb database per volume, so the process-wide
|
||||
// ceiling is roughly (volumes x budget).
|
||||
assert_eq!(NeedleMapKind::Redb.redb_cache_bytes(), 4 * 1024 * 1024);
|
||||
assert_eq!(NeedleMapKind::RedbMedium.redb_cache_bytes(), 8 * 1024 * 1024);
|
||||
assert_eq!(NeedleMapKind::RedbLarge.redb_cache_bytes(), 16 * 1024 * 1024);
|
||||
assert_eq!(
|
||||
NeedleMapKind::RedbMedium.redb_cache_bytes(),
|
||||
8 * 1024 * 1024
|
||||
);
|
||||
assert_eq!(
|
||||
NeedleMapKind::RedbLarge.redb_cache_bytes(),
|
||||
16 * 1024 * 1024
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2198,7 +2201,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_redb_checkpoint_is_explicit_and_due_every_interval() {
|
||||
use test_support::durable_idx_size;
|
||||
use test_support::live_meta_idx_size;
|
||||
|
||||
// Every non-durable redb commit leaves bookkeeping behind until a
|
||||
// durable one clears it, so a writable map asks for a checkpoint on
|
||||
@@ -2208,8 +2211,12 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let (mut nm, db_path, _idx_path) = open_writable_redb(dir.path());
|
||||
for i in 1..EXPECTED_INTERVAL {
|
||||
nm.put(NeedleId(i), Offset::from_actual_offset((i * 8) as i64), Size(1))
|
||||
.unwrap();
|
||||
nm.put(
|
||||
NeedleId(i),
|
||||
Offset::from_actual_offset((i * 8) as i64),
|
||||
Size(1),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!nm.checkpoint_due(), "due after only {i} writes");
|
||||
}
|
||||
nm.put(
|
||||
@@ -2219,20 +2226,29 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
assert!(nm.checkpoint_due());
|
||||
assert_eq!(durable_idx_size(&db_path), None, "put() must not commit durably");
|
||||
// No checkpoint taken yet: META still holds the load-time .idx size.
|
||||
// put() only commits non-durably, so the live value is unchanged.
|
||||
assert_eq!(
|
||||
live_meta_idx_size(&nm),
|
||||
Some(0),
|
||||
"put() must not record checkpoint progress"
|
||||
);
|
||||
|
||||
nm.checkpoint(true).unwrap();
|
||||
assert!(!nm.checkpoint_due());
|
||||
assert_eq!(
|
||||
durable_idx_size(&db_path),
|
||||
live_meta_idx_size(&nm),
|
||||
Some(EXPECTED_INTERVAL * NEEDLE_MAP_ENTRY_SIZE as u64),
|
||||
"checkpoint records how much of the .idx the table reflects"
|
||||
);
|
||||
|
||||
// Snapshot the .rdb while the map is still open: what a crash leaves.
|
||||
// Everything is durable after the checkpoint, so the map is closed
|
||||
// first and the snapshot sees the same bytes on every platform.
|
||||
// (Copying while open fails on Windows, where redb's file lock is
|
||||
// mandatory: what a crash leaves.)
|
||||
drop(nm);
|
||||
let crash_copy = dir.path().join("crash.rdb");
|
||||
std::fs::copy(&db_path, &crash_copy).unwrap();
|
||||
drop(nm);
|
||||
let db = Database::open(&crash_copy).unwrap();
|
||||
let txn = db.begin_read().unwrap();
|
||||
let table = txn.open_table(NEEDLE_TABLE).unwrap();
|
||||
@@ -2247,8 +2263,12 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let (mut nm, db_path, idx_path) = open_writable_redb(dir.path());
|
||||
for i in 1..=5u64 {
|
||||
nm.put(NeedleId(i), Offset::from_actual_offset((i * 8) as i64), Size(1))
|
||||
.unwrap();
|
||||
nm.put(
|
||||
NeedleId(i),
|
||||
Offset::from_actual_offset((i * 8) as i64),
|
||||
Size(1),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
nm.close();
|
||||
drop(nm);
|
||||
@@ -2272,8 +2292,12 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let (mut nm, db_path, idx_path) = open_writable_redb(dir.path());
|
||||
for i in 1..=5u64 {
|
||||
nm.put(NeedleId(i), Offset::from_actual_offset((i * 8) as i64), Size(1))
|
||||
.unwrap();
|
||||
nm.put(
|
||||
NeedleId(i),
|
||||
Offset::from_actual_offset((i * 8) as i64),
|
||||
Size(1),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
// Drop without close(): redb makes the table durable on drop, but the
|
||||
// recorded .idx size stays at its load-time value (0), so the reload
|
||||
@@ -2344,12 +2368,14 @@ mod tests {
|
||||
reloaded.deleted_count(),
|
||||
reloaded.deleted_size(),
|
||||
);
|
||||
assert_eq!(
|
||||
after, live,
|
||||
"close_first={close_first} rebuild={rebuild}"
|
||||
);
|
||||
assert_eq!(after, live, "close_first={close_first} rebuild={rebuild}");
|
||||
assert_eq!(reloaded.get(NeedleId(1)).unwrap().unwrap().size, Size(200));
|
||||
assert!(reloaded.get(NeedleId(2)).unwrap().map_or(true, |v| v.size.is_deleted()));
|
||||
assert!(
|
||||
reloaded
|
||||
.get(NeedleId(2))
|
||||
.unwrap()
|
||||
.is_none_or(|v| v.size.is_deleted())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ struct CompactEntry {
|
||||
}
|
||||
|
||||
impl CompactEntry {
|
||||
fn to_needle_value(&self) -> NeedleValue {
|
||||
fn to_needle_value(self) -> NeedleValue {
|
||||
NeedleValue {
|
||||
offset: Offset::from_bytes(&self.offset),
|
||||
size: self.size,
|
||||
|
||||
@@ -205,6 +205,18 @@ mod tests {
|
||||
use std::os::unix::fs::FileExt;
|
||||
borrowed.read_exact_at(&mut buf, 0).unwrap();
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::fs::FileExt;
|
||||
let mut filled = 0;
|
||||
let mut at = 0;
|
||||
while filled < buf.len() {
|
||||
let n = borrowed.seek_read(&mut buf[filled..], at).unwrap();
|
||||
assert!(n != 0, "unexpected EOF in seek_read");
|
||||
filled += n;
|
||||
at += n as u64;
|
||||
}
|
||||
}
|
||||
assert_eq!(&buf, b"first");
|
||||
}
|
||||
|
||||
|
||||
@@ -132,9 +132,7 @@ mod tests {
|
||||
let mut seen = SeenKeys::new(10_000, FALSE_POSITIVE_RATE);
|
||||
// Fresh keys may occasionally collide (that is the false-positive
|
||||
// rate), but only rarely.
|
||||
let fresh_reported_seen = (0..10_000u64)
|
||||
.filter(|&key| seen.test_and_add(key))
|
||||
.count();
|
||||
let fresh_reported_seen = (0..10_000u64).filter(|&key| seen.test_and_add(key)).count();
|
||||
assert!(
|
||||
fresh_reported_seen < 50,
|
||||
"fresh keys reported seen: {fresh_reported_seen}"
|
||||
|
||||
@@ -18,6 +18,7 @@ use std::sync::{Mutex, RwLock};
|
||||
|
||||
use super::file_pool::pooled_index_files;
|
||||
use crate::storage::idx;
|
||||
use crate::storage::io::read_exact_at;
|
||||
use crate::storage::needle_map::{CompactNeedleMap, NeedleMapMetric, NeedleValue};
|
||||
use crate::storage::types::*;
|
||||
|
||||
@@ -133,9 +134,7 @@ impl SortedFileNeedleMap {
|
||||
}
|
||||
let file = pooled_index_files()
|
||||
.borrow(&self.db_file_name, false)
|
||||
.map_err(|e| {
|
||||
io::Error::new(e.kind(), format!("open {}: {}", self.db_file_name, e))
|
||||
})?;
|
||||
.map_err(|e| io::Error::new(e.kind(), format!("open {}: {}", self.db_file_name, e)))?;
|
||||
match search_sorted_index(&file, self.db_file_size, key)? {
|
||||
Some((_, offset, size)) => Ok(Some(NeedleValue { offset, size })),
|
||||
None => Ok(None),
|
||||
@@ -226,10 +225,7 @@ impl SortedFileNeedleMap {
|
||||
.fail_sdx_mark
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
{
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"injected .sdx mark failure",
|
||||
));
|
||||
return Err(io::Error::other("injected .sdx mark failure"));
|
||||
}
|
||||
let mut buf = [0u8; SIZE_SIZE];
|
||||
TOMBSTONE_FILE_SIZE.to_bytes(&mut buf);
|
||||
@@ -309,7 +305,7 @@ impl SortedFileNeedleMap {
|
||||
let rows = rows_per_read.min(entry_count - done) as usize;
|
||||
let bytes = &mut block[..rows * NEEDLE_MAP_ENTRY_SIZE];
|
||||
read_exact_at(&file, bytes, done * NEEDLE_MAP_ENTRY_SIZE as u64)?;
|
||||
for entry in bytes.chunks_exact(NEEDLE_MAP_ENTRY_SIZE) {
|
||||
for entry in bytes.as_chunks::<NEEDLE_MAP_ENTRY_SIZE>().0 {
|
||||
let (key, offset, size) = idx_entry_from_bytes(entry);
|
||||
if !size.is_valid() || pending.contains_key(&key) {
|
||||
continue; // deleted in place, or still awaiting that mark
|
||||
@@ -321,9 +317,11 @@ impl SortedFileNeedleMap {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ascending_visit<F>(&self, mut f: F) -> Result<(), String>
|
||||
/// Visit all live entries in ascending order by needle ID.
|
||||
pub fn ascending_visit<F, E>(&self, mut f: F) -> Result<(), E>
|
||||
where
|
||||
F: FnMut(NeedleId, &NeedleValue) -> Result<(), String>,
|
||||
F: FnMut(NeedleId, &NeedleValue) -> Result<(), E>,
|
||||
E: From<String>,
|
||||
{
|
||||
let mut visit_error = None;
|
||||
self.visit_live_entries(|id, nv| {
|
||||
@@ -333,7 +331,7 @@ impl SortedFileNeedleMap {
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| visit_error.take().unwrap_or_else(|| e.to_string()))
|
||||
.map_err(|e| visit_error.take().unwrap_or_else(|| E::from(e.to_string())))
|
||||
}
|
||||
|
||||
pub fn iter_entries(&self) -> io::Result<Vec<(NeedleId, NeedleValue)>> {
|
||||
@@ -525,32 +523,6 @@ fn search_sorted_index(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn read_exact_at(file: &File, buf: &mut [u8], offset: u64) -> io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::FileExt;
|
||||
file.read_exact_at(buf, offset)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::fs::FileExt;
|
||||
let mut filled = 0;
|
||||
let mut at = offset;
|
||||
while filled < buf.len() {
|
||||
let n = file.seek_read(&mut buf[filled..], at)?;
|
||||
if n == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"unexpected EOF in seek_read",
|
||||
));
|
||||
}
|
||||
filled += n;
|
||||
at += n as u64;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn write_at(file: &File, buf: &[u8], offset: u64) -> io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -703,10 +675,11 @@ mod tests {
|
||||
// without a reload — the same contract Go's Get has, where callers
|
||||
// check size.is_deleted().
|
||||
assert!(m.get(NeedleId(2)).unwrap().unwrap().size.is_deleted());
|
||||
assert!(m
|
||||
.delete(NeedleId(2), Offset::from_actual_offset(16))
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(
|
||||
m.delete(NeedleId(2), Offset::from_actual_offset(16))
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert!(!m.get(NeedleId(1)).unwrap().unwrap().size.is_deleted());
|
||||
}
|
||||
|
||||
@@ -1002,7 +975,8 @@ mod tests {
|
||||
|
||||
// The retry is a no-op: no second tombstone, no double counting.
|
||||
assert_eq!(
|
||||
m.delete(NeedleId(1), Offset::from_actual_offset(8)).unwrap(),
|
||||
m.delete(NeedleId(1), Offset::from_actual_offset(8))
|
||||
.unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(m.deleted_count(), deleted_before + 2);
|
||||
@@ -1032,7 +1006,8 @@ mod tests {
|
||||
);
|
||||
// And a retry must not append a second tombstone for it.
|
||||
assert_eq!(
|
||||
m.delete(NeedleId(1), Offset::from_actual_offset(8)).unwrap(),
|
||||
m.delete(NeedleId(1), Offset::from_actual_offset(8))
|
||||
.unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -1062,7 +1037,7 @@ mod tests {
|
||||
let mut visited = Vec::new();
|
||||
m.ascending_visit(|id, _| {
|
||||
visited.push(id);
|
||||
Ok(())
|
||||
Ok::<(), String>(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(visited, vec![NeedleId(2)]);
|
||||
|
||||
+897
-367
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ use std::path::Path;
|
||||
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::storage::disk_location::{parse_collection_volume_id_pub, DiskLocation};
|
||||
use crate::storage::disk_location::{DiskLocation, parse_collection_volume_id_pub};
|
||||
use crate::storage::store::Store;
|
||||
use crate::storage::types::VolumeId;
|
||||
|
||||
@@ -131,6 +131,12 @@ impl Store {
|
||||
let Some(base) = name.strip_suffix(".ecx") else {
|
||||
continue;
|
||||
};
|
||||
// A 0-byte .ecx is a corrupt stub from a failed copy, not a
|
||||
// credible owner — skip it so the scan keeps looking for a
|
||||
// real index on a sibling disk (Go's indexEcxOwners).
|
||||
if !ent.metadata().is_ok_and(|m| m.len() > 0) {
|
||||
continue;
|
||||
}
|
||||
let Some((collection, vid)) = parse_collection_volume_id_pub(base) else {
|
||||
continue;
|
||||
};
|
||||
@@ -286,9 +292,7 @@ fn collect_shard_disk_volumes(loc: &DiskLocation) -> HashMap<EcKey, Vec<String>>
|
||||
let Some((collection, vid)) = parse_collection_volume_id_pub(base) else {
|
||||
continue;
|
||||
};
|
||||
out.entry(EcKey { collection, vid })
|
||||
.or_default()
|
||||
.push(name);
|
||||
out.entry(EcKey { collection, vid }).or_default().push(name);
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -419,4 +423,33 @@ mod tests {
|
||||
let post = fs::read(dir0.join(format!("{}_{}.ecx", collection, vid))).unwrap();
|
||||
assert_eq!(post, ecx_local, "mirror overwrote dir0's existing .ecx");
|
||||
}
|
||||
|
||||
/// The mirror shares Go's indexEcxOwners, which skips a 0-byte `.ecx`:
|
||||
/// a stub must not be chosen as the source to mirror from.
|
||||
#[test]
|
||||
fn mirror_owner_index_skips_zero_byte_ecx() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let dir0 = tmp.path().join("data0");
|
||||
let dir1 = tmp.path().join("data1");
|
||||
fs::create_dir_all(&dir0).unwrap();
|
||||
fs::create_dir_all(&dir1).unwrap();
|
||||
|
||||
let collection = "video-recordings";
|
||||
let vid = 4123u32;
|
||||
plant_ecx(&dir0, collection, vid, b"");
|
||||
plant_ecx(&dir1, collection, vid, &[0xA1u8; 20]);
|
||||
|
||||
let mut store = Store::new(NeedleMapKind::InMemory);
|
||||
add_loc(&mut store, &dir0);
|
||||
add_loc(&mut store, &dir1);
|
||||
|
||||
let owners = store.index_ecx_owners_for_mirror();
|
||||
let owner = owners
|
||||
.get(&EcKey {
|
||||
collection: collection.to_string(),
|
||||
vid: VolumeId(vid),
|
||||
})
|
||||
.expect("the valid .ecx on disk 1 must be indexed");
|
||||
assert_eq!(owner.location, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ use std::fs;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::storage::disk_location::{is_ec_shard_extension, parse_collection_volume_id_pub};
|
||||
use crate::storage::erasure_coding::ec_shard::DATA_SHARDS_COUNT;
|
||||
use crate::storage::erasure_coding::ec_shard::{DATA_SHARDS_COUNT, ShardId};
|
||||
use crate::storage::store::Store;
|
||||
use crate::storage::types::VolumeId;
|
||||
|
||||
@@ -37,6 +37,7 @@ pub(crate) struct EcVolumeMissingIndex {
|
||||
pub data_dir: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn ec_local_ecx_path(dir: &str, collection: &str, vid: VolumeId) -> String {
|
||||
if collection.is_empty() {
|
||||
format!("{}/{}.ecx", dir, vid.0)
|
||||
@@ -80,6 +81,11 @@ struct EcxOwnerInfo {
|
||||
idx_dir: String,
|
||||
}
|
||||
|
||||
/// One unit of reconcile work: the disk holding orphan shards, the volume
|
||||
/// they belong to, the shard files, the `.ecx` owner, and whether the
|
||||
/// mirror already installed sidecars locally (`use_local_idx`).
|
||||
type OrphanShardLoad = (usize, EcKey, Vec<(String, ShardId)>, EcxOwnerInfo, bool);
|
||||
|
||||
impl Store {
|
||||
/// Run cross-disk orphan-shard reconciliation. Should be called
|
||||
/// after every DiskLocation has finished its per-disk EC scan.
|
||||
@@ -98,7 +104,7 @@ impl Store {
|
||||
// `use_local_idx` is the post-mirror fast path: when the
|
||||
// mirror already installed sidecars locally, mount against
|
||||
// loc.idx_directory instead of the owner disk.
|
||||
let mut to_load: Vec<(usize, EcKey, Vec<(String, u32)>, EcxOwnerInfo, bool)> = Vec::new();
|
||||
let mut to_load: Vec<OrphanShardLoad> = Vec::new();
|
||||
for (loc_idx, loc) in self.locations.iter().enumerate() {
|
||||
let orphans = collect_orphan_ec_shards(loc, loc_idx);
|
||||
for (key, shards) in orphans {
|
||||
@@ -112,14 +118,11 @@ impl Store {
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let local_ecx = ec_local_ecx_path(&loc.idx_directory, &key.collection, key.vid);
|
||||
let local_ecx_in_data = ec_local_ecx_path(&loc.directory, &key.collection, key.vid);
|
||||
let use_local_idx = std::path::Path::new(&local_ecx).exists()
|
||||
|| std::path::Path::new(&local_ecx_in_data).exists();
|
||||
// A 0-byte local stub is not a mirrored index (Go gates this fast
|
||||
// path on HasEcxFileOnDisk); mount against the owner instead.
|
||||
let use_local_idx = loc.has_ecx_file_on_disk(&key.collection, key.vid);
|
||||
|
||||
if !use_local_idx
|
||||
&& owner.location == loc_idx
|
||||
&& owner.idx_dir == loc.idx_directory
|
||||
if !use_local_idx && owner.location == loc_idx && owner.idx_dir == loc.idx_directory
|
||||
{
|
||||
// Same-disk no-op: load_all_ec_shards already
|
||||
// tried and logged the failure.
|
||||
@@ -132,7 +135,7 @@ impl Store {
|
||||
for (loc_idx, key, shards, owner, use_local_idx) in to_load {
|
||||
let shard_names: Vec<&str> = shards.iter().map(|(n, _)| n.as_str()).collect();
|
||||
let loc_dir = self.locations[loc_idx].directory.clone();
|
||||
let shard_ids: Vec<u32> = shards.iter().map(|(_, sid)| *sid).collect();
|
||||
let shard_ids: Vec<ShardId> = shards.iter().map(|(_, sid)| *sid).collect();
|
||||
|
||||
if use_local_idx {
|
||||
info!(
|
||||
@@ -293,10 +296,10 @@ impl Store {
|
||||
// may be sole copies of a distributed volume.
|
||||
let mut node_wide_bits = ev.shard_bits().0;
|
||||
for other in &self.locations {
|
||||
if let Some(other_ev) = other.find_ec_volume(*vid) {
|
||||
if other_ev.collection == ev.collection {
|
||||
node_wide_bits |= other_ev.shard_bits().0;
|
||||
}
|
||||
if let Some(other_ev) = other.find_ec_volume(*vid)
|
||||
&& other_ev.collection == ev.collection
|
||||
{
|
||||
node_wide_bits |= other_ev.shard_bits().0;
|
||||
}
|
||||
}
|
||||
let node_wide = node_wide_bits.count_ones() as usize;
|
||||
@@ -419,6 +422,12 @@ impl Store {
|
||||
let Some(base) = name.strip_suffix(".ecx") else {
|
||||
continue;
|
||||
};
|
||||
// A 0-byte .ecx is a corrupt stub from a failed copy, not a
|
||||
// credible owner — skip it so the scan keeps looking for a
|
||||
// real index on a sibling disk (Go's indexEcxOwners).
|
||||
if !ent.metadata().is_ok_and(|m| m.len() > 0) {
|
||||
continue;
|
||||
}
|
||||
let Some((collection, vid)) = parse_collection_volume_id_pub(base) else {
|
||||
continue;
|
||||
};
|
||||
@@ -474,13 +483,13 @@ impl Store {
|
||||
/// Unlike `reconcile_ec_shards_across_disks` it needs no sibling disk, so a
|
||||
/// single-disk store recovers once its index has been fetched from a peer.
|
||||
fn load_orphan_ec_shards_with_local_index(&mut self) {
|
||||
let mut work: Vec<(usize, EcKey, Vec<u32>)> = Vec::new();
|
||||
let mut work: Vec<(usize, EcKey, Vec<ShardId>)> = Vec::new();
|
||||
for (loc_idx, loc) in self.locations.iter().enumerate() {
|
||||
for (key, shards) in collect_orphan_ec_shards(loc, loc_idx) {
|
||||
if !loc.has_ecx_file_on_disk(&key.collection, key.vid) {
|
||||
continue;
|
||||
}
|
||||
let ids: Vec<u32> = shards.iter().map(|(_, sid)| *sid).collect();
|
||||
let ids: Vec<ShardId> = shards.iter().map(|(_, sid)| *sid).collect();
|
||||
work.push((loc_idx, key, ids));
|
||||
}
|
||||
}
|
||||
@@ -499,6 +508,53 @@ impl Store {
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk a disk's data directory and return the `.ec??` shard files
|
||||
/// that are present on disk but not yet registered in the location's
|
||||
/// `ec_volumes` map. Keyed by (collection, vid) so callers can match
|
||||
/// each group against its `.ecx`-owning disk in one lookup. Zero-byte
|
||||
/// shard files are ignored — same shape as `load_all_ec_shards`.
|
||||
fn collect_orphan_ec_shards(
|
||||
loc: &crate::storage::disk_location::DiskLocation,
|
||||
_loc_idx: usize,
|
||||
) -> HashMap<EcKey, Vec<(String, ShardId)>> {
|
||||
let mut orphans: HashMap<EcKey, Vec<(String, ShardId)>> = HashMap::new();
|
||||
let Ok(read) = fs::read_dir(&loc.directory) else {
|
||||
return orphans;
|
||||
};
|
||||
for ent in read.flatten() {
|
||||
if ent.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
let name = ent.file_name().to_string_lossy().into_owned();
|
||||
let Some(dot) = name.rfind('.') else {
|
||||
continue;
|
||||
};
|
||||
let (base, ext) = name.split_at(dot);
|
||||
let Some(shard_id) = is_ec_shard_extension(ext) else {
|
||||
continue;
|
||||
};
|
||||
// Ignore zero-byte shards. Use the DirEntry's metadata so we
|
||||
// don't pay a second stat syscall per file beyond what
|
||||
// read_dir already returned.
|
||||
match ent.metadata() {
|
||||
Ok(meta) if meta.len() > 0 => {}
|
||||
_ => continue,
|
||||
}
|
||||
let Some((collection, vid)) = parse_collection_volume_id_pub(base) else {
|
||||
continue;
|
||||
};
|
||||
// Skip shards that are already registered to an EcVolume.
|
||||
if let Some(ecv) = loc.find_ec_volume(vid)
|
||||
&& ecv.has_shard(shard_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let key = EcKey { collection, vid };
|
||||
orphans.entry(key).or_default().push((name, shard_id));
|
||||
}
|
||||
orphans
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -541,7 +597,13 @@ mod tests {
|
||||
std::fs::write(&p, b"shard data nonempty").unwrap();
|
||||
}
|
||||
|
||||
fn write_index_files(idx_dir: &str, collection: &str, vid: u32, data_shards: u32, parity_shards: u32) {
|
||||
fn write_index_files(
|
||||
idx_dir: &str,
|
||||
collection: &str,
|
||||
vid: u32,
|
||||
data_shards: u32,
|
||||
parity_shards: u32,
|
||||
) {
|
||||
// Minimal sealed .ecx (the loader only opens the file; it
|
||||
// doesn't parse it during placement).
|
||||
std::fs::write(
|
||||
@@ -583,6 +645,34 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// A 0-byte `.ecx` is not a credible owner (Go's indexEcxOwners skips
|
||||
/// it): picking the stub would hide the valid index on the sibling disk.
|
||||
#[test]
|
||||
fn test_index_ecx_owners_skips_zero_byte_stub() {
|
||||
let (store, _tmp) = make_test_store(2, None);
|
||||
let d0 = store.locations[0].directory.clone();
|
||||
let d1 = store.locations[1].directory.clone();
|
||||
std::fs::write(ec_local_ecx_path(&d0, "pics", VolumeId(7)), b"").unwrap();
|
||||
write_index_files(&d1, "pics", 7, 10, 4);
|
||||
|
||||
let owners = store.index_ecx_owners();
|
||||
let owner = owners
|
||||
.get(&EcKey {
|
||||
collection: "pics".to_string(),
|
||||
vid: VolumeId(7),
|
||||
})
|
||||
.expect("the valid .ecx on disk 1 must be indexed");
|
||||
assert_eq!(owner.location, 1);
|
||||
assert_eq!(owner.idx_dir, d1);
|
||||
|
||||
// A stub with no real index anywhere owns nothing.
|
||||
std::fs::write(ec_local_ecx_path(&d0, "pics", VolumeId(8)), b"").unwrap();
|
||||
assert!(!store.index_ecx_owners().contains_key(&EcKey {
|
||||
collection: "pics".to_string(),
|
||||
vid: VolumeId(8),
|
||||
}));
|
||||
}
|
||||
|
||||
/// An empty `.dat` (<= a superblock, i.e. zero needles) for an EC volume
|
||||
/// is a leftover stub from the pre-fix loader. It must be swept on startup,
|
||||
/// not loaded as a phantom empty volume. With the same vid's stub on two
|
||||
@@ -895,15 +985,74 @@ mod tests {
|
||||
// dir1 owns the .ecx and so already has shard 1 mounted via
|
||||
// its own load_all_ec_shards.
|
||||
let ev1 = store.locations[1].find_ec_volume(VolumeId(vid));
|
||||
assert!(ev1.is_some(), "baseline broken: dir1 should have mounted shard 1");
|
||||
assert!(
|
||||
ev1.is_some(),
|
||||
"baseline broken: dir1 should have mounted shard 1"
|
||||
);
|
||||
|
||||
// dir0's shards must be reconciled across to its own
|
||||
// ec_volumes map, pointing at dir1's idx dir.
|
||||
let ev0 = store.locations[0]
|
||||
.find_ec_volume(VolumeId(vid))
|
||||
.expect("dir0 should now have an EcVolume after reconcile");
|
||||
assert!(ev0.has_shard(0), "shard 0 missing from dir0 after reconcile");
|
||||
assert!(ev0.has_shard(12), "shard 12 missing from dir0 after reconcile");
|
||||
assert!(
|
||||
ev0.has_shard(0),
|
||||
"shard 0 missing from dir0 after reconcile"
|
||||
);
|
||||
assert!(
|
||||
ev0.has_shard(12),
|
||||
"shard 12 missing from dir0 after reconcile"
|
||||
);
|
||||
}
|
||||
|
||||
/// dir0 holds orphan shards next to a 0-byte `.ecx` stub from a failed
|
||||
/// copy; the real index is on dir1. The stub must not count as a
|
||||
/// locally-mirrored index (Go gates that fast path on HasEcxFileOnDisk),
|
||||
/// or the shards get registered against an empty index.
|
||||
#[test]
|
||||
fn test_reconcile_ignores_zero_byte_local_ecx_stub() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let dir0 = tmp.path().join("data0");
|
||||
let dir1 = tmp.path().join("data1");
|
||||
std::fs::create_dir_all(&dir0).unwrap();
|
||||
std::fs::create_dir_all(&dir1).unwrap();
|
||||
|
||||
let collection = "grafana-loki";
|
||||
let vid = 1094u32;
|
||||
|
||||
write_shard(dir0.to_str().unwrap(), collection, vid, 0);
|
||||
write_shard(dir1.to_str().unwrap(), collection, vid, 1);
|
||||
write_index_files(dir1.to_str().unwrap(), collection, vid, 10, 4);
|
||||
|
||||
let mut store = Store::new(NeedleMapKind::InMemory);
|
||||
for dir in [&dir0, &dir1] {
|
||||
store
|
||||
.add_location(
|
||||
dir.to_str().unwrap(),
|
||||
dir.to_str().unwrap(),
|
||||
100,
|
||||
DiskType::HardDrive,
|
||||
MinFreeSpace::Percent(0.0),
|
||||
Vec::new(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
// Plant the stub after the startup scan so only the reconcile decision
|
||||
// is under test, then drop dir0's mount and reconcile again.
|
||||
store.locations[0].remove_ec_volume(VolumeId(vid));
|
||||
std::fs::write(
|
||||
ec_local_ecx_path(dir0.to_str().unwrap(), collection, VolumeId(vid)),
|
||||
b"",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
store.reconcile_ec_shards_across_disks();
|
||||
|
||||
let ev0 = store.locations[0]
|
||||
.find_ec_volume(VolumeId(vid))
|
||||
.expect("dir0's shard must be mounted against the owner's index");
|
||||
assert!(ev0.has_shard(0));
|
||||
assert_eq!(ev0.ecx_actual_dir(), dir1.to_str().unwrap());
|
||||
}
|
||||
|
||||
/// PR 9244 review case: idx_directory is configured but the
|
||||
@@ -1012,7 +1161,13 @@ mod tests {
|
||||
assert!(store.locations[0].find_ec_volume(VolumeId(vid)).is_none());
|
||||
// Shard files must still exist on disk for operator recovery.
|
||||
for sid in [0u8, 12u8] {
|
||||
let p = format!("{}/{}_{}.ec{:02}", dir0.to_str().unwrap(), collection, vid, sid);
|
||||
let p = format!(
|
||||
"{}/{}_{}.ec{:02}",
|
||||
dir0.to_str().unwrap(),
|
||||
collection,
|
||||
vid,
|
||||
sid
|
||||
);
|
||||
assert!(
|
||||
std::path::Path::new(&p).exists(),
|
||||
"orphan shard {} was destroyed",
|
||||
@@ -1077,10 +1232,12 @@ mod tests {
|
||||
assert!(ev1.has_shard(6), "dir1 shard missing");
|
||||
|
||||
// Nothing left to recover.
|
||||
assert!(store
|
||||
.collect_ec_volumes_missing_index()
|
||||
.iter()
|
||||
.all(|m| m.vid != VolumeId(vid)));
|
||||
assert!(
|
||||
store
|
||||
.collect_ec_volumes_missing_index()
|
||||
.iter()
|
||||
.all(|m| m.vid != VolumeId(vid))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1112,10 +1269,12 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert!(store
|
||||
.collect_ec_volumes_missing_index()
|
||||
.iter()
|
||||
.all(|m| m.vid != VolumeId(vid)));
|
||||
assert!(
|
||||
store
|
||||
.collect_ec_volumes_missing_index()
|
||||
.iter()
|
||||
.all(|m| m.vid != VolumeId(vid))
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper: build a 2-disk store where reconcile produces the
|
||||
@@ -1193,7 +1352,11 @@ mod tests {
|
||||
let vid = VolumeId(7010);
|
||||
|
||||
let all = store.find_all_ec_volumes(vid);
|
||||
assert_eq!(all.len(), 2, "expected one EcVolume per disk holding the vid");
|
||||
assert_eq!(
|
||||
all.len(),
|
||||
2,
|
||||
"expected one EcVolume per disk holding the vid"
|
||||
);
|
||||
|
||||
// Disk 0 carries shards 0 and 12; disk 1 carries shard 1.
|
||||
assert!(all[0].has_shard(0));
|
||||
@@ -1214,7 +1377,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_scrub_plans_reach_every_disk_through_the_store() {
|
||||
use crate::storage::erasure_coding::ec_volume::{
|
||||
merge_ec_runtimes, EcChecksumScrubPlan, EcLocalScrubPlan,
|
||||
EcChecksumScrubPlan, EcLocalScrubPlan, merge_ec_runtimes,
|
||||
};
|
||||
|
||||
let (store, _tmp) = build_split_disk_store(7030);
|
||||
@@ -1229,8 +1392,15 @@ mod tests {
|
||||
let merged = merge_ec_runtimes(&runtimes).expect("two runtimes merge");
|
||||
assert!(merged.slots[0].is_some(), "disk 0's shard 0 unreachable");
|
||||
assert!(merged.slots[12].is_some(), "disk 0's shard 12 unreachable");
|
||||
assert!(merged.slots[1].is_some(), "disk 1's shard 1 unreachable — the bug");
|
||||
assert!(merged.skipped.is_empty(), "same generation: {:?}", merged.skipped);
|
||||
assert!(
|
||||
merged.slots[1].is_some(),
|
||||
"disk 1's shard 1 unreachable — the bug"
|
||||
);
|
||||
assert!(
|
||||
merged.skipped.is_empty(),
|
||||
"same generation: {:?}",
|
||||
merged.skipped
|
||||
);
|
||||
|
||||
// Shard 1 is owned by the sibling runtime, not the anchor.
|
||||
let (owner, _) = merged.slots[1].unwrap();
|
||||
@@ -1306,7 +1476,7 @@ mod tests {
|
||||
let vid = VolumeId(7004);
|
||||
let collection = "grafana-loki";
|
||||
|
||||
store.delete_ec_shards(vid, collection, &[1]);
|
||||
store.delete_ec_shards(vid, collection, &[1]).unwrap();
|
||||
|
||||
// Shard 1 file is gone on disk 1.
|
||||
let p1 = format!(
|
||||
@@ -1337,17 +1507,22 @@ mod tests {
|
||||
let (_ev, dirs) = store.collect_ec_shard_dirs(vid, max_shards).unwrap();
|
||||
|
||||
// Shards 0 and 12 → disk 0's directory.
|
||||
assert_eq!(dirs[0].as_deref(), Some(store.locations[0].directory.as_str()));
|
||||
assert_eq!(dirs[12].as_deref(), Some(store.locations[0].directory.as_str()));
|
||||
assert_eq!(
|
||||
dirs[0].as_deref(),
|
||||
Some(store.locations[0].directory.as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
dirs[12].as_deref(),
|
||||
Some(store.locations[0].directory.as_str())
|
||||
);
|
||||
// Shard 1 → disk 1's directory.
|
||||
assert_eq!(dirs[1].as_deref(), Some(store.locations[1].directory.as_str()));
|
||||
assert_eq!(
|
||||
dirs[1].as_deref(),
|
||||
Some(store.locations[1].directory.as_str())
|
||||
);
|
||||
// Unmounted shards → None.
|
||||
for sid in [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13] {
|
||||
assert_eq!(
|
||||
dirs[sid], None,
|
||||
"shard {} unexpectedly reported a dir",
|
||||
sid,
|
||||
);
|
||||
assert_eq!(dirs[sid], None, "shard {} unexpectedly reported a dir", sid,);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1656,11 +1831,7 @@ mod tests {
|
||||
vec![0u8; 20],
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
ec_dir.join(format!("{}_{}.ecj", collection, vid)),
|
||||
b"",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(ec_dir.join(format!("{}_{}.ecj", collection, vid)), b"").unwrap();
|
||||
|
||||
let mut store = Store::new(NeedleMapKind::InMemory);
|
||||
store
|
||||
@@ -1759,50 +1930,3 @@ mod tests {
|
||||
assert!(std::path::Path::new(&format!("{}.ecx", ec_base)).exists());
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk a disk's data directory and return the `.ec??` shard files
|
||||
/// that are present on disk but not yet registered in the location's
|
||||
/// `ec_volumes` map. Keyed by (collection, vid) so callers can match
|
||||
/// each group against its `.ecx`-owning disk in one lookup. Zero-byte
|
||||
/// shard files are ignored — same shape as `load_all_ec_shards`.
|
||||
fn collect_orphan_ec_shards(
|
||||
loc: &crate::storage::disk_location::DiskLocation,
|
||||
_loc_idx: usize,
|
||||
) -> HashMap<EcKey, Vec<(String, u32)>> {
|
||||
let mut orphans: HashMap<EcKey, Vec<(String, u32)>> = HashMap::new();
|
||||
let Ok(read) = fs::read_dir(&loc.directory) else {
|
||||
return orphans;
|
||||
};
|
||||
for ent in read.flatten() {
|
||||
if ent.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
let name = ent.file_name().to_string_lossy().into_owned();
|
||||
let Some(dot) = name.rfind('.') else {
|
||||
continue;
|
||||
};
|
||||
let (base, ext) = name.split_at(dot);
|
||||
let Some(shard_id) = is_ec_shard_extension(ext) else {
|
||||
continue;
|
||||
};
|
||||
// Ignore zero-byte shards. Use the DirEntry's metadata so we
|
||||
// don't pay a second stat syscall per file beyond what
|
||||
// read_dir already returned.
|
||||
match ent.metadata() {
|
||||
Ok(meta) if meta.len() > 0 => {}
|
||||
_ => continue,
|
||||
}
|
||||
let Some((collection, vid)) = parse_collection_volume_id_pub(base) else {
|
||||
continue;
|
||||
};
|
||||
// Skip shards that are already registered to an EcVolume.
|
||||
if let Some(ecv) = loc.find_ec_volume(vid) {
|
||||
if ecv.has_shard(shard_id as u8) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let key = EcKey { collection, vid };
|
||||
orphans.entry(key).or_default().push((name, shard_id));
|
||||
}
|
||||
orphans
|
||||
}
|
||||
|
||||
@@ -221,6 +221,35 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::storage::types::*;
|
||||
|
||||
/// Multi-byte input must be an error, not a panic: `to_digit` on the
|
||||
/// leading characters rejects it before `chars[2]` is ever indexed.
|
||||
#[test]
|
||||
fn replica_placement_rejects_non_ascii_instead_of_panicking() {
|
||||
for s in ["é", "0é", "é0", "🦀", "ééé"] {
|
||||
assert!(
|
||||
ReplicaPlacement::from_string(s).is_err(),
|
||||
"non-ASCII replication {:?} must error",
|
||||
s
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The ASCII guard must not change any accepted input, including the
|
||||
/// zero-padding shorthands.
|
||||
#[test]
|
||||
fn replica_placement_still_accepts_ascii_shorthands() {
|
||||
assert_eq!(
|
||||
ReplicaPlacement::from_string("1").unwrap(),
|
||||
ReplicaPlacement::from_string("001").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
ReplicaPlacement::from_string("01").unwrap(),
|
||||
ReplicaPlacement::from_string("001").unwrap()
|
||||
);
|
||||
let rp = ReplicaPlacement::from_string("010").unwrap();
|
||||
assert_eq!(rp.diff_rack_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_super_block_round_trip() {
|
||||
let sb = SuperBlock {
|
||||
|
||||
@@ -155,7 +155,7 @@ impl Size {
|
||||
return 0;
|
||||
}
|
||||
if self.0 < 0 {
|
||||
return (self.0 * -1) as u32;
|
||||
return -self.0 as u32;
|
||||
}
|
||||
self.0 as u32
|
||||
}
|
||||
@@ -284,8 +284,9 @@ impl fmt::Display for Offset {
|
||||
// DiskType
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
|
||||
pub enum DiskType {
|
||||
#[default]
|
||||
HardDrive,
|
||||
Ssd,
|
||||
Custom(String),
|
||||
@@ -319,12 +320,6 @@ impl fmt::Display for DiskType {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DiskType {
|
||||
fn default() -> Self {
|
||||
DiskType::HardDrive
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// VolumeId
|
||||
// ============================================================================
|
||||
@@ -397,7 +392,7 @@ impl From<u8> for Version {
|
||||
///
|
||||
/// Fields are split into request-side options (set by the caller) and response-side
|
||||
/// flags (set during the read to communicate status back).
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ReadOption {
|
||||
// -- request --
|
||||
/// If true, allow reading needles that have been soft-deleted.
|
||||
@@ -423,21 +418,6 @@ pub struct ReadOption {
|
||||
pub read_buffer_size: i32,
|
||||
}
|
||||
|
||||
impl Default for ReadOption {
|
||||
fn default() -> Self {
|
||||
ReadOption {
|
||||
read_deleted: false,
|
||||
attempt_meta_only: false,
|
||||
must_meta_only: false,
|
||||
is_meta_only: false,
|
||||
volume_revision: 0,
|
||||
is_out_of_range: false,
|
||||
has_slow_read: false,
|
||||
read_buffer_size: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NeedleMapEntry helpers (for .idx file)
|
||||
// ============================================================================
|
||||
|
||||
+3011
-1223
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ use crate::storage::needle::Needle;
|
||||
use crate::storage::super_block::SuperBlock;
|
||||
use crate::storage::types::*;
|
||||
use crate::storage::volume::{
|
||||
fsync_dir, needle_disk_end, scan_volume_file, Volume, VolumeError, VolumeFileVisitor,
|
||||
Volume, VolumeError, VolumeFileVisitor, fsync_dir, needle_disk_end, scan_volume_file,
|
||||
};
|
||||
|
||||
/// Writes one .idx row per .dat record, in .dat append order, which is the
|
||||
@@ -105,11 +105,11 @@ impl Volume {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::storage::needle::crc::CRC;
|
||||
use crate::storage::needle::Needle;
|
||||
use crate::storage::needle::crc::CRC;
|
||||
use crate::storage::needle_map::NeedleMapKind;
|
||||
use crate::storage::types::*;
|
||||
use crate::storage::volume::Volume;
|
||||
use crate::storage::volume::{Volume, VolumeSpec};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
@@ -145,13 +145,9 @@ mod tests {
|
||||
let mut v = Volume::new(
|
||||
data,
|
||||
old_idx,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
for id in 1..=3 {
|
||||
@@ -167,13 +163,9 @@ mod tests {
|
||||
let reopened = Volume::new(
|
||||
data,
|
||||
new_idx,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -207,13 +199,9 @@ mod tests {
|
||||
let mut v = Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
v.write_needle(&mut needle(1), true, false).unwrap();
|
||||
@@ -233,13 +221,9 @@ mod tests {
|
||||
let reopened = Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
drop(reopened);
|
||||
@@ -261,13 +245,9 @@ mod tests {
|
||||
let mut v = Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
v.write_needle(&mut needle(1), true, false).unwrap();
|
||||
@@ -290,13 +270,9 @@ mod tests {
|
||||
let reopened = Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
drop(reopened);
|
||||
@@ -318,13 +294,9 @@ mod tests {
|
||||
let mut v = Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
v.write_needle(&mut needle(1), true, false).unwrap();
|
||||
@@ -356,13 +328,9 @@ mod tests {
|
||||
let reopened = Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
drop(reopened);
|
||||
|
||||
@@ -9,10 +9,10 @@ use std::path::Path;
|
||||
use tracing::info;
|
||||
|
||||
use crate::storage::idx;
|
||||
use crate::storage::needle::needle::needle_body_length;
|
||||
use crate::storage::needle::Needle;
|
||||
use crate::storage::needle::needle::needle_body_length;
|
||||
use crate::storage::types::*;
|
||||
use crate::storage::volume::{fsync_dir, Volume, VolumeError};
|
||||
use crate::storage::volume::{Volume, VolumeError, fsync_dir};
|
||||
|
||||
/// Needles found in the head of .dat, keyed by id, plus the ids in .dat order.
|
||||
type DatHeadNeedles = (HashMap<NeedleId, (Offset, Size)>, Vec<NeedleId>);
|
||||
@@ -122,7 +122,19 @@ impl Volume {
|
||||
} else {
|
||||
found.remove(&id);
|
||||
}
|
||||
offset += NEEDLE_HEADER_SIZE as i64 + needle_body_length(size, version);
|
||||
let record_size = NEEDLE_HEADER_SIZE as i64 + needle_body_length(size, version);
|
||||
// A corrupt header can make the record length zero or negative;
|
||||
// the scan cannot advance past it.
|
||||
if record_size <= 0 {
|
||||
return Err(VolumeError::Io(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"corrupt needle header at offset {offset}: size {}, record length {record_size}",
|
||||
size.0
|
||||
),
|
||||
)));
|
||||
}
|
||||
offset += record_size;
|
||||
}
|
||||
|
||||
Ok((found, order))
|
||||
@@ -215,20 +227,19 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::storage::needle::crc::CRC;
|
||||
use crate::storage::needle_map::NeedleMapKind;
|
||||
use std::os::unix::fs::{FileExt, PermissionsExt};
|
||||
use crate::storage::volume::VolumeSpec;
|
||||
use std::io::{Seek, SeekFrom};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn open_volume(dir: &str) -> Volume {
|
||||
Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
"",
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
@@ -256,7 +267,7 @@ mod tests {
|
||||
/// writes (key, offset 0, tombstone) rows over the front of .idx instead of
|
||||
/// appending them.
|
||||
fn clobber_idx_head(idx_path: &str, keys: &[u64]) {
|
||||
let file = OpenOptions::new().write(true).open(idx_path).unwrap();
|
||||
let mut file = OpenOptions::new().write(true).open(idx_path).unwrap();
|
||||
for (i, key) in keys.iter().enumerate() {
|
||||
let mut row = Vec::new();
|
||||
idx::write_index_entry(
|
||||
@@ -266,8 +277,13 @@ mod tests {
|
||||
TOMBSTONE_FILE_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
file.write_at(&row, (i * NEEDLE_MAP_ENTRY_SIZE) as u64)
|
||||
// Positional write without Unix-only `FileExt::write_at`, so this
|
||||
// helper (and the tests using it) also builds on Windows.
|
||||
// Single-threaded test helper: no concurrent reader can move the
|
||||
// offset between seek and write.
|
||||
file.seek(SeekFrom::Start((i * NEEDLE_MAP_ENTRY_SIZE) as u64))
|
||||
.unwrap();
|
||||
file.write_all(&row).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +321,7 @@ mod tests {
|
||||
let size_before = idx_size(&idx_path);
|
||||
|
||||
// The rewrite replaces .idx wholesale, so it must not widen the mode.
|
||||
#[cfg(unix)]
|
||||
fs::set_permissions(&idx_path, fs::Permissions::from_mode(0o600)).unwrap();
|
||||
|
||||
// Deletes against needles 9..12 land on the front of .idx and take the
|
||||
@@ -324,11 +341,14 @@ mod tests {
|
||||
let want = size_before + 4 * NEEDLE_MAP_ENTRY_SIZE as u64;
|
||||
assert_eq!(idx_size(&idx_path), want, "idx size after recovery");
|
||||
|
||||
assert_eq!(
|
||||
fs::metadata(&idx_path).unwrap().permissions().mode() & 0o777,
|
||||
0o600,
|
||||
"idx mode after recovery"
|
||||
);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
assert_eq!(
|
||||
fs::metadata(&idx_path).unwrap().permissions().mode() & 0o777,
|
||||
0o600,
|
||||
"idx mode after recovery"
|
||||
);
|
||||
}
|
||||
|
||||
// The recovered rows go back in front, so .idx is in .dat append order
|
||||
// again: the fingerprint is gone and the last row is still the .dat tail.
|
||||
@@ -418,4 +438,29 @@ mod tests {
|
||||
"needle 1 should have been recovered"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scan_dat_head_fails_at_a_header_it_cannot_advance_past() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let dir = tmp.path().to_str().unwrap();
|
||||
write_test_volume(dir, 2);
|
||||
|
||||
let mut dat = OpenOptions::new()
|
||||
.append(true)
|
||||
.open(format!("{}/1.dat", dir))
|
||||
.unwrap();
|
||||
let mut corrupt = [0u8; NEEDLE_HEADER_SIZE];
|
||||
NeedleId(99).to_bytes(&mut corrupt[4..12]);
|
||||
Size(-100).to_bytes(&mut corrupt[12..16]);
|
||||
dat.write_all(&corrupt).unwrap();
|
||||
drop(dat);
|
||||
|
||||
let v = open_volume(dir);
|
||||
let first = v.super_block.block_size() as i64;
|
||||
let err = v.scan_dat_head(v.version(), first, 10).unwrap_err();
|
||||
assert!(
|
||||
matches!(&err, VolumeError::Io(e) if e.kind() == io::ErrorKind::InvalidData),
|
||||
"expected a corrupt-data error, got {err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,10 @@ mod tests {
|
||||
let empty = master_pb::VolumeInformationMessage::default();
|
||||
assert_eq!(report_hash(&empty), 10988706248825469653);
|
||||
|
||||
let mut one = master_pb::VolumeInformationMessage::default();
|
||||
one.id = 1;
|
||||
let one = master_pb::VolumeInformationMessage {
|
||||
id: 1,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(report_hash(&one), 2035849960016744285);
|
||||
|
||||
let full = master_pb::VolumeInformationMessage {
|
||||
|
||||
@@ -66,7 +66,7 @@ fn parse_go_version_number() -> Option<String> {
|
||||
}
|
||||
}
|
||||
match (major, minor) {
|
||||
(Some(maj), Some(min)) => Some(format!("{}.{}", maj, format!("{:02}", min))),
|
||||
(Some(maj), Some(min)) => Some(format!("{}.{:02}", maj, min)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,26 +20,71 @@ use std::collections::{HashMap, HashSet};
|
||||
fn ungated_handlers() -> HashMap<&'static str, &'static str> {
|
||||
[
|
||||
// Cluster-internal: issued volume server -> volume server.
|
||||
("copy_file", "replica sync and EC task pull whole files from a peer"),
|
||||
("read_needle_blob", "replica sync, vacuum and EC rebuild read needles from a peer"),
|
||||
("read_needle_meta", "replica sync compares needle metadata across peers"),
|
||||
(
|
||||
"copy_file",
|
||||
"replica sync and EC task pull whole files from a peer",
|
||||
),
|
||||
(
|
||||
"read_needle_blob",
|
||||
"replica sync, vacuum and EC rebuild read needles from a peer",
|
||||
),
|
||||
(
|
||||
"read_needle_meta",
|
||||
"replica sync compares needle metadata across peers",
|
||||
),
|
||||
("write_needle_blob", "replica sync repairs a peer's needle"),
|
||||
("receive_file", "EC shard distribution pushes shards to a peer"),
|
||||
("read_volume_file_status", "the copy path queries the source volume server"),
|
||||
("volume_ec_shard_read", "a volume server reads EC shards held by a peer"),
|
||||
("volume_ec_blob_delete", "EC delete is fanned out to the shard holders"),
|
||||
("volume_ec_shards_info", "EC verification polls shard holders"),
|
||||
("volume_ec_shards_mount", "EC shard distribution mounts on the receiving peer"),
|
||||
("volume_incremental_copy", "volume backup pulls increments from a peer"),
|
||||
("volume_sync_status", "sync compares volume state across peers"),
|
||||
("volume_tail_sender", "the tail source streams to the receiving peer"),
|
||||
("volume_status", "replica sync and the master's vacuum loop poll volume status"),
|
||||
(
|
||||
"receive_file",
|
||||
"EC shard distribution pushes shards to a peer",
|
||||
),
|
||||
(
|
||||
"read_volume_file_status",
|
||||
"the copy path queries the source volume server",
|
||||
),
|
||||
(
|
||||
"volume_ec_shard_read",
|
||||
"a volume server reads EC shards held by a peer",
|
||||
),
|
||||
(
|
||||
"volume_ec_blob_delete",
|
||||
"EC delete is fanned out to the shard holders",
|
||||
),
|
||||
(
|
||||
"volume_ec_shards_info",
|
||||
"EC verification polls shard holders",
|
||||
),
|
||||
(
|
||||
"volume_ec_shards_mount",
|
||||
"EC shard distribution mounts on the receiving peer",
|
||||
),
|
||||
(
|
||||
"volume_incremental_copy",
|
||||
"volume backup pulls increments from a peer",
|
||||
),
|
||||
(
|
||||
"volume_sync_status",
|
||||
"sync compares volume state across peers",
|
||||
),
|
||||
(
|
||||
"volume_tail_sender",
|
||||
"the tail source streams to the receiving peer",
|
||||
),
|
||||
(
|
||||
"volume_status",
|
||||
"replica sync and the master's vacuum loop poll volume status",
|
||||
),
|
||||
// Read-only or liveness: no state change.
|
||||
("ping", "liveness probe"),
|
||||
("get_state", "read-only volume server state"),
|
||||
("query", "read-only data query"),
|
||||
("vacuum_volume_check", "read-only garbage ratio; the vacuum steps that act on it are gated"),
|
||||
("volume_server_status", "read-only status, the gRPC counterpart of the /status page"),
|
||||
(
|
||||
"vacuum_volume_check",
|
||||
"read-only garbage ratio; the vacuum steps that act on it are gated",
|
||||
),
|
||||
(
|
||||
"volume_server_status",
|
||||
"read-only status, the gRPC counterpart of the /status page",
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
@@ -129,5 +174,9 @@ fn volume_server_admin_auth_coverage() {
|
||||
}
|
||||
}
|
||||
|
||||
assert!(problems.is_empty(), "admin-auth coverage gaps:\n{}", problems.join("\n"));
|
||||
assert!(
|
||||
problems.is_empty(),
|
||||
"admin-auth coverage gaps:\n{}",
|
||||
problems.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,12 +12,13 @@ use tower::ServiceExt; // for `oneshot`
|
||||
|
||||
use seaweed_volume::security::{Guard, SigningKey};
|
||||
use seaweed_volume::server::volume_server::{
|
||||
build_admin_router, build_admin_router_with_ui, build_metrics_router, build_public_router,
|
||||
VolumeServerState,
|
||||
VolumeServerState, build_admin_router, build_admin_router_with_ui, build_metrics_router,
|
||||
build_public_router,
|
||||
};
|
||||
use seaweed_volume::storage::needle_map::NeedleMapKind;
|
||||
use seaweed_volume::storage::store::Store;
|
||||
use seaweed_volume::storage::types::{DiskType, Version, VolumeId};
|
||||
use seaweed_volume::storage::types::{DiskType, VolumeId};
|
||||
use seaweed_volume::storage::volume::VolumeSpec;
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -73,12 +74,11 @@ fn build_test_state(
|
||||
store
|
||||
.add_volume(
|
||||
VolumeId(1),
|
||||
"",
|
||||
replica_placement,
|
||||
None,
|
||||
0,
|
||||
DiskType::HardDrive,
|
||||
Version::current(),
|
||||
&VolumeSpec {
|
||||
replica_placement,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("failed to create volume");
|
||||
|
||||
@@ -112,9 +112,6 @@ fn build_test_state(
|
||||
pre_stop_seconds: 0,
|
||||
volume_state_notify: tokio::sync::Notify::new(),
|
||||
write_queue: std::sync::OnceLock::new(),
|
||||
s3_tier_registry: std::sync::RwLock::new(
|
||||
seaweed_volume::remote_storage::s3_tier::S3TierRegistry::new(),
|
||||
),
|
||||
read_mode: seaweed_volume::config::ReadMode::Local,
|
||||
allow_untrusted_remote_endpoints: false,
|
||||
master_url,
|
||||
@@ -480,6 +477,74 @@ async fn delete_then_get_returns_404() {
|
||||
);
|
||||
}
|
||||
|
||||
// Go answers both with 500 and an error containing "volume N is read only"
|
||||
// (the write behind "failed to write to local disk: ").
|
||||
#[tokio::test]
|
||||
async fn write_and_delete_on_read_only_volume_say_is_read_only() {
|
||||
let (state, _tmp) = test_state();
|
||||
let uri = "/1,01637037d6";
|
||||
|
||||
let request = |method: &str, body: &[u8]| {
|
||||
Request::builder()
|
||||
.method(method)
|
||||
.uri(uri)
|
||||
.body(Body::from(body.to_vec()))
|
||||
.unwrap()
|
||||
};
|
||||
let error_of = |body: Vec<u8>| -> String {
|
||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
json["error"].as_str().unwrap_or_default().to_string()
|
||||
};
|
||||
|
||||
let response = build_admin_router(state.clone())
|
||||
.oneshot(request("POST", b"written before read-only"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::CREATED);
|
||||
|
||||
state
|
||||
.store
|
||||
.write()
|
||||
.unwrap()
|
||||
.find_volume_mut(VolumeId(1))
|
||||
.unwrap()
|
||||
.1
|
||||
.set_no_write_or_delete(true);
|
||||
|
||||
let response = build_admin_router(state.clone())
|
||||
.oneshot(request("POST", b"refused"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
let error = error_of(body_bytes(response).await);
|
||||
assert!(error.contains("volume 1 is read only"), "{error}");
|
||||
|
||||
let response = build_admin_router(state.clone())
|
||||
.oneshot(request("DELETE", b""))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(
|
||||
error_of(body_bytes(response).await),
|
||||
"Deletion Failed: volume 1 is read only"
|
||||
);
|
||||
|
||||
// The needle is still there to delete once the volume is writable again.
|
||||
state
|
||||
.store
|
||||
.write()
|
||||
.unwrap()
|
||||
.find_volume_mut(VolumeId(1))
|
||||
.unwrap()
|
||||
.1
|
||||
.set_no_write_or_delete(false);
|
||||
let response = build_admin_router(state.clone())
|
||||
.oneshot(request("DELETE", b""))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::ACCEPTED);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 6. HEAD returns headers without body
|
||||
// ============================================================================
|
||||
@@ -957,9 +1022,10 @@ async fn replicate_write_does_not_re_replicate() {
|
||||
#[tokio::test]
|
||||
async fn chunk_manifest_expands_chunk_stored_on_ec_volume() {
|
||||
use seaweed_volume::storage::erasure_coding::ec_encoder::write_ec_files;
|
||||
use seaweed_volume::storage::erasure_coding::ec_shard::ShardId;
|
||||
use seaweed_volume::storage::needle::needle::{FileId, Needle};
|
||||
use seaweed_volume::storage::types::{Cookie, NeedleId};
|
||||
use seaweed_volume::storage::volume::Volume;
|
||||
use seaweed_volume::storage::volume::{Volume, VolumeSpec};
|
||||
|
||||
let (state, tmp) = test_state();
|
||||
let dir = tmp.path().to_str().unwrap();
|
||||
@@ -976,13 +1042,9 @@ async fn chunk_manifest_expands_chunk_stored_on_ec_volume() {
|
||||
let mut v = Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
"",
|
||||
VolumeId(2),
|
||||
NeedleMapKind::InMemory,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
Version::current(),
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut n = Needle {
|
||||
@@ -1002,7 +1064,7 @@ async fn chunk_manifest_expands_chunk_stored_on_ec_volume() {
|
||||
// after ec.encode retired the regular volume.
|
||||
{
|
||||
let mut store = state.store.write().unwrap();
|
||||
let shard_ids: Vec<u32> = (0..14).collect();
|
||||
let shard_ids: Vec<ShardId> = (0..14).collect();
|
||||
store.mount_ec_shards(VolumeId(2), "", &shard_ids).unwrap();
|
||||
}
|
||||
|
||||
@@ -1046,3 +1108,317 @@ async fn chunk_manifest_expands_chunk_stored_on_ec_volume() {
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(body_bytes(response).await, chunk_data);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HTTP DELETE on an EC volume whose shards are not all mounted locally
|
||||
//
|
||||
// The delete handler used to validate the cookie with the local-only
|
||||
// `EcVolume::read_ec_shard_needle`, which errors "ec shard N not available
|
||||
// locally" for any interval held by a peer. Every such error was mapped to 500
|
||||
// and no `.ecj` tombstone was written, so on a standard 10+4 spread across 14
|
||||
// servers no HTTP delete of an EC needle could ever succeed.
|
||||
//
|
||||
// A single node can exercise that path without peers: mount 13 of the 14
|
||||
// shards, leaving out the one holding the needle's interval. The distributed
|
||||
// reader seeds its Reed-Solomon buffers from locally mounted siblings (Phase 0
|
||||
// in `store_ec.rs`), so with >= 10 survivors it reconstructs without any peer
|
||||
// fan-out — while the local-only read still fails outright.
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_on_ec_volume_succeeds_when_the_needles_shard_is_not_mounted() {
|
||||
use seaweed_volume::storage::erasure_coding::ec_encoder::write_ec_files;
|
||||
use seaweed_volume::storage::erasure_coding::ec_shard::ShardId;
|
||||
use seaweed_volume::storage::needle::needle::{FileId, Needle};
|
||||
use seaweed_volume::storage::types::{Cookie, NeedleId};
|
||||
use seaweed_volume::storage::volume::{Volume, VolumeSpec};
|
||||
|
||||
let (state, tmp) = test_state();
|
||||
let dir = tmp.path().to_str().unwrap();
|
||||
|
||||
let data: Vec<u8> = (0..4096u32).map(|i| (i % 251) as u8).collect();
|
||||
let nid = NeedleId(0x5c);
|
||||
let cookie = Cookie(0x0badc0de);
|
||||
|
||||
// Build regular volume 4, then EC-encode it. The volume is standalone and
|
||||
// never registered in the store, so afterwards it exists only as shards.
|
||||
{
|
||||
let mut v = Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
VolumeId(4),
|
||||
NeedleMapKind::InMemory,
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut n = Needle {
|
||||
id: nid,
|
||||
cookie,
|
||||
data: data.clone(),
|
||||
data_size: data.len() as u32,
|
||||
..Needle::default()
|
||||
};
|
||||
v.write_needle(&mut n, true, false).unwrap();
|
||||
v.sync_to_disk().unwrap();
|
||||
v.close();
|
||||
}
|
||||
write_ec_files(dir, dir, "", VolumeId(4), 10, 4).unwrap();
|
||||
|
||||
// Mount shards 1..=13 only. A needle at .dat offset 0 lives in shard 0's
|
||||
// first small block, so the interval this delete needs is deliberately the
|
||||
// one shard that is absent.
|
||||
{
|
||||
let mut store = state.store.write().unwrap();
|
||||
let shard_ids: Vec<ShardId> = (1..14).collect();
|
||||
store.mount_ec_shards(VolumeId(4), "", &shard_ids).unwrap();
|
||||
}
|
||||
|
||||
let fid = FileId::new(VolumeId(4), nid, cookie).to_string();
|
||||
let app = build_admin_router(state.clone());
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("DELETE")
|
||||
.uri(format!("/{}", fid))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
response.status(),
|
||||
StatusCode::ACCEPTED,
|
||||
"DELETE of an EC needle must reconstruct through the distributed \
|
||||
reader instead of failing 500 on a non-local shard"
|
||||
);
|
||||
|
||||
// The tombstone must actually have landed: a later GET is a 404.
|
||||
let app = build_admin_router(state.clone());
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(format!("/{}", fid))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.status(),
|
||||
StatusCode::NOT_FOUND,
|
||||
"the delete must have been journalled, not just answered 202"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hostile response-header override params must not panic the handler
|
||||
//
|
||||
// The `response-*` query params are attacker-controlled and were inserted with
|
||||
// `parse().unwrap()`. `%0A` decodes to a newline, `HeaderValue::from_str`
|
||||
// rejects it, and the unwrap panicked the connection task — unauthenticated.
|
||||
// The override must simply be skipped.
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn hostile_response_header_overrides_are_skipped_not_panicked() {
|
||||
let (state, _tmp) = test_state();
|
||||
let uri = "/1,01637037d6";
|
||||
|
||||
let app = build_admin_router(state.clone());
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri(uri)
|
||||
.body(Body::from(b"payload".to_vec()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::CREATED);
|
||||
|
||||
// One request per override param, each carrying a raw newline.
|
||||
for param in [
|
||||
"response-cache-control",
|
||||
"response-content-encoding",
|
||||
"response-expires",
|
||||
"response-content-language",
|
||||
"response-content-disposition",
|
||||
"response-content-type",
|
||||
] {
|
||||
let app = build_admin_router(state.clone());
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(format!("{}?{}=%0Aevil", uri, param))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.status(),
|
||||
StatusCode::OK,
|
||||
"{} with a newline must be ignored, not panic",
|
||||
param
|
||||
);
|
||||
assert_eq!(body_bytes(response).await, b"payload".to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Non-ASCII in the fid and in ?ttl= must be rejected, not panic
|
||||
//
|
||||
// `parse_needle_id_cookie` split the hex by BYTE offset and `TTL::read` took
|
||||
// the unit as the last BYTE, so a multi-byte character split inside itself.
|
||||
// Both are reachable unauthenticated from the request line / query string.
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_ascii_fid_and_ttl_are_rejected_not_panicked() {
|
||||
let (state, _tmp) = test_state();
|
||||
|
||||
// A fid whose hex part is multi-byte UTF-8.
|
||||
let app = build_admin_router(state.clone());
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/1,%C3%A9%C3%A9%C3%A9%C3%A9a")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
response.status().is_client_error() || response.status().is_server_error(),
|
||||
"non-ASCII fid must produce an error status, got {}",
|
||||
response.status()
|
||||
);
|
||||
|
||||
// A TTL whose unit character is multi-byte. The upload path does
|
||||
// `TTL::read(..).ok()`, so *any* unparseable TTL is simply dropped and the
|
||||
// write succeeds — the point here is that a non-ASCII one now takes that
|
||||
// same road instead of panicking. Assert it matches an ASCII-invalid TTL
|
||||
// rather than inventing a stricter contract than the handler has.
|
||||
let mut statuses = Vec::new();
|
||||
// Distinct needle ids: reusing one id with a different cookie is a
|
||||
// cookie-mismatch overwrite, which would mask what this test measures.
|
||||
for (fid, ttl) in [("/1,03637037d7", "5%C3%A9"), ("/1,04637037d8", "5z")] {
|
||||
let app = build_admin_router(state.clone());
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri(format!("{}?ttl={}", fid, ttl))
|
||||
.body(Body::from(b"x".to_vec()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
statuses.push(response.status());
|
||||
}
|
||||
assert_eq!(
|
||||
statuses[0], statuses[1],
|
||||
"a non-ASCII ttl must behave like any other invalid ttl, not panic"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// The write queue answers an upload with the needle's real ETag
|
||||
//
|
||||
// The queue worker computes the CRC on the needle it was handed, so the handler
|
||||
// has to know the checksum before it submits. Without that every queued upload
|
||||
// came back as "00000000". The direct path is the reference: same payload, same
|
||||
// ETag, for a plain body and for one the handler gzips before storing.
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_queue_upload_returns_same_etag_as_direct_write() {
|
||||
use seaweed_volume::server::write_queue::WriteQueue;
|
||||
|
||||
let (direct_state, _direct_tmp) = test_state();
|
||||
let (queued_state, _queued_tmp) = test_state();
|
||||
let wq = WriteQueue::new(queued_state.clone(), 128);
|
||||
let _ = queued_state.write_queue.set(wq);
|
||||
|
||||
let compressible = "seaweedfs ".repeat(200).into_bytes();
|
||||
let uploads: [(&str, &[u8]); 2] = [
|
||||
("/1,01637037d6", b"hello, seaweedfs!"),
|
||||
("/1/02637037d6/notes.txt", &compressible),
|
||||
];
|
||||
|
||||
for (uri, payload) in uploads {
|
||||
let mut etags = Vec::new();
|
||||
for state in [&direct_state, &queued_state] {
|
||||
let app = build_admin_router(state.clone());
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri(uri)
|
||||
.body(Body::from(payload.to_vec()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::CREATED);
|
||||
|
||||
let header = response
|
||||
.headers()
|
||||
.get("ETag")
|
||||
.expect("upload response has no ETag")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let body = body_bytes(response).await;
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_slice(&body).expect("POST response is not valid JSON");
|
||||
let etag = json["eTag"].as_str().unwrap().to_string();
|
||||
assert_eq!(header, format!("\"{}\"", etag));
|
||||
etags.push(etag);
|
||||
}
|
||||
assert_ne!(etags[0], "00000000", "{}: direct ETag is the zero CRC", uri);
|
||||
assert_eq!(
|
||||
etags[1], etags[0],
|
||||
"{}: queued upload must return the direct path's ETag",
|
||||
uri
|
||||
);
|
||||
}
|
||||
|
||||
// The second upload really was stored gzipped, so its ETag is the CRC of
|
||||
// the compressed bytes on both paths.
|
||||
let app = build_admin_router(queued_state.clone());
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(uploads[1].0)
|
||||
.header("Accept-Encoding", "gzip")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.headers()["Content-Encoding"], "gzip");
|
||||
|
||||
// Re-uploading the same bytes is the unchanged path: 204 with the same ETag.
|
||||
let (uri, payload) = uploads[0];
|
||||
let mut etags = Vec::new();
|
||||
for state in [&direct_state, &queued_state] {
|
||||
let app = build_admin_router(state.clone());
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri(uri)
|
||||
.body(Body::from(payload.to_vec()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::NO_CONTENT);
|
||||
etags.push(response.headers()["ETag"].to_str().unwrap().to_string());
|
||||
}
|
||||
assert_eq!(etags[1], etags[0], "unchanged upload ETag differs");
|
||||
}
|
||||
|
||||
Generated
+111
-210
@@ -394,28 +394,6 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||
dependencies = [
|
||||
"async-stream-impl",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream-impl"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.92"
|
||||
@@ -701,7 +679,7 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -856,13 +834,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
@@ -875,14 +853,13 @@ dependencies = [
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -890,19 +867,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.4.5"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"futures-core",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
@@ -1965,7 +1940,7 @@ dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"itertools",
|
||||
"parking_lot",
|
||||
"petgraph 0.8.3",
|
||||
"petgraph",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -2793,7 +2768,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.5",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -3249,9 +3224,9 @@ dependencies = [
|
||||
"object_store",
|
||||
"permutation",
|
||||
"pin-project",
|
||||
"prost 0.14.4",
|
||||
"prost-build 0.14.4",
|
||||
"prost-types 0.14.4",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"rand 0.9.5",
|
||||
"rayon",
|
||||
"roaring",
|
||||
@@ -3357,7 +3332,7 @@ dependencies = [
|
||||
"num_cpus",
|
||||
"object_store",
|
||||
"pin-project",
|
||||
"prost 0.14.4",
|
||||
"prost",
|
||||
"quick_cache",
|
||||
"rand 0.9.5",
|
||||
"roaring",
|
||||
@@ -3398,8 +3373,8 @@ dependencies = [
|
||||
"lance-datagen",
|
||||
"log",
|
||||
"pin-project",
|
||||
"prost 0.14.4",
|
||||
"prost-build 0.14.4",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -3461,8 +3436,8 @@ dependencies = [
|
||||
"log",
|
||||
"lz4",
|
||||
"num-traits",
|
||||
"prost 0.14.4",
|
||||
"prost-build 0.14.4",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"rand 0.9.5",
|
||||
"strum",
|
||||
"tokio",
|
||||
@@ -3497,9 +3472,9 @@ dependencies = [
|
||||
"log",
|
||||
"num-traits",
|
||||
"object_store",
|
||||
"prost 0.14.4",
|
||||
"prost-build 0.14.4",
|
||||
"prost-types 0.14.4",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -3554,9 +3529,9 @@ dependencies = [
|
||||
"ndarray",
|
||||
"num-traits",
|
||||
"object_store",
|
||||
"prost 0.14.4",
|
||||
"prost-build 0.14.4",
|
||||
"prost-types 0.14.4",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"rand 0.9.5",
|
||||
"rand_distr",
|
||||
"rangemap",
|
||||
@@ -3590,7 +3565,7 @@ dependencies = [
|
||||
"lance-core",
|
||||
"lance-io",
|
||||
"lance-select",
|
||||
"prost-types 0.14.4",
|
||||
"prost-types",
|
||||
"roaring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3624,7 +3599,7 @@ dependencies = [
|
||||
"opendal",
|
||||
"path_abs",
|
||||
"pin-project",
|
||||
"prost 0.14.4",
|
||||
"prost",
|
||||
"rand 0.9.5",
|
||||
"serde",
|
||||
"tempfile",
|
||||
@@ -3719,9 +3694,9 @@ dependencies = [
|
||||
"lance-select",
|
||||
"log",
|
||||
"object_store",
|
||||
"prost 0.14.4",
|
||||
"prost-build 0.14.4",
|
||||
"prost-types 0.14.4",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"rand 0.9.5",
|
||||
"rangemap",
|
||||
"roaring",
|
||||
@@ -3938,9 +3913,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "matrixmultiply"
|
||||
@@ -4411,16 +4386,6 @@ version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df202b0b0f5b8e389955afd5f27b007b00fb948162953f1db9c70d2c7e3157d7"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"indexmap 2.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.8.3"
|
||||
@@ -4563,16 +4528,6 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive 0.13.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.14.4"
|
||||
@@ -4580,27 +4535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-build"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"itertools",
|
||||
"log",
|
||||
"multimap",
|
||||
"once_cell",
|
||||
"petgraph 0.7.1",
|
||||
"prettyplease",
|
||||
"prost 0.13.5",
|
||||
"prost-types 0.13.5",
|
||||
"regex",
|
||||
"syn 2.0.119",
|
||||
"tempfile",
|
||||
"prost-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4613,28 +4548,17 @@ dependencies = [
|
||||
"itertools",
|
||||
"log",
|
||||
"multimap",
|
||||
"petgraph 0.8.3",
|
||||
"petgraph",
|
||||
"prettyplease",
|
||||
"prost 0.14.4",
|
||||
"prost-types 0.14.4",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"pulldown-cmark",
|
||||
"pulldown-cmark-to-cmark",
|
||||
"regex",
|
||||
"syn 2.0.119",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.14.4"
|
||||
@@ -4648,22 +4572,13 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
||||
dependencies = [
|
||||
"prost 0.13.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a"
|
||||
dependencies = [
|
||||
"prost 0.14.4",
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4730,6 +4645,26 @@ version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3"
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"memchr",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark-to-cmark"
|
||||
version = "22.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab1ad36992cead65f02aa399a373a42730922f1525d988172634fdefdecb8a60"
|
||||
dependencies = [
|
||||
"pulldown-cmark",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.39.4"
|
||||
@@ -4775,7 +4710,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.6.5",
|
||||
"socket2",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -4814,7 +4749,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.6.5",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -4846,24 +4781,13 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_chacha",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
@@ -4878,16 +4802,6 @@ dependencies = [
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
@@ -4898,15 +4812,6 @@ dependencies = [
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
@@ -5160,7 +5065,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
@@ -5199,7 +5104,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
@@ -5309,15 +5214,6 @@ dependencies = [
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.1"
|
||||
@@ -5433,6 +5329,13 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "seaweed-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "seaweed-worker-core"
|
||||
version = "0.1.0"
|
||||
@@ -5441,13 +5344,15 @@ dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"prometheus",
|
||||
"prost 0.13.5",
|
||||
"prost-types 0.13.5",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"protoc-bin-vendored",
|
||||
"seaweed-common",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
"tonic-build",
|
||||
"tonic-prost",
|
||||
"tonic-prost-build",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -5723,16 +5628,6 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.5"
|
||||
@@ -6027,7 +5922,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.6.5",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -6081,11 +5976,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tonic"
|
||||
version = "0.12.3"
|
||||
version = "0.14.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
|
||||
checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
@@ -6099,13 +5993,12 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"prost 0.13.5",
|
||||
"rustls-pemfile",
|
||||
"socket2 0.5.10",
|
||||
"socket2",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"tower 0.4.13",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -6113,36 +6006,41 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tonic-build"
|
||||
version = "0.12.3"
|
||||
version = "0.14.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11"
|
||||
checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322"
|
||||
dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"prost-build 0.13.5",
|
||||
"prost-types 0.13.5",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
name = "tonic-prost"
|
||||
version = "0.14.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||
checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"indexmap 1.9.3",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"rand 0.8.7",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"bytes",
|
||||
"prost",
|
||||
"tonic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic-prost-build"
|
||||
version = "0.14.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27"
|
||||
dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"tempfile",
|
||||
"tonic-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6153,9 +6051,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"indexmap 2.14.0",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -6178,7 +6079,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"url",
|
||||
@@ -6556,7 +6457,7 @@ dependencies = [
|
||||
"lance-table",
|
||||
"prometheus",
|
||||
"reqwest 0.12.28",
|
||||
"rustls",
|
||||
"seaweed-common",
|
||||
"seaweed-worker-core",
|
||||
"seaweed-worker-sort",
|
||||
"serde",
|
||||
|
||||
@@ -16,17 +16,25 @@ edition = "2024"
|
||||
# lance's `aws` feature pulls in).
|
||||
rust-version = "1.94.1"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
# Protobuf message literals keep `..Default::default()` on purpose: it is
|
||||
# what lets a proto gain a field without touching every constructor.
|
||||
needless_update = "allow"
|
||||
# Every `unsafe` block states its precondition, right above the block.
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
async-trait = "0.1"
|
||||
prost = "0.13"
|
||||
prost-types = "0.13"
|
||||
prost = "0.14"
|
||||
prost-types = "0.14"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
tonic = { version = "0.12", features = ["tls"] }
|
||||
tonic = { version = "0.14", features = ["tls-aws-lc"] }
|
||||
tonic-prost = "0.14"
|
||||
# Already in the tree via tonic; named here so the metrics server can use them.
|
||||
axum = "0.7"
|
||||
axum = "0.8"
|
||||
prometheus = { version = "0.13", default-features = false }
|
||||
tonic-build = "0.12"
|
||||
tonic-prost-build = "0.14"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
@@ -9,6 +9,10 @@ one.
|
||||
crates/core the contract: stream, handshake, heartbeat, registry, config forms
|
||||
crates/lance maintenance jobs for Lance tables, and a binary
|
||||
|
||||
It also depends on `../seaweed-common`, a small crate outside this workspace
|
||||
holding the few helpers the Rust volume server needs identically — the
|
||||
HTTP<->gRPC address rule and the rustls provider choice.
|
||||
|
||||
`core` knows nothing about any job. A second worker is a new crate beside
|
||||
`lance` that depends on it, not a fork of the protocol.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user