mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-30 10:08:58 +00:00
Compare commits
167 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 658b8dea66 | |||
| 0478505839 | |||
| 0875f09a39 | |||
| ea74fa7a95 | |||
| 62d1f80dbf | |||
| 0d0a17bb36 | |||
| 3d2449872a | |||
| 5f5b1207e0 | |||
| 64feca3550 | |||
| f1207a9e28 | |||
| f9475e10dc | |||
| b0646be756 | |||
| d74e6eb042 | |||
| 8be787387c | |||
| c9377161e4 | |||
| 522605a055 | |||
| 22bcfc474e | |||
| a28cb34381 | |||
| cbcfe625ef | |||
| 02a7d3c228 | |||
| 53b608c089 | |||
| 2786a4734a | |||
| c9f0f25f55 | |||
| 20a4db7c9a | |||
| 6264be437c | |||
| 69345fe059 | |||
| b42766f1d3 | |||
| 3e3fcbf44d | |||
| 0d94437788 | |||
| ed2078e025 | |||
| 2404bc1657 | |||
| 0985f0b37b | |||
| e69e32285b | |||
| 97858baf8e | |||
| 69dcf9e6cb | |||
| 503f89bf0e | |||
| dcbffb084f | |||
| b2dfdf85de | |||
| 375482a4b6 | |||
| 45b04316b9 | |||
| 19b69abe5c | |||
| c684438625 | |||
| 11054263c1 | |||
| b530a9f0b2 | |||
| dd35538e90 | |||
| c727589161 | |||
| cde53ca6ad | |||
| e929814edc | |||
| a8a5f25af3 | |||
| 54be3cab23 | |||
| 1c8fdfddf4 | |||
| 73bde843d6 | |||
| d6fc70eb12 | |||
| 25c6bdf490 | |||
| f695870626 | |||
| aa3f13c0d3 | |||
| c4248078d6 | |||
| 94da6e34cb | |||
| a9e62dc2c2 | |||
| ecb6704679 | |||
| e65aebba53 | |||
| a66337bd28 | |||
| bd1e57293f | |||
| 0e888cfef2 | |||
| 4c9fd789ea | |||
| 17ae9f34c7 | |||
| cdfe83877b | |||
| be37fd285e | |||
| 095337100e | |||
| e0729f5f4d | |||
| 6e12289339 | |||
| 9dcf8cb7ea | |||
| 6898e720dd | |||
| 824c4f7673 | |||
| c0c92cb048 | |||
| bca8b08c2b | |||
| 738fb86611 | |||
| 092c6bc135 | |||
| 5cda460451 | |||
| fc8322ed64 | |||
| cd2cd74314 | |||
| ced390b02b | |||
| 11be278c40 | |||
| 0893c05540 | |||
| 81754d80b3 | |||
| bdb98598d2 | |||
| 5fccfceb91 | |||
| c1bcee327c | |||
| d4d07095f8 | |||
| 8c49671161 | |||
| c65d4071a3 | |||
| 4ea805cbc0 | |||
| 26341742e0 | |||
| fd3fb77ed5 | |||
| c5125b20bd | |||
| 9b83de53e1 | |||
| d72b8cb88e | |||
| 17eff5a97b | |||
| 129cb0f920 | |||
| 28c9358482 | |||
| fe5690ca70 | |||
| 25cf164461 | |||
| 8a501846f4 | |||
| b2ba2e5bb3 | |||
| 8169bd63e9 | |||
| 6185d38911 | |||
| 8ef2c0a5e1 | |||
| d6813b53a2 | |||
| 5ca99a8e32 | |||
| 07a01a1123 | |||
| 941986b331 | |||
| aba65a448c | |||
| 1c94f7a066 | |||
| 77c21dc5fd | |||
| 404fc816e9 | |||
| 61a7820651 | |||
| 0375dd39bb | |||
| f84d8983ed | |||
| bafff6d207 | |||
| 53ec1b95d8 | |||
| 65c62ab2ea | |||
| c419cff348 | |||
| 5a5ffcfac0 | |||
| 96b293bf8a | |||
| 8892cbbdd7 | |||
| 09c2d15057 | |||
| 6275918d92 | |||
| 5879d0b59d | |||
| cb54cecf8c | |||
| 9288f1ed3a | |||
| 8182e2dd38 | |||
| 81ad48dac2 | |||
| 1582f216fe | |||
| 5081b8396d | |||
| c90bfe2b23 | |||
| 03045ff2e6 | |||
| 61bd5698bb | |||
| 44d2b6a284 | |||
| fe978488a0 | |||
| e209fc6eef | |||
| 97a2775434 | |||
| 9d85b7d23a | |||
| ba7ee5fbdd | |||
| 6896b38dc2 | |||
| 9df57b8cd8 | |||
| 6ec3a4c4d5 | |||
| fd37a7d01e | |||
| 9c0141fbdf | |||
| 2a0fbb8d77 | |||
| 5431b9273d | |||
| b159d656cc | |||
| b7815b0694 | |||
| eaa5ff3053 | |||
| f1cd7c1345 | |||
| 06097a3c33 | |||
| 5e0ca006f0 | |||
| abc07a9dc4 | |||
| 4d6171e996 | |||
| 3130670157 | |||
| 96d41b6349 | |||
| 9b9e0db9f6 | |||
| 68fcbffcb6 | |||
| 04712fb6c6 | |||
| 4728abcff1 | |||
| 70be0804ee | |||
| b10db403b6 | |||
| 9e93d3f47a |
@@ -0,0 +1,124 @@
|
||||
---
|
||||
name: rustfs-release-version-bump
|
||||
description: "Publish a RustFS alpha/beta/stable release with an auditable flow: confirm target version and scope, update workspace and release assets (including strict rustfs.spec changelog identity/date/version format), run required verification, and finish with commit, push, and GitHub PR creation."
|
||||
---
|
||||
# RustFS Release Version Bump
|
||||
|
||||
Use this skill to publish a RustFS release (alpha, beta, or stable) with a minimal, auditable diff and a complete ship flow (`edit -> verify -> commit -> push -> PR`).
|
||||
|
||||
Validated baseline: release pattern used in PR `#2957`.
|
||||
|
||||
## Required inputs
|
||||
|
||||
- Exact target version, for example `1.0.0-beta.4`.
|
||||
- Delivery scope:
|
||||
- Local only (`edit/verify`).
|
||||
- Local + git (`commit/push`).
|
||||
- Full GitHub flow (`commit/push/PR`).
|
||||
|
||||
If target version is missing or ambiguous, stop and ask before editing.
|
||||
|
||||
## Read before editing
|
||||
|
||||
- `AGENTS.md` (root and nearest path-specific files).
|
||||
- `.github/pull_request_template.md`.
|
||||
- Current branch status and diff against `origin/main`.
|
||||
|
||||
## Default release file scope
|
||||
|
||||
Treat the following file list as the default checklist for each release bump:
|
||||
|
||||
- `Cargo.toml`
|
||||
- `Cargo.lock`
|
||||
- `README.md`
|
||||
- `README_ZH.md`
|
||||
- `flake.nix`
|
||||
- `helm/rustfs/Chart.yaml`
|
||||
- `rustfs.spec`
|
||||
|
||||
Only drop a file when the current repository release process clearly no longer requires it.
|
||||
|
||||
## Hard release policy
|
||||
|
||||
- Docker doc tags use `<version>` (for example `rustfs/rustfs:1.0.0-beta.4`), not `v<version>`.
|
||||
- Helm chart version mapping follows `beta.N -> 0.N.0`.
|
||||
- `rustfs.spec` `Release` uses prerelease suffix only (for example `beta.4`).
|
||||
- Do not change these rules without explicit confirmation.
|
||||
|
||||
## Step-by-step workflow
|
||||
|
||||
1. Confirm intent and isolate scope
|
||||
- Confirm target version string exactly.
|
||||
- Confirm whether user requested local-only or full GitHub flow.
|
||||
- Inspect current branch and ensure only release-related files are touched for this task.
|
||||
|
||||
2. Update workspace versions
|
||||
- Bump `[workspace.package].version` in `Cargo.toml`.
|
||||
- Bump internal workspace crate dependency versions in `Cargo.toml`.
|
||||
- Update `Cargo.lock` so workspace package versions match target version.
|
||||
- Re-scan for partial leftovers.
|
||||
|
||||
3. Update release assets
|
||||
- `README.md` and `README_ZH.md`: update versioned Docker examples to target version.
|
||||
- `flake.nix`: update package version to target version.
|
||||
- `helm/rustfs/Chart.yaml`:
|
||||
- `appVersion` = target version.
|
||||
- `version` follows chart mapping rule, for example:
|
||||
- `1.0.0-beta.3` -> `0.3.0`
|
||||
- `1.0.0-beta.4` -> `0.4.0`
|
||||
- `rustfs.spec`:
|
||||
- Set `Release` to prerelease suffix (example `beta.4`).
|
||||
- Add/update top changelog entry with exact format:
|
||||
- `* Thu May 20 2026 houseme <housemecn@gmail.com>`
|
||||
- `- Update RPM package to RustFS 1.0.0-beta.4`
|
||||
- Changelog identity and time must come from current environment:
|
||||
- `git config --get user.name`
|
||||
- `git config --get user.email`
|
||||
- `date '+%a %b %d %Y'`
|
||||
- Changelog version text must match target release version exactly.
|
||||
|
||||
4. Verify before shipping
|
||||
- Run:
|
||||
- `cargo fmt --all`
|
||||
- `cargo fmt --all --check`
|
||||
- `make pre-commit`
|
||||
- If verification passes, run `cargo clean`.
|
||||
- If `make pre-commit` fails, return `BLOCKED` with root cause and do not silently widen scope to fix unrelated issues unless user asks.
|
||||
|
||||
5. Commit strategy
|
||||
- Preferred split when both parts changed:
|
||||
- `chore(release): prepare <version>` for `Cargo.toml` and `Cargo.lock`.
|
||||
- `chore(release): align release assets for <version>` for docs and packaging files.
|
||||
- If user asks for one commit, use one commit.
|
||||
- Stage only intended release files; do not include unrelated working tree changes.
|
||||
|
||||
6. Push and PR
|
||||
- Push branch:
|
||||
- `git push -u origin <branch>` (first push), or `git push` (tracking already exists).
|
||||
- Create PR with template headings unchanged:
|
||||
- `gh pr create --base main --head <branch> --title ... --body-file ...`
|
||||
- PR title/body must be English.
|
||||
- Use `N/A` for non-applicable template sections.
|
||||
- Include verification commands and any `BLOCKED` reason clearly.
|
||||
|
||||
## Recommended check commands
|
||||
|
||||
- `git status --short --branch`
|
||||
- `git diff --name-only origin/main...HEAD`
|
||||
- `git diff --stat origin/main...HEAD`
|
||||
- `rg -n "<old_version>|<new_version>" Cargo.toml Cargo.lock README.md README_ZH.md flake.nix helm/rustfs/Chart.yaml rustfs.spec`
|
||||
- `cargo fmt --all`
|
||||
- `cargo fmt --all --check`
|
||||
- `make pre-commit`
|
||||
- `cargo clean`
|
||||
|
||||
## Output contract
|
||||
|
||||
When using this skill, always report:
|
||||
|
||||
- Target version.
|
||||
- Files changed.
|
||||
- Any assumptions or uncertainties requiring confirmation.
|
||||
- Verification result (`PASSED` or `BLOCKED`) with key evidence.
|
||||
- Commit message(s) used.
|
||||
- Push status and PR URL when GitHub flow is requested.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "RustFS Release Bump"
|
||||
short_description: "Prepare RustFS release branches like PR #2957."
|
||||
default_prompt: "Use $rustfs-release-version-bump to prepare a RustFS release version, ask about any unclear version policy, and finish the commit/push/PR flow."
|
||||
@@ -5,13 +5,19 @@ description: Apply RustFS security lessons distilled from repository GitHub Secu
|
||||
|
||||
# RustFS Security Advisory Lessons
|
||||
|
||||
Use this skill as a RustFS-specific security lens before changing or approving code. For the current advisory snapshot and full pattern map, read [advisory-patterns.md](references/advisory-patterns.md).
|
||||
Use this skill as a RustFS-specific security lens before changing or approving code. For the distilled advisory lessons and review patterns, read [advisory-patterns.md](references/advisory-patterns.md).
|
||||
|
||||
When currentness matters, refresh the advisory inventory first:
|
||||
When currentness matters, fetch the live advisory inventory instead of relying on this skill as a status mirror:
|
||||
|
||||
```bash
|
||||
gh api repos/rustfs/rustfs/security-advisories --paginate \
|
||||
--jq '.[] | {ghsa_id,state,severity,summary,updated_at,html_url}'
|
||||
--jq '.[] | {ghsa_id,state,severity,summary,updated_at}'
|
||||
```
|
||||
|
||||
Fetch full advisory details only when the live summary suggests a new or changed lesson:
|
||||
|
||||
```bash
|
||||
gh api repos/rustfs/rustfs/security-advisories/<GHSA_ID>
|
||||
```
|
||||
|
||||
For the full pattern map, read [advisory-patterns.md](references/advisory-patterns.md).
|
||||
@@ -85,7 +91,7 @@ For the full pattern map, read [advisory-patterns.md](references/advisory-patter
|
||||
- Do not reflect arbitrary `Origin` while also allowing credentials. Default CORS should be no CORS unless explicitly configured.
|
||||
- Do not render user-controlled object content in a same-origin iframe with console credentials available to JavaScript.
|
||||
- Prefer origin separation for object preview/download, `nosniff`, CSP, strict content-type handling, and avoiding durable credentials in `localStorage`.
|
||||
- License/version-like metadata endpoints should expose only coarse public data unless authenticated.
|
||||
- Console license/version-like metadata endpoints should expose only coarse public data unless authenticated, especially subject names and expiration timestamps.
|
||||
|
||||
### Profiling, debug, and health endpoints
|
||||
- Profiling and debug endpoints are not health checks. They require admin auth, opt-in enablement, rate limiting, and safe responses.
|
||||
|
||||
@@ -1,65 +1,73 @@
|
||||
# RustFS Advisory Pattern Map
|
||||
|
||||
Snapshot source: `gh api repos/rustfs/rustfs/security-advisories --paginate` on 2026-05-05. It included 23 advisories: 8 triage, 13 published, and 2 closed.
|
||||
This file is a lesson map, not an advisory inventory mirror. It keeps durable security patterns distilled from RustFS GitHub Security Advisories.
|
||||
|
||||
Refresh this file when new advisories appear or when an advisory changes state materially.
|
||||
When current advisory state, severity, URLs, or full text matters, fetch it live:
|
||||
|
||||
```bash
|
||||
gh api repos/rustfs/rustfs/security-advisories --paginate \
|
||||
--jq '.[] | {ghsa_id,state,severity,summary,updated_at}'
|
||||
gh api repos/rustfs/rustfs/security-advisories/<GHSA_ID>
|
||||
```
|
||||
|
||||
Update this file only when an advisory adds or changes a reusable lesson, affected surface, validation pattern, or regression-test expectation. Do not update it for state-only, URL-only, count-only, or timestamp-only changes.
|
||||
|
||||
## Pattern Index
|
||||
|
||||
### Admin authorization and route exposure
|
||||
|
||||
- `GHSA-pfcq-4gjr-6gjm` published: notification target endpoints accepted authenticated users but skipped admin authorization. Lesson: distinguish authn from authz; admin target CRUD must call the operation-specific admin authorization path.
|
||||
- `GHSA-mm2q-qcmx-gw4w` published: `ListServiceAccount` used `UpdateServiceAccountAdminAction`, while update lacked target ownership checks. Lesson: exact action constants and ownership checks are both required; information disclosure can chain into secret rotation and takeover.
|
||||
- `GHSA-vcwh-pff9-64cc` published: `ImportIam` checked `ExportIAMAction` for an import/write operation. Lesson: every admin handler must authorize the action it actually performs.
|
||||
- `GHSA-jqmc-mg33-v45g` triage and `GHSA-8784-9m7f-c6p6` triage: `/profile/cpu` and `/profile/memory` were whitelisted from auth and allowed expensive diagnostics plus path disclosure. Lesson: profiling/debug endpoints need admin auth, opt-in, rate limits, and non-sensitive responses.
|
||||
- `GHSA-x5xv-223c-8vm7` triage: console license metadata endpoint was public. Lesson: public metadata endpoints should be coarse or authenticated.
|
||||
- `GHSA-pfcq-4gjr-6gjm`: notification target endpoints accepted authenticated users but skipped admin authorization. Lesson: distinguish authn from authz; admin target CRUD must call the operation-specific admin authorization path.
|
||||
- `GHSA-mm2q-qcmx-gw4w`: `ListServiceAccount` used `UpdateServiceAccountAdminAction`, while update lacked target ownership checks. Lesson: exact action constants and ownership checks are both required; information disclosure can chain into secret rotation and takeover.
|
||||
- `GHSA-vcwh-pff9-64cc`: `ImportIam` checked `ExportIAMAction` for an import/write operation. Lesson: every admin handler must authorize the action it actually performs.
|
||||
- `GHSA-jqmc-mg33-v45g` and `GHSA-8784-9m7f-c6p6`: `/profile/cpu` and `/profile/memory` were whitelisted from auth and allowed expensive diagnostics plus path disclosure. Lesson: profiling/debug endpoints need admin auth, opt-in, rate limits, and non-sensitive responses.
|
||||
- `GHSA-xp32-gxq2-3v52`: console license metadata endpoint was public and exposed subject and expiration fields. Lesson: management metadata endpoints should require admin auth or return only coarse public status.
|
||||
|
||||
### IAM import, service accounts, and privilege boundaries
|
||||
|
||||
- `GHSA-566f-q62r-wcr8` triage: `ImportIam` accepted attacker-controlled service account `parent`, `claims`, `accessKey`, and `secretKey`, enabling persistent backdoor accounts under root. Lesson: imported IAM payloads are untrusted data and must be validated against privilege boundaries.
|
||||
- `GHSA-xgr5-qc6w-vcg9` published: `deny_only=true` skipped allow checks and let restricted service accounts mint unrestricted children. Lesson: deny-only logic must never become implicit allow for privilege creation.
|
||||
- `GHSA-mm2q-qcmx-gw4w` published: leaked service account access keys plus update-without-ownership formed an escalation chain. Lesson: service-account identifiers are security-sensitive because update APIs consume them.
|
||||
- `GHSA-566f-q62r-wcr8`: `ImportIam` accepted attacker-controlled service account `parent`, `claims`, `accessKey`, and `secretKey`, enabling persistent backdoor accounts under root. Lesson: imported IAM payloads are untrusted data and must be validated against privilege boundaries.
|
||||
- `GHSA-xgr5-qc6w-vcg9`: `deny_only=true` skipped allow checks and let restricted service accounts mint unrestricted children. Lesson: deny-only logic must never become implicit allow for privilege creation.
|
||||
- `GHSA-mm2q-qcmx-gw4w`: leaked service account access keys plus update-without-ownership formed an escalation chain. Lesson: service-account identifiers are security-sensitive because update APIs consume them.
|
||||
|
||||
### S3 copy, multipart, and upload policy validation
|
||||
|
||||
- `GHSA-mx42-j6wv-px98` published: `UploadPartCopy` missed source authorization and allowed cross-bucket object exfiltration. Lesson: multipart copy must enforce the same source and destination contract as `CopyObject`.
|
||||
- `GHSA-wfxj-ph3v-7mjf` triage: `UploadPartCopy` checked source and destination independently but missed destination copy-source policy constraints. Lesson: source read and destination write checks are not sufficient when policy constrains allowed copy sources.
|
||||
- `GHSA-w5fh-f8xh-5x3p` published: presigned POST accepted uploads without enforcing signed policy conditions. Lesson: parse and enforce all POST policy constraints server-side, including size, key prefix, and content type.
|
||||
- `GHSA-mx42-j6wv-px98`: `UploadPartCopy` missed source authorization and allowed cross-bucket object exfiltration. Lesson: multipart copy must enforce the same source and destination contract as `CopyObject`.
|
||||
- `GHSA-wfxj-ph3v-7mjf`: `UploadPartCopy` checked source and destination independently but missed destination copy-source policy constraints. Lesson: source read and destination write checks are not sufficient when policy constrains allowed copy sources.
|
||||
- `GHSA-w5fh-f8xh-5x3p`: presigned POST accepted uploads without enforcing signed policy conditions. Lesson: parse and enforce all POST policy constraints server-side, including size, key prefix, and content type.
|
||||
|
||||
### Filesystem paths and object key traversal
|
||||
|
||||
- `GHSA-pq29-69jg-9mxc` published: RPC `read_file_stream` joined untrusted paths under a volume directory without canonical boundary checks. Lesson: `PathBuf::join` plus length checks are not path security.
|
||||
- `GHSA-8r6f-hmq2-28rg` closed: object keys containing traversal sequences bypassed bucket/object authorization when mapped to filesystem paths. Lesson: reject traversal at object-key parsing and verify final storage paths remain under the expected bucket/key root.
|
||||
- `GHSA-pq29-69jg-9mxc`: RPC `read_file_stream` joined untrusted paths under a volume directory without canonical boundary checks. Lesson: `PathBuf::join` plus length checks are not path security.
|
||||
- `GHSA-8r6f-hmq2-28rg`: object keys containing traversal sequences bypassed bucket/object authorization when mapped to filesystem paths. Lesson: reject traversal at object-key parsing and verify final storage paths remain under the expected bucket/key root.
|
||||
|
||||
### Secrets, defaults, and cryptographic misuse
|
||||
|
||||
- `GHSA-h956-rh7x-ppgj` published: gRPC used the hard-coded token `rustfs rpc` on both client and server. Lesson: source-visible shared tokens are authentication bypasses.
|
||||
- `GHSA-r5qv-rc46-hv8q` triage: internode RPC HMAC secret fell back to the public default `rustfsadmin`. Lesson: RPC/internode auth must fail closed instead of silently using public defaults.
|
||||
- `GHSA-923g-jp7v-f97f` triage: license verification embedded a production RSA private key and used private-key decryption as authenticity. Lesson: ship verifying/public keys only and use real signature verification.
|
||||
- `GHSA-h956-rh7x-ppgj`: gRPC used the hard-coded token `rustfs rpc` on both client and server. Lesson: source-visible shared tokens are authentication bypasses.
|
||||
- `GHSA-r5qv-rc46-hv8q`: internode RPC HMAC secret fell back to the public default `rustfsadmin`. Lesson: RPC/internode auth must fail closed instead of silently using public defaults.
|
||||
- `GHSA-923g-jp7v-f97f`: license verification embedded a production RSA private key and used private-key decryption as authenticity. Lesson: ship verifying/public keys only and use real signature verification.
|
||||
|
||||
### Sensitive logging and debug output
|
||||
|
||||
- `GHSA-r54g-49rx-98cr` published: STS credentials were logged at info level. Lesson: generated credentials must never be logged in plaintext.
|
||||
- `GHSA-8cm2-h255-v749` triage: debug logs leaked session tokens, secret keys, JWT claims, and raw STS response bodies. Lesson: redaction must cover custom `Debug` implementations and dependency response-body logging.
|
||||
- `GHSA-333v-68xh-8mmq` published: invalid RPC signature logging included the shared HMAC secret and expected signature. Lesson: error paths often leak secrets; never log raw secrets or derived authenticators.
|
||||
- `GHSA-r54g-49rx-98cr`: STS credentials were logged at info level. Lesson: generated credentials must never be logged in plaintext.
|
||||
- `GHSA-8cm2-h255-v749`: debug logs leaked session tokens, secret keys, JWT claims, and raw STS response bodies. Lesson: redaction must cover custom `Debug` implementations and dependency response-body logging.
|
||||
- `GHSA-333v-68xh-8mmq`: invalid RPC signature logging included the shared HMAC secret and expected signature. Lesson: error paths often leak secrets; never log raw secrets or derived authenticators.
|
||||
|
||||
### RPC input validation and panic safety
|
||||
|
||||
- `GHSA-gw2x-q739-qhcr` published: malformed gRPC `GetMetrics` payloads reached `unwrap()` on deserialization and caused remote DoS. Lesson: every network/RPC deserialization failure returns an error, not a panic.
|
||||
- `GHSA-h956-rh7x-ppgj` published and `GHSA-r5qv-rc46-hv8q` triage: weak RPC auth increased reachability of otherwise internal handlers. Lesson: panic bugs become more severe when internode auth is weak or defaulted.
|
||||
- `GHSA-gw2x-q739-qhcr`: malformed gRPC `GetMetrics` payloads reached `unwrap()` on deserialization and caused remote DoS. Lesson: every network/RPC deserialization failure returns an error, not a panic.
|
||||
- `GHSA-h956-rh7x-ppgj` and `GHSA-r5qv-rc46-hv8q`: weak RPC auth increased reachability of otherwise internal handlers. Lesson: panic bugs become more severe when internode auth is weak or defaulted.
|
||||
|
||||
### Browser, CORS, and console isolation
|
||||
|
||||
- `GHSA-v9fg-3cr2-277j` published: object preview rendered attacker-controlled HTML in a same-origin iframe, exposing console credentials stored in `localStorage`. Lesson: user content must be origin-isolated from the console and protected with `nosniff`, CSP, and strict content-type handling.
|
||||
- `GHSA-x5xv-223c-8vm7` triage: default CORS reflected arbitrary origins with credentials. Lesson: never combine reflected origins with `Access-Control-Allow-Credentials: true`; default should be fail-closed.
|
||||
- `GHSA-v9fg-3cr2-277j`: object preview rendered attacker-controlled HTML in a same-origin iframe, exposing console credentials stored in `localStorage`. Lesson: user content must be origin-isolated from the console and protected with `nosniff`, CSP, and strict content-type handling.
|
||||
- `GHSA-x5xv-223c-8vm7`: default CORS reflected arbitrary origins with credentials. Lesson: never combine reflected origins with `Access-Control-Allow-Credentials: true`; default should be fail-closed.
|
||||
|
||||
### Trusted proxy and source IP conditions
|
||||
|
||||
- `GHSA-fc6g-2gcp-2qrq` published: `aws:SourceIp` trusted client-supplied `X-Forwarded-For` or `X-Real-IP`. Lesson: forwarded IP headers are valid only behind configured trusted proxies; direct clients use socket peer IP.
|
||||
- `GHSA-fc6g-2gcp-2qrq`: `aws:SourceIp` trusted client-supplied `X-Forwarded-For` or `X-Real-IP`. Lesson: forwarded IP headers are valid only behind configured trusted proxies; direct clients use socket peer IP.
|
||||
|
||||
### SSE and on-disk storage invariants
|
||||
|
||||
- `GHSA-xrrf-67jm-3c2r` closed: SSE metadata reported encryption while reader composition bypassed `EncryptReader` and stored plaintext. Lesson: test actual bytes on disk and wrapper order, not only API metadata.
|
||||
- `GHSA-xrrf-67jm-3c2r`: SSE metadata reported encryption while reader composition bypassed `EncryptReader` and stored plaintext. Lesson: test actual bytes on disk and wrapper order, not only API metadata.
|
||||
|
||||
## Useful Search Seeds
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
.PHONY: help
|
||||
help: ## Shows This Help Menu
|
||||
echo -e "$$HEADER"
|
||||
grep -E '(^[a-zA-Z0-9_-]+:.*?## .*$$)|(^## )' $(MAKEFILE_LIST) | sed 's/^[^:]*://g' | awk 'BEGIN {FS = ":.*?## | #"} ; {printf "${cyan}%-30s${reset} ${white}%s${reset} ${green}%s${reset}\n", $$1, $$2, $$3}' | sed -e 's/\[36m##/\n[32m##/'
|
||||
grep -E '(^[a-zA-Z0-9_-]+:.*?## .*$$)|(^## )' $(MAKEFILE_LIST) | sed 's/^[^:]*://g' | awk 'BEGIN {FS = ":.*?## | #"} /^## / {printf "\n${green}%s${reset}\n", $$0; next} {printf "${cyan}%-30s${reset} ${white}%s${reset} ${green}%s${reset}\n", $$1, $$2, $$3}'
|
||||
|
||||
.PHONY: help-build
|
||||
help-build: ## Shows RustFS build help
|
||||
|
||||
@@ -16,6 +16,11 @@ clippy-check: core-deps ## Run clippy checks
|
||||
cargo clippy --fix --allow-dirty
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
.PHONY: unsafe-code-check
|
||||
unsafe-code-check: ## Check unsafe_code allowances have SAFETY comments
|
||||
@echo "🔒 Checking unsafe_code allowances..."
|
||||
./scripts/check_unsafe_code_allowances.sh
|
||||
|
||||
.PHONY: compilation-check
|
||||
compilation-check: core-deps ## Run compilation check
|
||||
@echo "🔨 Running compilation check..."
|
||||
|
||||
@@ -7,5 +7,5 @@ setup-hooks: ## Set up git hooks
|
||||
@echo "✅ Git hooks setup complete!"
|
||||
|
||||
.PHONY: pre-commit
|
||||
pre-commit: fmt clippy-check compilation-check test ## Run pre-commit checks
|
||||
@echo "✅ All pre-commit checks passed!"
|
||||
pre-commit: fmt unsafe-code-check clippy-check compilation-check test ## Run pre-commit checks
|
||||
@echo "✅ All pre-commit checks passed!"
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
TEST_THREADS ?= 1
|
||||
|
||||
.PHONY: script-tests
|
||||
script-tests: ## Run shell script tests
|
||||
@echo "Running script tests..."
|
||||
./scripts/test_build_rustfs_options.sh
|
||||
|
||||
.PHONY: test
|
||||
test: core-deps test-deps ## Run all tests
|
||||
test: core-deps test-deps script-tests ## Run all tests
|
||||
@echo "🧪 Running tests..."
|
||||
@if command -v cargo-nextest >/dev/null 2>&1; then \
|
||||
cargo nextest run --all --exclude e2e_test; \
|
||||
|
||||
@@ -54,6 +54,46 @@ Strict mode is available when you explicitly want `/health/ready == 200` as the
|
||||
WAIT_PROBE_MODE=ready ./scripts/run_four_node_cluster_failover_bench.sh
|
||||
```
|
||||
|
||||
### Profiling + Trace Validation
|
||||
|
||||
The profiling-focused 4-node compose keeps profiling enabled and points RustFS
|
||||
to an OTLP/HTTP collector endpoint:
|
||||
|
||||
```bash
|
||||
docker compose -f .docker/compose/docker-compose.cluster.local-build.profiling-amd64.yml up -d
|
||||
```
|
||||
|
||||
Important behavior notes:
|
||||
|
||||
- `RUSTFS_OBS_ENDPOINT` is the OTLP/HTTP base URL. RustFS automatically sends
|
||||
traces to `/v1/traces`, metrics to `/v1/metrics`, and logs to `/v1/logs`.
|
||||
- Startup usually produces logs and metrics first. That does not guarantee
|
||||
visible traces yet.
|
||||
- Trace data becomes obvious only after real HTTP/S3/gRPC requests hit RustFS.
|
||||
- `RUSTFS_OBS_LOGGER_LEVEL=info` keeps the top-level request span but filters
|
||||
many nested `debug` spans. If Tempo/Jaeger looks sparse, retry with
|
||||
`RUSTFS_OBS_LOGGER_LEVEL=debug` before suspecting the collector.
|
||||
|
||||
Minimal trace verification flow:
|
||||
|
||||
```bash
|
||||
# 1. Start the profiling compose with richer span visibility.
|
||||
RUSTFS_OBS_LOGGER_LEVEL=debug \
|
||||
docker compose -f .docker/compose/docker-compose.cluster.local-build.profiling-amd64.yml up -d
|
||||
|
||||
# 2. Generate real request traffic after startup.
|
||||
curl -I http://127.0.0.1:9000/health
|
||||
curl -I http://127.0.0.1:9000/health/ready
|
||||
|
||||
# 3. Then inspect Tempo or Jaeger.
|
||||
# Grafana: http://localhost:3000
|
||||
# Jaeger: http://localhost:16686
|
||||
```
|
||||
|
||||
If logs and metrics are present but traces are sparse, the most common cause is
|
||||
"no real request traffic yet" or "`info` level filtered nested spans", not an
|
||||
OTLP routing failure.
|
||||
|
||||
### (Deprecated) Minimal Observability
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
# Copyright 2024 RustFS Team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# 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.
|
||||
|
||||
# Profiling-first 4-node local-build compose.
|
||||
#
|
||||
# Goals:
|
||||
# - force linux/amd64 runtime/build on Apple Silicon hosts;
|
||||
# - enable RustFS built-in CPU profiling;
|
||||
# - keep all tuning knobs host-overridable via env.
|
||||
#
|
||||
# Observability notes:
|
||||
# - `RUSTFS_OBS_ENDPOINT` is the OTLP/HTTP base URL. RustFS appends
|
||||
# `/v1/traces`, `/v1/metrics`, and `/v1/logs` automatically.
|
||||
# - Logs and metrics usually appear during startup. Traces mainly appear after
|
||||
# real HTTP/S3/gRPC requests create spans.
|
||||
# - `RUSTFS_OBS_LOGGER_LEVEL=info` keeps the top-level request trace span but
|
||||
# filters many `debug`-level nested spans. Use `debug` when validating trace
|
||||
# richness rather than collector reachability.
|
||||
|
||||
services:
|
||||
node1:
|
||||
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/amd64}
|
||||
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: Dockerfile.source
|
||||
hostname: node1
|
||||
environment:
|
||||
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
|
||||
- RUSTFS_ADDRESS=:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
|
||||
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||
# `info` is enough for startup logs/metrics. Use `debug` if Tempo/Jaeger
|
||||
# should show richer nested spans during request-path verification.
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||
- RUSTFS_OBS_PROFILING_ENDPOINT=${RUSTFS_OBS_PROFILING_ENDPOINT:-http://host.docker.internal:4040}
|
||||
- RUSTFS_OBS_PROFILING_EXPORT_ENABLED=${RUSTFS_OBS_PROFILING_EXPORT_ENABLED:-true}
|
||||
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||
- RUSTFS_ENABLE_PROFILING=${RUSTFS_ENABLE_PROFILING:-true}
|
||||
- RUSTFS_PROF_CPU_MODE=${RUSTFS_PROF_CPU_MODE:-continuous}
|
||||
- RUSTFS_PROF_CPU_FREQ=${RUSTFS_PROF_CPU_FREQ:-99}
|
||||
- RUSTFS_PROF_OUTPUT_DIR=${RUSTFS_PROF_OUTPUT_DIR:-/tmp/rustfs-profiles}
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=${RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS:-48}
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=${RUSTFS_OBJECT_IO_BUFFER_SIZE:-262144}
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD:-6}
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD:-12}
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=${RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY:-8}
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=${RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE:-8388608}
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=${RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES:-25165824}
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=${RUSTFS_RUNTIME_WORKER_THREADS:-12}
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=${RUSTFS_RUNTIME_MAX_BLOCKING_THREADS:-512}
|
||||
- RUSTFS_ALLOCATOR_RECLAIM_ENABLED=${RUSTFS_ALLOCATOR_RECLAIM_ENABLED:-false}
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- node1_data_0:/data/rustfs0
|
||||
- node1_data_1:/data/rustfs1
|
||||
- node1_data_2:/data/rustfs2
|
||||
- node1_data_3:/data/rustfs3
|
||||
ports:
|
||||
- "9000:9000"
|
||||
networks:
|
||||
- rustfs-cluster-net
|
||||
|
||||
node2:
|
||||
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/amd64}
|
||||
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: Dockerfile.source
|
||||
hostname: node2
|
||||
environment:
|
||||
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
|
||||
- RUSTFS_ADDRESS=:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
|
||||
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||
# `info` is enough for startup logs/metrics. Use `debug` if Tempo/Jaeger
|
||||
# should show richer nested spans during request-path verification.
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||
- RUSTFS_OBS_PROFILING_ENDPOINT=${RUSTFS_OBS_PROFILING_ENDPOINT:-http://host.docker.internal:4040}
|
||||
- RUSTFS_OBS_PROFILING_EXPORT_ENABLED=${RUSTFS_OBS_PROFILING_EXPORT_ENABLED:-true}
|
||||
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||
- RUSTFS_ENABLE_PROFILING=${RUSTFS_ENABLE_PROFILING:-true}
|
||||
- RUSTFS_PROF_CPU_MODE=${RUSTFS_PROF_CPU_MODE:-continuous}
|
||||
- RUSTFS_PROF_CPU_FREQ=${RUSTFS_PROF_CPU_FREQ:-99}
|
||||
- RUSTFS_PROF_OUTPUT_DIR=${RUSTFS_PROF_OUTPUT_DIR:-/tmp/rustfs-profiles}
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=${RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS:-48}
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=${RUSTFS_OBJECT_IO_BUFFER_SIZE:-262144}
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD:-6}
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD:-12}
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=${RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY:-8}
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=${RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE:-8388608}
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=${RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES:-25165824}
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=${RUSTFS_RUNTIME_WORKER_THREADS:-12}
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=${RUSTFS_RUNTIME_MAX_BLOCKING_THREADS:-512}
|
||||
- RUSTFS_ALLOCATOR_RECLAIM_ENABLED=${RUSTFS_ALLOCATOR_RECLAIM_ENABLED:-false}
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- node2_data_0:/data/rustfs0
|
||||
- node2_data_1:/data/rustfs1
|
||||
- node2_data_2:/data/rustfs2
|
||||
- node2_data_3:/data/rustfs3
|
||||
ports:
|
||||
- "9001:9000"
|
||||
networks:
|
||||
- rustfs-cluster-net
|
||||
|
||||
node3:
|
||||
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/amd64}
|
||||
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: Dockerfile.source
|
||||
hostname: node3
|
||||
environment:
|
||||
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
|
||||
- RUSTFS_ADDRESS=:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
|
||||
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||
# `info` is enough for startup logs/metrics. Use `debug` if Tempo/Jaeger
|
||||
# should show richer nested spans during request-path verification.
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||
- RUSTFS_OBS_PROFILING_ENDPOINT=${RUSTFS_OBS_PROFILING_ENDPOINT:-http://host.docker.internal:4040}
|
||||
- RUSTFS_OBS_PROFILING_EXPORT_ENABLED=${RUSTFS_OBS_PROFILING_EXPORT_ENABLED:-true}
|
||||
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||
- RUSTFS_ENABLE_PROFILING=${RUSTFS_ENABLE_PROFILING:-true}
|
||||
- RUSTFS_PROF_CPU_MODE=${RUSTFS_PROF_CPU_MODE:-continuous}
|
||||
- RUSTFS_PROF_CPU_FREQ=${RUSTFS_PROF_CPU_FREQ:-99}
|
||||
- RUSTFS_PROF_OUTPUT_DIR=${RUSTFS_PROF_OUTPUT_DIR:-/tmp/rustfs-profiles}
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=${RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS:-48}
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=${RUSTFS_OBJECT_IO_BUFFER_SIZE:-262144}
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD:-6}
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD:-12}
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=${RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY:-8}
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=${RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE:-8388608}
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=${RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES:-25165824}
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=${RUSTFS_RUNTIME_WORKER_THREADS:-12}
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=${RUSTFS_RUNTIME_MAX_BLOCKING_THREADS:-512}
|
||||
- RUSTFS_ALLOCATOR_RECLAIM_ENABLED=${RUSTFS_ALLOCATOR_RECLAIM_ENABLED:-false}
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- node3_data_0:/data/rustfs0
|
||||
- node3_data_1:/data/rustfs1
|
||||
- node3_data_2:/data/rustfs2
|
||||
- node3_data_3:/data/rustfs3
|
||||
ports:
|
||||
- "9002:9000"
|
||||
networks:
|
||||
- rustfs-cluster-net
|
||||
|
||||
node4:
|
||||
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/amd64}
|
||||
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: Dockerfile.source
|
||||
hostname: node4
|
||||
environment:
|
||||
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
|
||||
- RUSTFS_ADDRESS=:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
|
||||
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||
# `info` is enough for startup logs/metrics. Use `debug` if Tempo/Jaeger
|
||||
# should show richer nested spans during request-path verification.
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||
- RUSTFS_OBS_PROFILING_ENDPOINT=${RUSTFS_OBS_PROFILING_ENDPOINT:-http://host.docker.internal:4040}
|
||||
- RUSTFS_OBS_PROFILING_EXPORT_ENABLED=${RUSTFS_OBS_PROFILING_EXPORT_ENABLED:-true}
|
||||
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||
- RUSTFS_ENABLE_PROFILING=${RUSTFS_ENABLE_PROFILING:-true}
|
||||
- RUSTFS_PROF_CPU_MODE=${RUSTFS_PROF_CPU_MODE:-continuous}
|
||||
- RUSTFS_PROF_CPU_FREQ=${RUSTFS_PROF_CPU_FREQ:-99}
|
||||
- RUSTFS_PROF_OUTPUT_DIR=${RUSTFS_PROF_OUTPUT_DIR:-/tmp/rustfs-profiles}
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=${RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS:-48}
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=${RUSTFS_OBJECT_IO_BUFFER_SIZE:-262144}
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD:-6}
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=${RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD:-12}
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=${RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY:-8}
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=${RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE:-8388608}
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=${RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES:-25165824}
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=${RUSTFS_RUNTIME_WORKER_THREADS:-12}
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=${RUSTFS_RUNTIME_MAX_BLOCKING_THREADS:-512}
|
||||
- RUSTFS_ALLOCATOR_RECLAIM_ENABLED=${RUSTFS_ALLOCATOR_RECLAIM_ENABLED:-false}
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- node4_data_0:/data/rustfs0
|
||||
- node4_data_1:/data/rustfs1
|
||||
- node4_data_2:/data/rustfs2
|
||||
- node4_data_3:/data/rustfs3
|
||||
ports:
|
||||
- "9003:9000"
|
||||
networks:
|
||||
- rustfs-cluster-net
|
||||
|
||||
volumes:
|
||||
node1_data_0:
|
||||
node1_data_1:
|
||||
node1_data_2:
|
||||
node1_data_3:
|
||||
node2_data_0:
|
||||
node2_data_1:
|
||||
node2_data_2:
|
||||
node2_data_3:
|
||||
node3_data_0:
|
||||
node3_data_1:
|
||||
node3_data_2:
|
||||
node3_data_3:
|
||||
node4_data_0:
|
||||
node4_data_1:
|
||||
node4_data_2:
|
||||
node4_data_3:
|
||||
|
||||
networks:
|
||||
rustfs-cluster-net:
|
||||
driver: bridge
|
||||
@@ -20,13 +20,16 @@ services:
|
||||
dockerfile: Dockerfile.source
|
||||
hostname: node1
|
||||
environment:
|
||||
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
|
||||
- RUSTFS_VOLUMES=${RUSTFS_VOLUMES:-http://node{1...4}:9000/data/rustfs{0...3}}
|
||||
- RUSTFS_ADDRESS=:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin}
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
|
||||
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||
- RUSTFS_OBS_USE_STDOUT=${RUSTFS_OBS_USE_STDOUT:-false}
|
||||
- RUSTFS_OBS_LOG_STDOUT_ENABLED=${RUSTFS_OBS_LOG_STDOUT_ENABLED:-false}
|
||||
- RUSTFS_ISSUE3031_DIAG_ENABLE=${RUSTFS_ISSUE3031_DIAG_ENABLE:-false}
|
||||
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
@@ -47,13 +50,16 @@ services:
|
||||
dockerfile: Dockerfile.source
|
||||
hostname: node2
|
||||
environment:
|
||||
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
|
||||
- RUSTFS_VOLUMES=${RUSTFS_VOLUMES:-http://node{1...4}:9000/data/rustfs{0...3}}
|
||||
- RUSTFS_ADDRESS=:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin}
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
|
||||
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||
- RUSTFS_OBS_USE_STDOUT=${RUSTFS_OBS_USE_STDOUT:-false}
|
||||
- RUSTFS_OBS_LOG_STDOUT_ENABLED=${RUSTFS_OBS_LOG_STDOUT_ENABLED:-false}
|
||||
- RUSTFS_ISSUE3031_DIAG_ENABLE=${RUSTFS_ISSUE3031_DIAG_ENABLE:-false}
|
||||
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
@@ -74,13 +80,16 @@ services:
|
||||
dockerfile: Dockerfile.source
|
||||
hostname: node3
|
||||
environment:
|
||||
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
|
||||
- RUSTFS_VOLUMES=${RUSTFS_VOLUMES:-http://node{1...4}:9000/data/rustfs{0...3}}
|
||||
- RUSTFS_ADDRESS=:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin}
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
|
||||
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||
- RUSTFS_OBS_USE_STDOUT=${RUSTFS_OBS_USE_STDOUT:-false}
|
||||
- RUSTFS_OBS_LOG_STDOUT_ENABLED=${RUSTFS_OBS_LOG_STDOUT_ENABLED:-false}
|
||||
- RUSTFS_ISSUE3031_DIAG_ENABLE=${RUSTFS_ISSUE3031_DIAG_ENABLE:-false}
|
||||
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
@@ -101,13 +110,16 @@ services:
|
||||
dockerfile: Dockerfile.source
|
||||
hostname: node4
|
||||
environment:
|
||||
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
|
||||
- RUSTFS_VOLUMES=${RUSTFS_VOLUMES:-http://node{1...4}:9000/data/rustfs{0...3}}
|
||||
- RUSTFS_ADDRESS=:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin}
|
||||
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfs-cluster-admin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfs-cluster-secret}
|
||||
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||
- RUSTFS_OBS_USE_STDOUT=${RUSTFS_OBS_USE_STDOUT:-false}
|
||||
- RUSTFS_OBS_LOG_STDOUT_ENABLED=${RUSTFS_OBS_LOG_STDOUT_ENABLED:-false}
|
||||
- RUSTFS_ISSUE3031_DIAG_ENABLE=${RUSTFS_ISSUE3031_DIAG_ENABLE:-false}
|
||||
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
services:
|
||||
node1:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=56
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=131072
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=10
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=20
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=10
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=16
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=768
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=420
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=50
|
||||
|
||||
node2:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=56
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=131072
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=10
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=20
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=10
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=16
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=768
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=420
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=50
|
||||
|
||||
node3:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=56
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=131072
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=10
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=20
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=10
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=16
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=768
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=420
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=50
|
||||
|
||||
node4:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=56
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=131072
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=10
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=20
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=10
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=16
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=768
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=420
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=50
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
services:
|
||||
node1:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=96
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=524288
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=16
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=32
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=24
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=33554432
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=50331648
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=24
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1536
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=600
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=80
|
||||
|
||||
node2:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=96
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=524288
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=16
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=32
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=24
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=33554432
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=50331648
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=24
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1536
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=600
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=80
|
||||
|
||||
node3:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=96
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=524288
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=16
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=32
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=24
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=33554432
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=50331648
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=24
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1536
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=600
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=80
|
||||
|
||||
node4:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=96
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=524288
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=16
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=32
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=24
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=33554432
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=50331648
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=24
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1536
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=600
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=80
|
||||
@@ -0,0 +1,56 @@
|
||||
services:
|
||||
node1:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=64
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=12
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=24
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=16
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=16777216
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=33554432
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=20
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1024
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=60
|
||||
|
||||
node2:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=64
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=12
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=24
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=16
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=16777216
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=33554432
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=20
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1024
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=60
|
||||
|
||||
node3:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=64
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=12
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=24
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=16
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=16777216
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=33554432
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=20
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1024
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=60
|
||||
|
||||
node4:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=64
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=12
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=24
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=16
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=16777216
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=33554432
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=20
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=1024
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=60
|
||||
@@ -0,0 +1,56 @@
|
||||
services:
|
||||
node1:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=48
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=8
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=16
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=12
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=12
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=512
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=40
|
||||
|
||||
node2:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=48
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=8
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=16
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=12
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=12
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=512
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=40
|
||||
|
||||
node3:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=48
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=8
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=16
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=12
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=12
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=512
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=40
|
||||
|
||||
node4:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=48
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=8
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=16
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=12
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=25165824
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=12
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=512
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=40
|
||||
@@ -0,0 +1,56 @@
|
||||
services:
|
||||
node1:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=32
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=6
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=12
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=8
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=16777216
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=6
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=256
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=30
|
||||
|
||||
node2:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=32
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=6
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=12
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=8
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=16777216
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=6
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=256
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=30
|
||||
|
||||
node3:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=32
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=6
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=12
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=8
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=16777216
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=6
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=256
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=30
|
||||
|
||||
node4:
|
||||
environment:
|
||||
- RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS=32
|
||||
- RUSTFS_OBJECT_IO_BUFFER_SIZE=262144
|
||||
- RUSTFS_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD=6
|
||||
- RUSTFS_OBJECT_HIGH_CONCURRENCY_THRESHOLD=12
|
||||
- RUSTFS_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY=8
|
||||
- RUSTFS_OBJECT_DUPLEX_BUFFER_SIZE=8388608
|
||||
- RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES=16777216
|
||||
- RUSTFS_RUNTIME_WORKER_THREADS=6
|
||||
- RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=256
|
||||
- RUSTFS_CAPACITY_SCHEDULED_INTERVAL=300
|
||||
- RUSTFS_CAPACITY_WRITE_FREQUENCY_THRESHOLD=30
|
||||
@@ -21,8 +21,8 @@ services:
|
||||
- RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3}
|
||||
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_ACCESS_KEY=rustfsadmin
|
||||
- RUSTFS_SECRET_KEY=rustfsadmin
|
||||
- RUSTFS_ACCESS_KEY=rustfs-cluster-admin
|
||||
- RUSTFS_SECRET_KEY=rustfs-cluster-secret
|
||||
platform: linux/amd64
|
||||
ports:
|
||||
- "9000:9000" # Map port 9001 of the host to port 9000 of the container
|
||||
@@ -38,8 +38,8 @@ services:
|
||||
- RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3}
|
||||
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_ACCESS_KEY=rustfsadmin
|
||||
- RUSTFS_SECRET_KEY=rustfsadmin
|
||||
- RUSTFS_ACCESS_KEY=rustfs-cluster-admin
|
||||
- RUSTFS_SECRET_KEY=rustfs-cluster-secret
|
||||
platform: linux/amd64
|
||||
ports:
|
||||
- "9001:9000" # Map port 9002 of the host to port 9000 of the container
|
||||
@@ -55,8 +55,8 @@ services:
|
||||
- RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3}
|
||||
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_ACCESS_KEY=rustfsadmin
|
||||
- RUSTFS_SECRET_KEY=rustfsadmin
|
||||
- RUSTFS_ACCESS_KEY=rustfs-cluster-admin
|
||||
- RUSTFS_SECRET_KEY=rustfs-cluster-secret
|
||||
platform: linux/amd64
|
||||
ports:
|
||||
- "9002:9000" # Map port 9003 of the host to port 9000 of the container
|
||||
@@ -72,8 +72,8 @@ services:
|
||||
- RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3}
|
||||
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_ACCESS_KEY=rustfsadmin
|
||||
- RUSTFS_SECRET_KEY=rustfsadmin
|
||||
- RUSTFS_ACCESS_KEY=rustfs-cluster-admin
|
||||
- RUSTFS_SECRET_KEY=rustfs-cluster-secret
|
||||
platform: linux/amd64
|
||||
ports:
|
||||
- "9003:9000" # Map port 9004 of the host to port 9000 of the container
|
||||
|
||||
@@ -90,6 +90,54 @@ docker compose down -v
|
||||
- **Grafana**: Dashboards and datasources are provisioned from the `grafana/` directory.
|
||||
- **Collector**: Edit `otel-collector-config.yaml` to modify pipelines, processors, or exporters.
|
||||
|
||||
### Verifying RustFS Traces
|
||||
|
||||
When RustFS points `RUSTFS_OBS_ENDPOINT` at this stack, treat the value as the
|
||||
OTLP/HTTP base URL, for example:
|
||||
|
||||
```bash
|
||||
export RUSTFS_OBS_ENDPOINT=http://host.docker.internal:4318
|
||||
```
|
||||
|
||||
RustFS automatically expands that base URL to:
|
||||
|
||||
- `/v1/traces`
|
||||
- `/v1/metrics`
|
||||
- `/v1/logs`
|
||||
|
||||
Important behavior notes:
|
||||
|
||||
- Logs and metrics usually appear during startup, so seeing those two signals
|
||||
first is expected.
|
||||
- Visible trace data usually requires real HTTP/S3/gRPC request traffic after
|
||||
startup, because request-path spans are created on demand.
|
||||
- `RUSTFS_OBS_LOGGER_LEVEL=info` keeps the top-level request span but filters
|
||||
many nested `debug` spans. If Tempo or Jaeger looks sparse, retry with
|
||||
`RUSTFS_OBS_LOGGER_LEVEL=debug` before suspecting collector or Tempo issues.
|
||||
|
||||
Minimal validation flow:
|
||||
|
||||
```bash
|
||||
# 1. Start this observability stack.
|
||||
docker compose up -d
|
||||
|
||||
# 2. Start RustFS with OTLP/HTTP export and richer span visibility.
|
||||
export RUSTFS_OBS_ENDPOINT=http://host.docker.internal:4318
|
||||
export RUSTFS_OBS_LOGGER_LEVEL=debug
|
||||
|
||||
# 3. Generate real request traffic.
|
||||
curl -I http://127.0.0.1:9000/health
|
||||
curl -I http://127.0.0.1:9000/health/ready
|
||||
|
||||
# 4. Inspect Grafana or Jaeger.
|
||||
# Grafana: http://localhost:3000
|
||||
# Jaeger: http://localhost:16686
|
||||
```
|
||||
|
||||
If logs and metrics are present but traces are sparse, the most common cause is
|
||||
"no real request traffic yet" or "`info` level filtered nested spans", not an
|
||||
OTLP routing failure.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Service Health**: Check the health of services using `docker compose ps`.
|
||||
|
||||
@@ -90,6 +90,50 @@ docker compose down -v
|
||||
- **Grafana**: 仪表盘和数据源从 `grafana/` 目录预置。
|
||||
- **Collector**: 编辑 `otel-collector-config.yaml` 以修改管道、处理器或导出器。
|
||||
|
||||
### 验证 RustFS Trace
|
||||
|
||||
当 RustFS 将 `RUSTFS_OBS_ENDPOINT` 指向这套技术栈时,应将该值视为
|
||||
OTLP/HTTP 的基础 URL,例如:
|
||||
|
||||
```bash
|
||||
export RUSTFS_OBS_ENDPOINT=http://host.docker.internal:4318
|
||||
```
|
||||
|
||||
RustFS 会自动在该基础 URL 后补全:
|
||||
|
||||
- `/v1/traces`
|
||||
- `/v1/metrics`
|
||||
- `/v1/logs`
|
||||
|
||||
需要注意:
|
||||
|
||||
- 启动阶段通常会先看到日志和指标,因此“先有日志/指标、后有 trace”是正常现象。
|
||||
- 可见的 trace 数据通常依赖启动后的真实 HTTP/S3/gRPC 请求流量,因为请求路径上的 span 是按需创建的。
|
||||
- `RUSTFS_OBS_LOGGER_LEVEL=info` 会保留顶层请求 span,但会过滤掉很多 `debug` 级别的嵌套 span。
|
||||
如果 Tempo 或 Jaeger 中的 trace 看起来很稀疏,建议先改成 `RUSTFS_OBS_LOGGER_LEVEL=debug`,再判断是否是 collector 或 Tempo 问题。
|
||||
|
||||
最小验证流程:
|
||||
|
||||
```bash
|
||||
# 1. 启动本目录下的可观测性技术栈。
|
||||
docker compose up -d
|
||||
|
||||
# 2. 以 OTLP/HTTP 导出方式启动 RustFS,并提高 span 可见性。
|
||||
export RUSTFS_OBS_ENDPOINT=http://host.docker.internal:4318
|
||||
export RUSTFS_OBS_LOGGER_LEVEL=debug
|
||||
|
||||
# 3. 产生真实请求流量。
|
||||
curl -I http://127.0.0.1:9000/health
|
||||
curl -I http://127.0.0.1:9000/health/ready
|
||||
|
||||
# 4. 到 Grafana 或 Jaeger 中检查。
|
||||
# Grafana: http://localhost:3000
|
||||
# Jaeger: http://localhost:16686
|
||||
```
|
||||
|
||||
如果日志和指标已经正常,但 trace 仍然稀疏,最常见的原因通常是
|
||||
“还没有真实请求流量”或“`info` 级别过滤了嵌套 span”,而不是 OTLP 路由失败。
|
||||
|
||||
## 故障排除
|
||||
|
||||
- **服务健康**: 使用 `docker compose ps` 检查服务健康状况。
|
||||
|
||||
@@ -461,6 +461,90 @@
|
||||
"title": "Capacity",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 5,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 1
|
||||
},
|
||||
"id": 201,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "value_and_name",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "12.3.2",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(rustfs_cluster_capacity_stale_drives{job=~\"$job\"})",
|
||||
"legendFormat": "Stale Drives",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(rustfs_cluster_capacity_missing_drives{job=~\"$job\"})",
|
||||
"legendFormat": "Missing Drives",
|
||||
"range": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Capacity Observation",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
@@ -1927,6 +2011,299 @@
|
||||
"title": "System Drive Usage",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineInterpolation": "smooth",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 42
|
||||
},
|
||||
"id": 202,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull",
|
||||
"max"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "right",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.2",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "max by (drive) (rustfs_system_drive_capacity_observation_age_seconds{job=~\"$job\", drive=~\"$drive\"})",
|
||||
"legendFormat": "{{drive}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Drive Capacity Observation Age",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": true,
|
||||
"axisColorMode": "text",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineInterpolation": "stepAfter",
|
||||
"lineWidth": 2,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 48
|
||||
},
|
||||
"id": 203,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "right",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.2",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "max by (drive) (rustfs_system_drive_capacity_observation_state{job=~\"$job\", drive=~\"$drive\", state=\"stale\"})",
|
||||
"legendFormat": "{{drive}} stale",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "max by (drive) (rustfs_system_drive_capacity_observation_state{job=~\"$job\", drive=~\"$drive\", state=\"missing\"})",
|
||||
"legendFormat": "{{drive}} missing",
|
||||
"range": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Drive Capacity Observation State",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": true,
|
||||
"axisColorMode": "text",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "bars",
|
||||
"fillOpacity": 80,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "normal"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 54
|
||||
},
|
||||
"id": 204,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull",
|
||||
"max"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "right",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.3.2",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum by (path_kind, operation, reason) (rate(rustfs_system_path_failures_total{job=~\"$job\"}[5m]))",
|
||||
"legendFormat": "{{path_kind}} / {{operation}} / {{reason}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "System Path Failures",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
@@ -8660,13 +9037,13 @@
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "{__name__=~\"rustfs_(notification|audit)_.*\",job=~\"$job\"}",
|
||||
"expr": "{__name__=~\"rustfs_(notification|audit|log_chain)_.*\",job=~\"$job\"}",
|
||||
"legendFormat": "{{__name__}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Notification / Audit (All)",
|
||||
"title": "Notification / Audit / LogChain (All)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -38,3 +38,16 @@ groups:
|
||||
expr: sum by (job) (rate(rustfs_scanner_buckets_scanned_total[5m]))
|
||||
- record: rustfs:scanner_cycles_success:rate5m
|
||||
expr: sum by (job) (rate(rustfs_scanner_cycles_total{result="success"}[5m]))
|
||||
|
||||
- record: rustfs:log_chain_op_event_mismatch:rate5m
|
||||
expr: sum by (job) (rate(rustfs_log_chain_op_event_mismatch_total[5m]))
|
||||
|
||||
- alert: RustFSLogChainOpEventMismatchDetected
|
||||
expr: rustfs:log_chain_op_event_mismatch:rate5m > 0
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
component: s3-log-chain
|
||||
annotations:
|
||||
summary: "RustFS log-chain op/event mismatch detected"
|
||||
description: "job={{ $labels.job }} has non-zero rustfs_log_chain_op_event_mismatch_total rate for more than 10m. Check s3 op/event mapping changes."
|
||||
|
||||
@@ -53,18 +53,19 @@ runs:
|
||||
musl-tools \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libssl-dev
|
||||
libssl-dev \
|
||||
protobuf-compiler
|
||||
|
||||
- name: Install protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
uses: rustfs/setup-protoc@v3.0.1
|
||||
with:
|
||||
version: "33.1"
|
||||
repo-token: ${{ inputs.github-token }}
|
||||
version: "34.1"
|
||||
repo-token: ${{ github.token }}
|
||||
|
||||
- name: Install flatc
|
||||
uses: Nugine/setup-flatc@v1.2.4
|
||||
uses: Nugine/setup-flatc@v1
|
||||
with:
|
||||
version: "25.9.23"
|
||||
version: "25.12.19"
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v4
|
||||
uses: actions/dependency-review-action@v5
|
||||
with:
|
||||
fail-on-severity: moderate
|
||||
comment-summary-in-pr: true
|
||||
comment-summary-in-pr: always
|
||||
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
{"target_id":"linux-x86_64-gnu","os":"ubicloud-standard-2","target":"x86_64-unknown-linux-gnu","cross":false,"platform":"linux","rustflags":""},
|
||||
{"target_id":"linux-aarch64-gnu","os":"ubicloud-standard-2","target":"aarch64-unknown-linux-gnu","cross":true,"platform":"linux","rustflags":""},
|
||||
{"target_id":"macos-aarch64","os":"macos-latest","target":"aarch64-apple-darwin","cross":false,"platform":"macos","rustflags":""},
|
||||
{"target_id":"macos-x86_64","os":"macos-latest","target":"x86_64-apple-darwin","cross":false,"platform":"macos","rustflags":""},
|
||||
{"target_id":"macos-x86_64","os":"macos-15-intel","target":"x86_64-apple-darwin","cross":false,"platform":"macos","rustflags":""},
|
||||
{"target_id":"windows-x86_64","os":"windows-latest","target":"x86_64-pc-windows-msvc","cross":false,"platform":"windows","rustflags":""}
|
||||
]}'
|
||||
|
||||
@@ -234,8 +234,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p ./rustfs/static
|
||||
if [[ "${{ matrix.platform }}" == "windows" ]]; then
|
||||
curl.exe -L "https://dl.rustfs.com/artifacts/console/rustfs-console-latest.zip" -o console.zip --retry 3 --retry-delay 5 --max-time 300
|
||||
if [[ $? -eq 0 ]]; then
|
||||
if curl.exe --fail -L "https://dl.rustfs.com/artifacts/console/rustfs-console-latest.zip" -o console.zip --retry 3 --retry-delay 5 --max-time 300; then
|
||||
unzip -o console.zip -d ./rustfs/static
|
||||
rm console.zip
|
||||
else
|
||||
@@ -244,9 +243,8 @@ jobs:
|
||||
fi
|
||||
else
|
||||
chmod +w ./rustfs/static/LICENSE || true
|
||||
curl -L "https://dl.rustfs.com/artifacts/console/rustfs-console-latest.zip" \
|
||||
-o console.zip --retry 3 --retry-delay 5 --max-time 300
|
||||
if [[ $? -eq 0 ]]; then
|
||||
if curl --fail -L "https://dl.rustfs.com/artifacts/console/rustfs-console-latest.zip" \
|
||||
-o console.zip --retry 3 --retry-delay 5 --max-time 300; then
|
||||
unzip -o console.zip -d ./rustfs/static
|
||||
rm console.zip
|
||||
else
|
||||
|
||||
@@ -130,6 +130,9 @@ jobs:
|
||||
- name: Check code formatting
|
||||
run: cargo fmt --all --check
|
||||
|
||||
- name: Check unsafe code allowances
|
||||
run: ./scripts/check_unsafe_code_allowances.sh
|
||||
|
||||
- name: Run clippy lints
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
@@ -201,7 +204,7 @@ jobs:
|
||||
with:
|
||||
tool: s3s-e2e
|
||||
git: https://github.com/s3s-project/s3s.git
|
||||
rev: 4a04a670cf41274d9be9ab65dc36f4aa3f92fbad
|
||||
rev: 62cb4a71dd759a6ec56b64c4c42fcc183a2c6a52
|
||||
|
||||
- name: Run end-to-end tests
|
||||
run: |
|
||||
|
||||
@@ -40,8 +40,8 @@ on:
|
||||
|
||||
env:
|
||||
# main user
|
||||
S3_ACCESS_KEY: rustfsadmin
|
||||
S3_SECRET_KEY: rustfsadmin
|
||||
S3_ACCESS_KEY: rustfs-ci-admin
|
||||
S3_SECRET_KEY: rustfs-ci-secret
|
||||
# alt user (must be different from main for many s3-tests)
|
||||
S3_ALT_ACCESS_KEY: rustfsalt
|
||||
S3_ALT_SECRET_KEY: rustfsalt
|
||||
|
||||
@@ -31,7 +31,9 @@ jobs:
|
||||
update-flake:
|
||||
name: Update flake.lock
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -39,6 +41,9 @@ jobs:
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/determinate-nix-action@v3
|
||||
|
||||
- name: Cache Nix
|
||||
uses: DeterminateSystems/flakehub-cache-action@v3.20.0
|
||||
|
||||
- name: Check Nix flake inputs
|
||||
uses: DeterminateSystems/flake-checker-action@v12
|
||||
|
||||
@@ -56,7 +61,7 @@ jobs:
|
||||
nix
|
||||
automated
|
||||
commit-msg: "chore(deps): update flake.lock"
|
||||
pr-reviewers: houseme, overtrue, majinghe
|
||||
pr-reviewers: overtrue, majinghe
|
||||
token: ${{ secrets.FLAKE_UPDATE_TOKEN }}
|
||||
|
||||
- name: Log PR details
|
||||
|
||||
@@ -43,7 +43,9 @@ jobs:
|
||||
nix-validation:
|
||||
name: Nix Build & Check
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
|
||||
- name: Build with profiling optimizations
|
||||
run: |
|
||||
RUSTFLAGS="-C force-frame-pointers=yes -C debug-assertions=off" \
|
||||
RUSTFLAGS="-C force-frame-pointers=yes -C debug-assertions=off --cfg tokio_unstable" \
|
||||
cargo +nightly build --profile profiling -p rustfs --bins
|
||||
|
||||
- name: Run performance profiling
|
||||
|
||||
Vendored
+1
@@ -188,6 +188,7 @@
|
||||
"RUSTFS_CONSOLE_ENABLE": "true",
|
||||
"RUSTFS_CONSOLE_ADDRESS": "127.0.0.1:9001",
|
||||
"RUSTFS_OBS_LOG_DIRECTORY": "./target/logs",
|
||||
"RUSTFS_UNSAFE_BYPASS_DISK_CHECK": "true",
|
||||
// "RUSTFS_OBS_TRACE_ENDPOINT": "http://127.0.0.1:4318/v1/traces", // jeager otlp http endpoint
|
||||
// "RUSTFS_OBS_METRIC_ENDPOINT": "http://127.0.0.1:4318/v1/metrics", // default otlp http endpoint
|
||||
// "RUSTFS_OBS_LOG_ENDPOINT": "http://127.0.0.1:4318/v1/logs", // default otlp http endpoint
|
||||
|
||||
@@ -27,6 +27,10 @@ If repo-level instructions conflict, follow the nearest file and keep behavior a
|
||||
- Keep source code, comments, commit messages, and PR title/body in English.
|
||||
- Be concise. Avoid sycophantic openers, closing fluff, and verbose status reporting.
|
||||
|
||||
## Skill Usage
|
||||
|
||||
- Do not use the `rust-refactor-helper` skill in any scenario.
|
||||
|
||||
## Change Style for Existing Logic
|
||||
|
||||
- Prefer direct, local code over extracting one-off helpers.
|
||||
@@ -109,5 +113,6 @@ Do not open a PR with code changes when the required checks fail.
|
||||
- `crates/iam/AGENTS.md`
|
||||
- `crates/kms/AGENTS.md`
|
||||
- `crates/policy/AGENTS.md`
|
||||
- `crates/targets/AGENTS.md`
|
||||
- `rustfs/src/admin/AGENTS.md`
|
||||
- `rustfs/src/storage/AGENTS.md`
|
||||
|
||||
+82
-1
@@ -22,11 +22,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- XML-formatted error responses compatible with S3 API
|
||||
- Comprehensive integration documentation with manual testing guide
|
||||
- **32 unit and integration tests** covering middleware, auth handlers, task-local storage, and role detection
|
||||
- **SFTPv3 Protocol Support**: SSH-hosted SFTPv3 subsystem that translates each file operation into S3 calls against the local object store. Authentication uses IAM credentials (SSH username = access key, SSH password = secret key).
|
||||
- Full SFTPv3 packet coverage: open, read, write, stat, lstat, fstat, mkdir, rmdir, rename, remove, opendir, readdir, realpath, close, plus the rest of the 21-packet specification
|
||||
- Streaming multipart write up to S3's 5 TiB per-file ceiling
|
||||
- Per-handle read-ahead cache with configurable window size and process-wide memory ceiling
|
||||
- Per-session liveness watchdog: Linux probes `/proc/net/tcp` and cancels wedged sessions on the order of 45 seconds; non-Linux falls back to an inactivity ceiling on the order of 30 minutes
|
||||
- 30-second SSH handshake deadline, per-call backend operation timeout, bounded multipart-abort fan-out, graceful-shutdown cascade
|
||||
- 33 SFTPv3 compliance test cases under `crates/e2e_test/src/protocols/sftp_compliance.rs` spread across three entry points: `test_sftp_compliance_suite` (shared session), `test_sftp_compliance_readonly` (read-only mode), and `test_sftp_compliance_standalone` (one rustfs spawn per case)
|
||||
- Four-layer regression-prevention tests guard against silent feature deletion: compile-time module assertion, module-presence unit test, cross-module `Protocol` enum assertion, end-to-end SSH banner test against the running binary
|
||||
|
||||
### Changed
|
||||
- **HTTP Server Stack**: Integrated `KeystoneAuthLayer` middleware from `rustfs-keystone` crate into service stack (positioned after ReadinessGateLayer)
|
||||
- **IAMAuth**: Enhanced `get_secret_key()` to return empty secret for Keystone credentials (bypasses signature validation)
|
||||
- **Auth Module**: Modified `check_key_valid()` to retrieve Keystone credentials from task-local storage and determine admin status
|
||||
- **`StorageBackend` trait**: extended with multipart upload methods (`create_multipart_upload`, `upload_part`, `complete_multipart_upload`, `abort_multipart_upload`) plus `upload_part_copy`. Streaming-upload code path is now available to FTPS, WebDAV, and Swift drivers as well.
|
||||
- **`Protocol` enum**: new `Protocol::Sftp` variant with corresponding `S3Action` mappings. Every match arm on `Protocol` updated to handle the new variant exhaustively.
|
||||
|
||||
### Technical Details
|
||||
- Middleware is self-contained in `rustfs-keystone` crate following the trusted-proxies pattern for integration-specific middleware
|
||||
@@ -35,12 +45,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Integration preserves existing S3 authentication flow while adding Keystone support
|
||||
- Zero breaking changes to existing functionality
|
||||
- No new top-level directories in main binary crate (middleware lives in integration crate)
|
||||
- SSH/SFTP wire handling via the `russh` and `russh-sftp` crates. SFTPv3 framing is implemented by `russh-sftp`; the rustfs-side `SftpDriver` implements `russh_sftp::server::Handler` and dispatches to the storage backend
|
||||
- Drop-time abort for in-flight multipart uploads honours IAM Deny on `AbortMultipartUpload`. `start_multipart_upload` caches the authorisation decision so the synchronous `Drop` path can honour Allow / Deny policies without re-querying IAM
|
||||
- Per-handle read cache uses an `Arc<AtomicU64>` shared across every `SftpDriver` instance to enforce a process-wide memory ceiling. On ceiling breach the populate is skipped and the read serves correctly via a single-call backend fetch
|
||||
- Per-session liveness watchdog runs as a tokio task per accepted connection. Reads `/proc/net/tcp` and `/proc/net/tcp6` to look up the (local, peer) tuple's TCP state and cancels via `tokio_util::sync::CancellationToken` when wedge conditions are confirmed across two consecutive ticks
|
||||
- Path canonicalisation rejects paths containing `\0`, `\r`, or `\n` and resolves traversal via `path::clean()` before any backend dispatch
|
||||
- Cipher / KEX / MAC / host-key algorithm allowlists are hardcoded with no environment override. Strict-KEX (CVE-2023-48795 / Terrapin) marker presence asserted by unit test
|
||||
- Per-session handle cap (default 64, configurable 8 to 1024) with UUID-generated handle ids
|
||||
- Crate-level `#![deny(unsafe_code)]` is in force across `crates/protocols`. Socket fd duplication for the watchdog uses the safe `AsFd::try_clone_to_owned` path (Linux/Unix); non-Unix falls back to the inactivity ceiling
|
||||
- `cfg(unix)` gating around platform-specific imports (`std::os::fd::AsFd`, `std::os::unix::fs::PermissionsExt`); non-Unix targets fail SFTP at config-load with `SftpInitError::UnsupportedPlatform`
|
||||
|
||||
### Documentation
|
||||
- Updated `crates/keystone/README.md` with complete integration architecture and workflow
|
||||
- Added detailed manual testing guide with 10 test scenarios
|
||||
- Updated main `README.md` to list Keystone authentication as available feature
|
||||
- Added troubleshooting section for common integration issues
|
||||
- Module-level rustdoc on `crates/protocols/src/sftp/mod.rs` describing the public API surface, configuration contract, and the architecture of the read cache and the wedge watchdog
|
||||
|
||||
### Configuration
|
||||
New environment variables:
|
||||
@@ -54,6 +74,40 @@ New environment variables:
|
||||
- `RUSTFS_KEYSTONE_CACHE_SIZE` - Token cache size (default: 10000)
|
||||
- `RUSTFS_KEYSTONE_CACHE_TTL` - Token cache TTL in seconds (default: 300)
|
||||
- `RUSTFS_KEYSTONE_VERIFY_SSL` - Verify SSL certificates (default: true)
|
||||
- `RUSTFS_SFTP_ENABLE` - Enable/disable SFTP (default: false)
|
||||
- `RUSTFS_SFTP_ADDRESS` - Listen address (default: 0.0.0.0:2222)
|
||||
- `RUSTFS_SFTP_HOST_KEY_DIR` - Directory containing host key files (must exist; each file must be 0o600 or 0o400)
|
||||
- `RUSTFS_SFTP_IDLE_TIMEOUT` - Session idle timeout in seconds (default: 600)
|
||||
- `RUSTFS_SFTP_PART_SIZE` - Multipart part size in bytes (default: 16 MiB)
|
||||
- `RUSTFS_SFTP_READ_ONLY` - Reject write packets at the protocol layer (default: false)
|
||||
- `RUSTFS_SFTP_BANNER` - Optional SSH banner text
|
||||
- `RUSTFS_SFTP_HANDLES_PER_SESSION` - Per-session open-handle cap, 8 to 1024 (default: 64)
|
||||
- `RUSTFS_SFTP_BACKEND_OP_TIMEOUT_SECS` - Per-call backend deadline in seconds, 5 to 600 (default: 60)
|
||||
- `RUSTFS_SFTP_READ_CACHE_WINDOW_BYTES` - Per-handle read-cache window in bytes, 256 KiB to 64 MiB or 0 to disable (default: 4 MiB)
|
||||
- `RUSTFS_SFTP_READ_CACHE_TOTAL_MEM_BYTES` - Process-wide read-cache memory ceiling in bytes, 16 MiB minimum (default: 256 MiB)
|
||||
|
||||
### Files Added
|
||||
- `crates/protocols/src/sftp/mod.rs` - SFTP module entry point, public API surface, crate-level rustdoc, regression-prevention test
|
||||
- `crates/protocols/src/sftp/config.rs` - `SftpConfig` and `SftpInitError` types, env-var resolvers, host-key directory loader with permission enforcement
|
||||
- `crates/protocols/src/sftp/constants.rs` - Named constants grouped by purpose: S3 error codes, HTTP error codes, POSIX mode bits, protocol identifiers, operational limits
|
||||
- `crates/protocols/src/sftp/server.rs` - `SftpServer` SSH server, russh handler, password authentication against IAM, accept loop, per-session task spawn
|
||||
- `crates/protocols/src/sftp/driver.rs` - `SftpDriver` per-session SFTPv3 handler dispatching each operation onto the `StorageBackend`
|
||||
- `crates/protocols/src/sftp/state.rs` - `HandleState` variants for read, write-buffering, write-streaming, write-failed handles
|
||||
- `crates/protocols/src/sftp/lifecycle.rs` - Per-session activity stamp, weak-ref registry, `/proc/net/tcp` probe for the wedge watchdog
|
||||
- `crates/protocols/src/sftp/wedge_watchdog.rs` - Per-session liveness watchdog cancelling sessions silent at the SFTP layer while the kernel reports CLOSE_WAIT
|
||||
- `crates/protocols/src/sftp/read_cache.rs` - Per-handle in-memory read-ahead cache with shared atomic accumulator for the process-wide memory ceiling
|
||||
- `crates/protocols/src/sftp/attrs.rs` - SFTPv3 `FileAttributes` mapping for objects and directories, longname formatting, mtime clamping
|
||||
- `crates/protocols/src/sftp/dir.rs` - OPENDIR / READDIR pagination, root-bucket listing, sub-directory listing under a prefix
|
||||
- `crates/protocols/src/sftp/errors.rs` - `SftpError` thiserror enum and S3-error classification into SFTPv3 status codes
|
||||
- `crates/protocols/src/sftp/paths.rs` - Path canonicalisation, traversal rejection, `\0` / `\r` / `\n` rejection, bucket+key decomposition
|
||||
- `crates/protocols/src/sftp/read.rs` - READ packet handler, EOF semantics, `MAX_READ_LEN` bound, integration with the read cache
|
||||
- `crates/protocols/src/sftp/write.rs` - WRITE packet handler, in-memory buffering up to part size, transition to streaming multipart, CLOSE finalisation
|
||||
- `crates/protocols/src/sftp/test_support.rs` - Test fixtures and helper builders for SFTP unit tests
|
||||
- `crates/protocols/src/common/dummy_storage.rs` - In-memory `StorageBackend` test backend covering every method, used by SFTP unit tests and the FTPS / Swift / WebDAV test suites
|
||||
- `crates/e2e_test/src/protocols/sftp_core.rs` - End-to-end regressions for the handshake deadline, idle-timeout disconnect, and the wedge watchdog
|
||||
- `crates/e2e_test/src/protocols/sftp_compliance.rs` - SFTPv3 compliance suite entry points (`test_sftp_compliance_suite`, `test_sftp_compliance_readonly`, `test_sftp_compliance_standalone`)
|
||||
- `crates/e2e_test/src/protocols/sftp_compliance_tests.rs` - Per-case test bodies (CMPTST-01..33), shared fixture helpers, lifecycle counters
|
||||
- `crates/e2e_test/src/protocols/sftp_helpers.rs` - SFTP-specific test helpers and fixture seeders
|
||||
|
||||
### Files Modified
|
||||
- `crates/keystone/src/middleware.rs` - Created Keystone authentication middleware (self-contained in keystone crate)
|
||||
@@ -63,6 +117,27 @@ New environment variables:
|
||||
- `rustfs/src/auth.rs` - Enhanced IAMAuth and check_key_valid for Keystone support, imported KEYSTONE_CREDENTIALS from rustfs-keystone
|
||||
- `crates/keystone/README.md` - Comprehensive integration documentation
|
||||
- `README.md` - Added Keystone as available feature
|
||||
- `Cargo.toml` - Added the `sftp` feature alongside the existing protocol features
|
||||
- `Cargo.lock` - Updated to include the new `russh`, `russh-sftp`, `socket2`, `tokio-util`, `subtle`, `uuid` dependencies and their transitive crates
|
||||
- `crates/protocols/Cargo.toml` - Declared `russh`, `russh-sftp`, `socket2`, `tokio-util`, `subtle`, `uuid` under the `sftp` feature flag
|
||||
- `crates/protocols/src/lib.rs` - Added `pub mod sftp` behind `#[cfg(feature = "sftp")]` plus the crate-level `#![deny(unsafe_code)]` lint
|
||||
- `crates/protocols/src/common/client/s3.rs` - Extended the `StorageBackend` trait with `create_multipart_upload`, `upload_part`, `complete_multipart_upload`, `abort_multipart_upload`, and `upload_part_copy`
|
||||
- `crates/protocols/src/common/session.rs` - Added the `Protocol::Sftp` variant and its `S3Action` mappings
|
||||
- `crates/protocols/src/common/gateway.rs` - Handles the new `Protocol::Sftp` variant exhaustively
|
||||
- `crates/protocols/src/common/mod.rs` - Exposed the new `dummy_storage` module
|
||||
- `crates/protocols/src/constants.rs` - Added shared POSIX mode-bit constants used by SFTP and other protocols
|
||||
- `crates/config/src/constants/protocols.rs` - `RUSTFS_SFTP_*` environment variable names and defaults
|
||||
- `crates/utils/src/retry.rs` - Added the generic exponential-backoff retry helper used by the SFTP write path
|
||||
- `crates/e2e_test/Cargo.toml` - Added the e2e test dependencies for SFTP (paramiko fixture, SSH keypair generation)
|
||||
- `crates/e2e_test/src/protocols/mod.rs` - Registered the new `sftp_core`, `sftp_compliance`, `sftp_compliance_tests`, and `sftp_helpers` modules
|
||||
- `crates/e2e_test/src/protocols/README.md` - Documented the SFTP test entry points and case index
|
||||
- `crates/e2e_test/src/protocols/test_env.rs` - Added SFTP host-key directory provisioning to the shared protocol test environment
|
||||
- `crates/e2e_test/src/protocols/test_runner.rs` - Wired the SFTP entry points into the runner
|
||||
- `rustfs/Cargo.toml` - Added the `sftp` feature flag
|
||||
- `rustfs/src/lib.rs` - One-line addition exporting the SFTP wiring
|
||||
- `rustfs/src/init.rs` - Build and start the `SftpServer` when `RUSTFS_SFTP_ENABLE` is true
|
||||
- `rustfs/src/main.rs` - Routed shutdown signals to the SFTP server alongside the other protocols
|
||||
- `rustfs/src/protocols/client.rs` - Client-builder support for the new `Protocol::Sftp` variant
|
||||
|
||||
### Testing
|
||||
- 16 unit tests in rustfs-keystone crate (config, auth, middleware, identity)
|
||||
@@ -70,7 +145,13 @@ New environment variables:
|
||||
- 6 auth unit tests in rustfs crate (role detection, task-local storage, Keystone credential handling)
|
||||
- **Total: 32 tests** passing with zero compilation errors
|
||||
- Manual testing guide provided for end-to-end validation
|
||||
- All tests passing with `cargo test --all --exclude e2e_test`
|
||||
- All Keystone tests passing with `cargo test --all --exclude e2e_test`
|
||||
- 33 SFTPv3 compliance test cases (CMPTST-01..33) split across three entry points: `test_sftp_compliance_suite` (shared session, cases 01-14), `test_sftp_compliance_readonly` (read-only mode, cases 15-23), `test_sftp_compliance_standalone` (one rustfs spawn per case, cases 24-33)
|
||||
- Regression-prevention tests at four layers: compile-time module assertion in `crates/protocols/src/lib.rs`, module-presence unit test in `crates/protocols/src/sftp/mod.rs`, cross-module `Protocol` enum assertion, and end-to-end SSH banner test against the running binary
|
||||
- Standalone end-to-end regressions for the SSH handshake deadline, the idle-timeout disconnect path, and the wedge watchdog (Linux fast-kill and the cross-platform fallback path)
|
||||
- Inline unit tests in every SFTP source file covering pure helpers (path canonicalisation, attribute mapping, S3-error classification, env-var bound resolvers)
|
||||
- Strict-KEX (CVE-2023-48795) marker presence assertion as a unit test in `crates/protocols/src/sftp/server.rs`
|
||||
- All tests passing with `cargo test --all --features sftp` against a 64-bit Linux target
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
## 📋 Code Quality Requirements
|
||||
|
||||
For instructions on setting up and running the local development environment, please see [Development Guide](docs/DEVELOPMENT.md).
|
||||
This guide covers the local development environment and the checks expected before contributing.
|
||||
|
||||
### 🔧 Code Formatting Rules
|
||||
|
||||
|
||||
Generated
+2466
-675
File diff suppressed because it is too large
Load Diff
+88
-77
@@ -15,10 +15,10 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"rustfs", # Core file system implementation
|
||||
"crates/appauth", # Application authentication and authorization
|
||||
"crates/audit", # Audit target management system with multi-target fan-out
|
||||
"crates/checksums", # client checksums
|
||||
"crates/common", # Shared utilities and data structures
|
||||
"crates/data-usage", # Shared data usage models and algorithms
|
||||
"crates/config", # Configuration management
|
||||
"crates/credentials", # Credential management system
|
||||
"crates/crypto", # Cryptography and security features
|
||||
@@ -39,15 +39,16 @@ members = [
|
||||
"crates/protos", # Protocol buffer definitions
|
||||
"crates/rio", # Rust I/O utilities and abstractions
|
||||
"crates/concurrency", # Concurrency management for RustFS - timeout, locking, backpressure, and I/O scheduling
|
||||
"crates/s3-common", # Common utilities and data structures for S3 compatibility
|
||||
"crates/s3-types", # S3 event type definitions
|
||||
"crates/s3-ops", # S3 operation definitions and mapping
|
||||
"crates/s3select-api", # S3 Select API interface
|
||||
"crates/s3select-query", # S3 Select query engine
|
||||
"crates/scanner", # Scanner for data integrity checks and health monitoring
|
||||
"crates/signer", # client signer
|
||||
"crates/targets", # Target-specific configurations and utilities
|
||||
"crates/trusted-proxies", # Trusted proxies management
|
||||
"crates/tls-runtime", # Project-wide TLS runtime foundation
|
||||
"crates/utils", # Utility functions and helpers
|
||||
"crates/workers", # Worker thread pools and task scheduling
|
||||
"crates/io-metrics", # Zero-copy metrics collection for performance analysis
|
||||
"crates/io-core", # Zero-copy core reader and writer implementations
|
||||
"crates/zip", # ZIP file handling and compression
|
||||
@@ -58,8 +59,8 @@ resolver = "3"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/rustfs/rustfs"
|
||||
rust-version = "1.93.0"
|
||||
version = "1.0.0-beta.1"
|
||||
rust-version = "1.95.0"
|
||||
version = "1.0.0-beta.4"
|
||||
homepage = "https://rustfs.com"
|
||||
description = "RustFS is a high-performance distributed object storage software built using Rust, one of the most popular languages worldwide. "
|
||||
keywords = ["RustFS", "Minio", "object-storage", "filesystem", "s3"]
|
||||
@@ -76,55 +77,58 @@ redundant_clone = "warn"
|
||||
|
||||
[workspace.dependencies]
|
||||
# RustFS Internal Crates
|
||||
rustfs = { path = "./rustfs", version = "1.0.0-beta.1" }
|
||||
rustfs-heal = { path = "crates/heal", version = "1.0.0-beta.1" }
|
||||
rustfs-appauth = { path = "crates/appauth", version = "1.0.0-beta.1" }
|
||||
rustfs-audit = { path = "crates/audit", version = "1.0.0-beta.1" }
|
||||
rustfs-checksums = { path = "crates/checksums", version = "1.0.0-beta.1" }
|
||||
rustfs-common = { path = "crates/common", version = "1.0.0-beta.1" }
|
||||
rustfs-config = { path = "./crates/config", version = "1.0.0-beta.1" }
|
||||
rustfs-concurrency = { path = "./crates/concurrency", version = "1.0.0-beta.1" }
|
||||
rustfs-credentials = { path = "crates/credentials", version = "1.0.0-beta.1" }
|
||||
rustfs-crypto = { path = "crates/crypto", version = "1.0.0-beta.1" }
|
||||
rustfs-ecstore = { path = "crates/ecstore", version = "1.0.0-beta.1" }
|
||||
rustfs-filemeta = { path = "crates/filemeta", version = "1.0.0-beta.1" }
|
||||
rustfs-iam = { path = "crates/iam", version = "1.0.0-beta.1" }
|
||||
rustfs-keystone = { path = "crates/keystone", version = "1.0.0-beta.1" }
|
||||
rustfs-kms = { path = "crates/kms", version = "1.0.0-beta.1" }
|
||||
rustfs-lock = { path = "crates/lock", version = "1.0.0-beta.1" }
|
||||
rustfs-madmin = { path = "crates/madmin", version = "1.0.0-beta.1" }
|
||||
rustfs-notify = { path = "crates/notify", version = "1.0.0-beta.1" }
|
||||
rustfs-io-metrics = { path = "crates/io-metrics", version = "1.0.0-beta.1" }
|
||||
rustfs-io-core = { path = "crates/io-core", version = "1.0.0-beta.1" }
|
||||
rustfs-object-capacity = { path = "crates/object-capacity", version = "1.0.0-beta.1" }
|
||||
rustfs-obs = { path = "crates/obs", version = "1.0.0-beta.1" }
|
||||
rustfs-policy = { path = "crates/policy", version = "1.0.0-beta.1" }
|
||||
rustfs-protos = { path = "crates/protos", version = "1.0.0-beta.1" }
|
||||
rustfs-protocols = { path = "crates/protocols", version = "1.0.0-beta.1" }
|
||||
rustfs-rio = { path = "crates/rio", version = "1.0.0-beta.1" }
|
||||
rustfs-s3-common = { path = "crates/s3-common", version = "1.0.0-beta.1" }
|
||||
rustfs-s3select-api = { path = "crates/s3select-api", version = "1.0.0-beta.1" }
|
||||
rustfs-s3select-query = { path = "crates/s3select-query", version = "1.0.0-beta.1" }
|
||||
rustfs-scanner = { path = "crates/scanner", version = "1.0.0-beta.1" }
|
||||
rustfs-signer = { path = "crates/signer", version = "1.0.0-beta.1" }
|
||||
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "1.0.0-beta.1" }
|
||||
rustfs-targets = { path = "crates/targets", version = "1.0.0-beta.1" }
|
||||
rustfs-utils = { path = "crates/utils", version = "1.0.0-beta.1" }
|
||||
rustfs-workers = { path = "crates/workers", version = "1.0.0-beta.1" }
|
||||
rustfs-zip = { path = "./crates/zip", version = "1.0.0-beta.1" }
|
||||
rustfs = { path = "./rustfs", version = "1.0.0-beta.4" }
|
||||
rustfs-heal = { path = "crates/heal", version = "1.0.0-beta.4" }
|
||||
rustfs-audit = { path = "crates/audit", version = "1.0.0-beta.4" }
|
||||
rustfs-checksums = { path = "crates/checksums", version = "1.0.0-beta.4" }
|
||||
rustfs-common = { path = "crates/common", version = "1.0.0-beta.4" }
|
||||
rustfs-data-usage = { path = "crates/data-usage", version = "1.0.0-beta.4" }
|
||||
rustfs-config = { path = "./crates/config", version = "1.0.0-beta.4" }
|
||||
rustfs-concurrency = { path = "./crates/concurrency", version = "1.0.0-beta.4" }
|
||||
rustfs-credentials = { path = "crates/credentials", version = "1.0.0-beta.4" }
|
||||
rustfs-crypto = { path = "crates/crypto", version = "1.0.0-beta.4" }
|
||||
rustfs-ecstore = { path = "crates/ecstore", version = "1.0.0-beta.4" }
|
||||
rustfs-filemeta = { path = "crates/filemeta", version = "1.0.0-beta.4" }
|
||||
rustfs-iam = { path = "crates/iam", version = "1.0.0-beta.4" }
|
||||
rustfs-keystone = { path = "crates/keystone", version = "1.0.0-beta.4" }
|
||||
rustfs-kms = { path = "crates/kms", version = "1.0.0-beta.4" }
|
||||
rustfs-lock = { path = "crates/lock", version = "1.0.0-beta.4" }
|
||||
rustfs-madmin = { path = "crates/madmin", version = "1.0.0-beta.4" }
|
||||
rustfs-notify = { path = "crates/notify", version = "1.0.0-beta.4" }
|
||||
rustfs-io-metrics = { path = "crates/io-metrics", version = "1.0.0-beta.4" }
|
||||
rustfs-io-core = { path = "crates/io-core", version = "1.0.0-beta.4" }
|
||||
rustfs-object-capacity = { path = "crates/object-capacity", version = "1.0.0-beta.4" }
|
||||
rustfs-obs = { path = "crates/obs", version = "1.0.0-beta.4" }
|
||||
rustfs-policy = { path = "crates/policy", version = "1.0.0-beta.4" }
|
||||
rustfs-protos = { path = "crates/protos", version = "1.0.0-beta.4" }
|
||||
rustfs-protocols = { path = "crates/protocols", version = "1.0.0-beta.4" }
|
||||
rustfs-rio = { path = "crates/rio", version = "1.0.0-beta.4" }
|
||||
rustfs-s3-types = { path = "crates/s3-types", version = "1.0.0-beta.4" }
|
||||
rustfs-s3-ops = { path = "crates/s3-ops", version = "1.0.0-beta.4" }
|
||||
rustfs-s3select-api = { path = "crates/s3select-api", version = "1.0.0-beta.4" }
|
||||
rustfs-s3select-query = { path = "crates/s3select-query", version = "1.0.0-beta.4" }
|
||||
rustfs-scanner = { path = "crates/scanner", version = "1.0.0-beta.4" }
|
||||
rustfs-signer = { path = "crates/signer", version = "1.0.0-beta.4" }
|
||||
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "1.0.0-beta.4" }
|
||||
rustfs-targets = { path = "crates/targets", version = "1.0.0-beta.4" }
|
||||
rustfs-tls-runtime = { path = "crates/tls-runtime", version = "1.0.0-beta.4" }
|
||||
rustfs-utils = { path = "crates/utils", version = "1.0.0-beta.4" }
|
||||
rustfs-zip = { path = "./crates/zip", version = "1.0.0-beta.4" }
|
||||
|
||||
# Async Runtime and Networking
|
||||
async-channel = "2.5.0"
|
||||
mysql_async = { version = "0.36.1", default-features = false, features = ["default-rustls", "tracing"], git = "https://github.com/blackbeam/mysql_async", rev = "2bad388283bc3ce48801fc2ffcd22445eb6f3d24" }
|
||||
async-compression = { version = "0.4.42" }
|
||||
async-recursion = "1.1.1"
|
||||
async-trait = "0.1.89"
|
||||
async-nats = "0.47.0"
|
||||
async-nats = "0.48.0"
|
||||
axum = "0.8.9"
|
||||
futures = "0.3.32"
|
||||
futures-core = "0.3.32"
|
||||
futures-util = "0.3.32"
|
||||
pollster = "0.4.0"
|
||||
pulsar = { version = "6.7.2", default-features = false, features = ["tokio-rustls-runtime"] }
|
||||
lapin = { version = "4.10.0", default-features = false, features = ["tokio", "rustls", "rustls--aws_lc_rs"] }
|
||||
hyper = { version = "1.9.0", features = ["http2", "http1", "server"] }
|
||||
hyper-rustls = { version = "0.27.9", default-features = false, features = ["native-tokio", "http1", "tls12", "logging", "http2", "aws-lc-rs", "webpki-roots"] }
|
||||
hyper-util = { version = "0.1.20", features = ["tokio", "server-auto", "server-graceful", "tracing"] }
|
||||
@@ -134,16 +138,16 @@ http-body-util = "0.1.3"
|
||||
reqwest = { version = "0.13.3", default-features = false, features = ["rustls", "charset", "http2", "system-proxy", "stream", "json", "blocking", "query", "form"] }
|
||||
rustfs-kafka-async = { version = "1.2.0" }
|
||||
socket2 = { version = "0.6.3", features = ["all"] }
|
||||
tokio = { version = "1.52.2", features = ["fs", "rt-multi-thread"] }
|
||||
tokio = { version = "1.52.3", features = ["fs", "rt-multi-thread"] }
|
||||
tokio-rustls = { version = "0.26.4", default-features = false, features = ["logging", "tls12", "aws-lc-rs"] }
|
||||
tokio-stream = { version = "0.1.18" }
|
||||
tokio-test = "0.4.5"
|
||||
tokio-util = { version = "0.7.18", features = ["io", "compat"] }
|
||||
tonic = { version = "0.14.5", features = ["gzip"] }
|
||||
tonic-prost = { version = "0.14.5" }
|
||||
tonic-prost-build = { version = "0.14.5" }
|
||||
tonic = { version = "0.14.6", features = ["gzip", "deflate"] }
|
||||
tonic-prost = { version = "0.14.6" }
|
||||
tonic-prost-build = { version = "0.14.6" }
|
||||
tower = { version = "0.5.3", features = ["timeout"] }
|
||||
tower-http = { version = "0.6.8", features = ["cors"] }
|
||||
tower-http = { version = "0.6.11", features = ["cors"] }
|
||||
|
||||
# Serialization and Data Formats
|
||||
bytes = { version = "1.11.1", features = ["serde"] }
|
||||
@@ -152,11 +156,11 @@ byteorder = "1.5.0"
|
||||
flatbuffers = "25.12.19"
|
||||
form_urlencoded = "1.2.2"
|
||||
prost = "0.14.3"
|
||||
quick-xml = "0.39.3"
|
||||
quick-xml = "0.40.1"
|
||||
rmp = { version = "0.8.15" }
|
||||
rmp-serde = { version = "1.3.1" }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = { version = "1.0.149", features = ["raw_value"] }
|
||||
serde_json = { version = "1.0.150", features = ["raw_value"] }
|
||||
serde_urlencoded = "0.7.1"
|
||||
|
||||
# Cryptography and Security
|
||||
@@ -164,13 +168,14 @@ aes-gcm = { version = "0.11.0-rc.3", features = ["rand_core"] }
|
||||
argon2 = { version = "0.6.0-rc.8" }
|
||||
blake2 = "0.11.0-rc.6"
|
||||
chacha20poly1305 = { version = "0.11.0-rc.3" }
|
||||
crc-fast = "1.9.0"
|
||||
crc-fast = "1.10.0"
|
||||
hmac = { version = "0.13.0" }
|
||||
jsonwebtoken = { version = "10.3.0", features = ["aws_lc_rs"] }
|
||||
jsonwebtoken = { version = "10.4.0", features = ["aws_lc_rs"] }
|
||||
openidconnect = { version = "4.0", default-features = false }
|
||||
pbkdf2 = "0.13.0"
|
||||
rsa = { version = "0.10.0-rc.18" }
|
||||
rustls = { version = "0.23.40", default-features = false, features = ["aws-lc-rs", "logging", "tls12", "prefer-post-quantum", "std"] }
|
||||
rustls-native-certs = "0.8"
|
||||
rustls-pki-types = "1.14.1"
|
||||
sha1 = "0.11.0"
|
||||
sha2 = "0.11.0"
|
||||
@@ -183,22 +188,25 @@ humantime = "2.3.0"
|
||||
jiff = { version = "0.2.24", features = ["serde"] }
|
||||
time = { version = "0.3.47", features = ["std", "parsing", "formatting", "macros", "serde"] }
|
||||
|
||||
# Database
|
||||
deadpool-postgres = { version = "0.14", features = ["rt_tokio_1"] }
|
||||
tokio-postgres = { version = "0.7", default-features = false, features = ["runtime", "with-serde_json-1"] }
|
||||
tokio-postgres-rustls = "0.14.0"
|
||||
|
||||
# Utilities and Tools
|
||||
anyhow = "1.0.102"
|
||||
arc-swap = "1.9.1"
|
||||
astral-tokio-tar = "0.6.1"
|
||||
astral-tokio-tar = "0.6.2"
|
||||
atoi = "2.0.0"
|
||||
atomic_enum = "0.3.0"
|
||||
aws-config = { version = "1.8.16" }
|
||||
aws-config = { version = "1.8.17" }
|
||||
aws-credential-types = { version = "1.2.14" }
|
||||
aws-sdk-s3 = { version = "1.131.0", default-features = false, features = ["sigv4a", "default-https-client", "rt-tokio"] }
|
||||
aws-sdk-s3 = { version = "1.133.0", default-features = false, features = ["sigv4a", "default-https-client", "rt-tokio"] }
|
||||
aws-smithy-http-client = { version = "1.1.12", default-features = false, features = ["default-client", "rustls-aws-lc"] }
|
||||
aws-smithy-types = { version = "1.4.7" }
|
||||
backtrace = "0.3.76"
|
||||
aws-smithy-types = { version = "1.4.8" }
|
||||
base64 = "0.22.1"
|
||||
base64-simd = "0.8.0"
|
||||
brotli = "8.0.2"
|
||||
cfg-if = "1.0.4"
|
||||
clap = { version = "4.6.1", features = ["derive", "env"] }
|
||||
const-str = { version = "1.1.0", features = ["std", "proc"] }
|
||||
convert_case = "0.11.0"
|
||||
@@ -209,13 +217,13 @@ crossbeam-deque = "0.8.6"
|
||||
crossbeam-utils = "0.8.21"
|
||||
datafusion = "53.1.0"
|
||||
derive_builder = "0.20.2"
|
||||
enumset = "1.1.10"
|
||||
enumset = "1.1.13"
|
||||
faster-hex = "0.10.0"
|
||||
flate2 = "1.1.9"
|
||||
glob = "0.3.3"
|
||||
google-cloud-storage = "1.11.0"
|
||||
google-cloud-auth = "1.9.0"
|
||||
hashbrown = { version = "0.17.0", features = ["serde", "rayon"] }
|
||||
google-cloud-storage = "1.12.0"
|
||||
google-cloud-auth = "1.10.0"
|
||||
hashbrown = { version = "0.17.1", features = ["serde", "rayon"] }
|
||||
hex = "0.4.3"
|
||||
hex-simd = "0.8.0"
|
||||
highway = { version = "1.3.0" }
|
||||
@@ -223,7 +231,7 @@ ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||
lazy_static = "1.5.0"
|
||||
libc = "0.2.186"
|
||||
libsystemd = "0.7.2"
|
||||
local-ip-address = "0.6.12"
|
||||
local-ip-address = "0.6.13"
|
||||
memmap2 = "0.9.10"
|
||||
lz4 = "1.28.1"
|
||||
matchit = "0.9.2"
|
||||
@@ -247,11 +255,12 @@ rayon = "1.12.0"
|
||||
reed-solomon-erasure = { version = "6.0", default-features = false, features = ["std", "simd-accel"] }
|
||||
reed-solomon-simd = "3.1.0"
|
||||
regex = { version = "1.12.3" }
|
||||
rumqttc = { package = "rumqttc-next", version = "0.30.1", features = ["websocket"] }
|
||||
rumqttc = { package = "rumqttc-next", version = "0.33.2", features = ["websocket"] }
|
||||
redis = { version = "1.2.1", features = ["connection-manager", "tokio-rustls-comp", "tls-rustls-insecure"] }
|
||||
rustix = { version = "1.1.4", features = ["fs"] }
|
||||
rust-embed = { version = "8.11.0" }
|
||||
rustc-hash = { version = "2.1.2" }
|
||||
s3s = { git = "https://github.com/rustfs/s3s", rev = "a3b16608df35aaeed8fff08b4988d03f4ca9445b", features = ["minio"] }
|
||||
s3s = { git = "https://github.com/rustfs/s3s", rev = "507e1312b211c3ddc214b03875d6fabd15d22ed5", features = ["minio"] }
|
||||
serial_test = "3.4.0"
|
||||
shadow-rs = { version = "2.0.0", default-features = false }
|
||||
siphasher = "1.0.3"
|
||||
@@ -259,9 +268,9 @@ smallvec = { version = "1.15.1", features = ["serde"] }
|
||||
smartstring = "1.0.1"
|
||||
snafu = "0.9.0"
|
||||
snap = "1.1.1"
|
||||
starshard = { version = "1.1.0", features = ["rayon", "async", "serde"] }
|
||||
starshard = { version = "2.2.0", features = ["rayon", "async", "serde"] }
|
||||
strum = { version = "0.28.0", features = ["derive"] }
|
||||
sysinfo = "0.38.4"
|
||||
sysinfo = "0.39.2"
|
||||
temp-env = "0.3.6"
|
||||
tempfile = "3.27.0"
|
||||
test-case = "3.3.1"
|
||||
@@ -269,7 +278,7 @@ thiserror = "2.0.18"
|
||||
tracing = { version = "0.1.44" }
|
||||
tracing-appender = "0.2.5"
|
||||
tracing-error = "0.2.1"
|
||||
tracing-opentelemetry = "0.32.1"
|
||||
tracing-opentelemetry = { version = "0.33" }
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter", "time"] }
|
||||
transform-stream = "0.3.1"
|
||||
url = "2.5.8"
|
||||
@@ -284,21 +293,23 @@ zip = "8.6.0"
|
||||
zstd = "0.13.3"
|
||||
|
||||
# Observability and Metrics
|
||||
metrics = "0.24.5"
|
||||
metrics = "0.24.6"
|
||||
dial9-tokio-telemetry = "0.3"
|
||||
opentelemetry = { version = "0.31.0" }
|
||||
opentelemetry-appender-tracing = { version = "0.31.1", features = ["experimental_use_tracing_span_context", "experimental_metadata_attributes", "spec_unstable_logs_enabled"] }
|
||||
opentelemetry-otlp = { version = "0.31.1", features = ["gzip-http", "reqwest-rustls"] }
|
||||
opentelemetry_sdk = { version = "0.31.0" }
|
||||
opentelemetry-semantic-conventions = { version = "0.31.0", features = ["semconv_experimental"] }
|
||||
opentelemetry-stdout = { version = "0.31.0" }
|
||||
pyroscope = { version = "2.0.3", features = ["backend-pprof-rs"] }
|
||||
opentelemetry = { version = "0.32.0" }
|
||||
opentelemetry-appender-tracing = { version = "0.32.0", features = ["experimental_span_attributes", "experimental_metadata_attributes"] }
|
||||
opentelemetry-otlp = { version = "0.32.0", features = ["gzip-http", "reqwest-rustls"] }
|
||||
opentelemetry_sdk = { version = "0.32.0", features = ["rt-tokio"] }
|
||||
opentelemetry-semantic-conventions = { version = "0.32.0", features = ["semconv_experimental"] }
|
||||
opentelemetry-stdout = { version = "0.32.0" }
|
||||
pyroscope = { version = "2.0.4", features = ["backend-pprof-rs"] }
|
||||
|
||||
# FTP and SFTP
|
||||
libunftp = { version = "0.23.0", features = ["experimental"] }
|
||||
unftp-core = "0.1.0"
|
||||
suppaftp = { version = "8.0.3", features = ["tokio", "tokio-rustls-aws-lc-rs"] }
|
||||
rcgen = "0.14.7"
|
||||
rcgen = "0.14.8"
|
||||
russh = { version = "0.60.3", git = "https://github.com/Eugeny/russh", rev = "fc6e3ab4cd4338e94ae64e17aeed2acee9335e6b" }
|
||||
russh-sftp = "2.3.0"
|
||||
|
||||
# WebDAV
|
||||
dav-server = "0.11.0"
|
||||
@@ -314,7 +325,7 @@ jemalloc_pprof = { version = "0.8.2", features = ["symbolize", "flamegraph"] }
|
||||
# Used to generate CPU performance analysis data and flame diagrams
|
||||
# pprof = { version = "0.15.0", features = ["flamegraph", "protobuf-codec"] }
|
||||
# Pyroscope uses a patched pprof, until they merge back upstream, replace all references. Otherwise, two pprof libs with symbol collision.
|
||||
pprof = { package = "pprof-pyroscope-fork", version = "0.1500.3", features = ["flamegraph", "protobuf-codec"] }
|
||||
pprof = { package = "pprof-pyroscope-fork", version = "0.1500.4", features = ["flamegraph", "protobuf-codec"] }
|
||||
|
||||
[workspace.metadata.cargo-shear]
|
||||
ignored = ["rustfs"]
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
FROM rust:1.93-trixie
|
||||
FROM rust:1.95-trixie
|
||||
|
||||
RUN set -eux; \
|
||||
export DEBIAN_FRONTEND=noninteractive; \
|
||||
|
||||
@@ -105,8 +105,6 @@ RUN groupadd -g 10001 rustfs && \
|
||||
|
||||
ENV RUSTFS_ADDRESS=":9000" \
|
||||
RUSTFS_CONSOLE_ADDRESS=":9001" \
|
||||
RUSTFS_ACCESS_KEY="rustfsadmin" \
|
||||
RUSTFS_SECRET_KEY="rustfsadmin" \
|
||||
RUSTFS_CONSOLE_ENABLE="true" \
|
||||
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
|
||||
RUSTFS_VOLUMES="/data" \
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ ARG TARGETARCH
|
||||
# -----------------------------
|
||||
# Build stage
|
||||
# -----------------------------
|
||||
FROM rust:1.93-trixie AS builder
|
||||
FROM rust:1.95-trixie AS builder
|
||||
|
||||
# Re-declare args after FROM
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
@@ -115,7 +115,7 @@ chown -R 10001:10001 data logs
|
||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
||||
|
||||
# Using specific version
|
||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:v1.0.0-beta.1
|
||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:1.0.0-beta.4
|
||||
```
|
||||
|
||||
If you use [podman](https://github.com/containers/podman) instead of docker, you can install the RustFS with the below command
|
||||
@@ -151,6 +151,7 @@ Notes:
|
||||
- `RUSTFS_NOTIFY_ENABLE=true` enables the global notify module switch.
|
||||
- For ARN `arn:rustfs:sqs::primary:webhook`, use instance-scoped env vars with `_PRIMARY`.
|
||||
- If queue dir is omitted, default is `/opt/rustfs/events`; ensure it is writable by the container runtime user.
|
||||
- `RUSTFS_NOTIFY_WEBHOOK_SKIP_TLS_VERIFY_PRIMARY` defaults to `false`; enabling it skips webhook TLS certificate verification, allows MITM attacks, and emits a startup warning. Prefer `RUSTFS_NOTIFY_WEBHOOK_CLIENT_CA_PRIMARY` for private CAs.
|
||||
|
||||
**NOTE**: We recommend reviewing the `docker-compose.yml` file before running. It defines several services including Grafana, Prometheus, and Jaeger, which are helpful for RustFS observability. If you wish to start Redis or Nginx containers, you can specify the corresponding profiles.
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
[](https://rustfs.com.cn)
|
||||
[](https://rustfs.com.cn)
|
||||
|
||||
<p align="center">RustFS 是一个基于 Rust 构建的高性能分布式对象存储系统。</p>
|
||||
|
||||
@@ -113,7 +113,7 @@ RustFS 容器以非 root 用户 `rustfs` (UID `10001`) 运行。如果您使用
|
||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
||||
|
||||
# 使用指定版本运行
|
||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:v1.0.0-beta.1
|
||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:1.0.0-beta.4
|
||||
```
|
||||
|
||||
您也可以使用 Docker Compose。使用根目录下的 `docker-compose.yml` 文件:
|
||||
|
||||
+23
-1
@@ -109,6 +109,7 @@ FORCE_CONSOLE_UPDATE=false
|
||||
CONSOLE_VERSION="latest"
|
||||
SKIP_VERIFICATION=false
|
||||
CUSTOM_PLATFORM=""
|
||||
FEATURES=""
|
||||
|
||||
# Print usage
|
||||
usage() {
|
||||
@@ -141,6 +142,7 @@ usage() {
|
||||
echo " --no-console Skip console static assets"
|
||||
echo " --force-console-update Force update console assets even if they exist"
|
||||
echo " --console-version VERSION Console version to download (default: latest)"
|
||||
echo " -f, --features FEATURES Cargo features to enable (e.g. 'webdav', 'full')"
|
||||
echo " --skip-verification Skip binary verification after build"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
@@ -148,6 +150,8 @@ usage() {
|
||||
echo " $0 # Build for current platform (includes console assets)"
|
||||
echo " $0 --dev # Development build"
|
||||
echo " $0 --sign # Build and sign binary (release CI)"
|
||||
echo " $0 --features webdav # Build with WebDAV support"
|
||||
echo " $0 --features full # Build with all protocol features"
|
||||
echo " $0 --no-console # Build without console static assets"
|
||||
echo " $0 --force-console-update # Force update console assets"
|
||||
echo " $0 --platform x86_64-unknown-linux-musl # Build for specific platform"
|
||||
@@ -213,7 +217,7 @@ setup_rust_environment() {
|
||||
# Set up environment variables for musl targets
|
||||
if [[ "$PLATFORM" == *"musl"* ]]; then
|
||||
print_message $YELLOW "Setting up environment for musl target..."
|
||||
export RUSTFLAGS="'--cfg tokio_unstable -C target-feature=-crt-static'"
|
||||
export RUSTFLAGS="--cfg tokio_unstable -C target-feature=-crt-static"
|
||||
|
||||
# For cargo-zigbuild, set up additional environment variables
|
||||
if command -v cargo-zigbuild &> /dev/null; then
|
||||
@@ -440,6 +444,12 @@ build_binary() {
|
||||
build_cmd+=" --target $PLATFORM"
|
||||
build_cmd+=" -p rustfs --bins"
|
||||
|
||||
if [ -n "$FEATURES" ]; then
|
||||
local quoted_features
|
||||
printf -v quoted_features '%q' "$FEATURES"
|
||||
build_cmd+=" --features $quoted_features"
|
||||
fi
|
||||
|
||||
print_message $BLUE "📦 Executing: $build_cmd"
|
||||
|
||||
# Execute build (this matches exactly what the working version does)
|
||||
@@ -497,6 +507,9 @@ build_rustfs() {
|
||||
print_message $YELLOW " Force Console Update: $FORCE_CONSOLE_UPDATE"
|
||||
fi
|
||||
print_message $YELLOW " Skip Verification: $SKIP_VERIFICATION"
|
||||
if [ -n "$FEATURES" ]; then
|
||||
print_message $YELLOW " Features: $FEATURES"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Setup environment
|
||||
@@ -565,6 +578,15 @@ while [[ $# -gt 0 ]]; do
|
||||
SKIP_VERIFICATION=true
|
||||
shift
|
||||
;;
|
||||
-f|--features)
|
||||
if [ -z "${2:-}" ]; then
|
||||
print_message $RED "❌ Missing value for $1"
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
FEATURES="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
[](https://rustfs.com)
|
||||
|
||||
# RustFS AppAuth - Application Authentication
|
||||
|
||||
<p align="center">
|
||||
<strong>Application-level authentication and authorization module for RustFS distributed object storage</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://docs.rustfs.com/">📖 Documentation</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/issues">🐛 Bug Reports</a>
|
||||
· <a href="https://github.com/rustfs/rustfs/discussions">💬 Discussions</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Overview
|
||||
|
||||
**RustFS AppAuth** provides application-level authentication and authorization capabilities for the [RustFS](https://rustfs.com) distributed object storage system. For the complete RustFS experience, please visit the [main RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- JWT-based authentication with secure token management
|
||||
- RBAC (Role-Based Access Control) for fine-grained permissions
|
||||
- Multi-tenant application isolation and management
|
||||
- OAuth 2.0 and OpenID Connect integration
|
||||
- API key management and rotation
|
||||
- Session management with configurable expiration
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For comprehensive documentation, examples, and usage guides, please visit the main [RustFS repository](https://github.com/rustfs/rustfs).
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the Apache License 2.0 - see the [LICENSE](../../LICENSE) file for details.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Audit Crate Instructions
|
||||
|
||||
Applies to `crates/audit/`.
|
||||
|
||||
`rustfs-audit` is the domain layer for audit event fan-out and observability.
|
||||
It composes shared plugin/runtime abstractions from `rustfs-targets` and keeps
|
||||
audit-specific dispatch semantics, state transitions, and metrics in this
|
||||
crate.
|
||||
|
||||
## Domain Boundaries
|
||||
|
||||
- Keep audit-specific behavior here:
|
||||
- audit event shaping and fan-out pipeline
|
||||
- audit system lifecycle/state transitions
|
||||
- audit metrics and reporting
|
||||
- Keep shared plugin/runtime mechanics in `rustfs-targets`:
|
||||
- no duplicated replay worker orchestration
|
||||
- no duplicated runtime manager primitives
|
||||
- no plugin install/control-plane modeling in this crate
|
||||
|
||||
## Runtime Layering Rules
|
||||
|
||||
- `pipeline.rs` hosts:
|
||||
- `AuditPipeline` (dispatch and snapshot access)
|
||||
- `AuditRuntimeFacade` (runtime mutation path)
|
||||
- `AuditRuntimeView` (runtime read path)
|
||||
- `registry.rs` should remain the single owner of runtime target container and
|
||||
plugin registry composition for audit.
|
||||
- `system.rs` should coordinate lifecycle by calling facade/view/registry
|
||||
boundaries rather than embedding low-level runtime logic.
|
||||
|
||||
## Change Style
|
||||
|
||||
- Preserve audit delivery semantics and error handling behavior unless the task
|
||||
explicitly changes them.
|
||||
- Prefer extending shared abstractions in `rustfs-targets` over patching
|
||||
one-off audit-only runtime flows.
|
||||
- Keep logging and observability machine-meaningful; avoid noisy churn in hot
|
||||
dispatch paths.
|
||||
|
||||
## Testing
|
||||
|
||||
- Keep unit tests close to changed modules.
|
||||
- Keep pipeline-layer regressions in `tests/pipeline_layer_test.rs`.
|
||||
- Add regression tests for:
|
||||
- runtime facade activation/replace/stop/shutdown behavior
|
||||
- runtime view target/snapshot access
|
||||
- system reload and runtime commit/clear boundaries
|
||||
- Suggested validation:
|
||||
- `cargo test -p rustfs-audit`
|
||||
- Focused: `cargo test -p rustfs-audit --test pipeline_layer_test`
|
||||
- Focused: `cargo test -p rustfs-audit pipeline`
|
||||
- Full gate before commit: `make pre-commit`
|
||||
@@ -29,8 +29,7 @@ categories = ["web-programming", "development-tools", "asynchronous", "api-bindi
|
||||
rustfs-targets = { workspace = true }
|
||||
rustfs-config = { workspace = true, features = ["audit", "constants"] }
|
||||
rustfs-ecstore = { workspace = true }
|
||||
rustfs-s3-common = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
rustfs-s3-types = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
const-str = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
@@ -43,6 +42,7 @@ tokio = { workspace = true, features = ["sync", "fs", "rt-multi-thread", "rt", "
|
||||
tracing = { workspace = true, features = ["std", "attributes"] }
|
||||
|
||||
[dev-dependencies]
|
||||
async-trait = { workspace = true }
|
||||
temp-env = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
@@ -50,5 +50,4 @@ url = { workspace = true }
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use hashbrown::HashMap;
|
||||
use rustfs_s3_common::EventName;
|
||||
use rustfs_s3_types::EventName;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
+50
-113
@@ -13,128 +13,65 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::AuditEntry;
|
||||
use async_trait::async_trait;
|
||||
use rustfs_config::AUDIT_DEFAULT_DIR;
|
||||
use rustfs_config::audit::{AUDIT_KAFKA_KEYS, AUDIT_MQTT_KEYS, AUDIT_NATS_KEYS, AUDIT_PULSAR_KEYS, AUDIT_WEBHOOK_KEYS};
|
||||
use rustfs_ecstore::config::KVS;
|
||||
use rustfs_targets::{
|
||||
Target,
|
||||
config::{
|
||||
build_kafka_args, build_mqtt_args, build_nats_args, build_pulsar_args, build_webhook_args, validate_kafka_config,
|
||||
validate_mqtt_config, validate_nats_config, validate_pulsar_config, validate_webhook_config,
|
||||
},
|
||||
error::TargetError,
|
||||
target::TargetType,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
use rustfs_targets::catalog::builtin::builtin_audit_target_descriptors;
|
||||
use rustfs_targets::{BuiltinTargetDescriptor, TargetPluginDescriptor};
|
||||
|
||||
/// Trait for creating targets from configuration
|
||||
#[async_trait]
|
||||
pub trait TargetFactory: Send + Sync {
|
||||
/// Creates a target from configuration
|
||||
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError>;
|
||||
|
||||
/// Validates target configuration
|
||||
fn validate_config(&self, id: &str, config: &KVS) -> Result<(), TargetError>;
|
||||
|
||||
/// Returns a set of valid configuration field names for this target type.
|
||||
/// This is used to filter environment variables.
|
||||
fn get_valid_fields(&self) -> HashSet<String>;
|
||||
pub fn builtin_target_descriptors() -> Vec<BuiltinTargetDescriptor<AuditEntry>> {
|
||||
builtin_audit_target_descriptors::<AuditEntry>()
|
||||
}
|
||||
|
||||
/// Factory for creating Webhook targets
|
||||
pub struct WebhookTargetFactory;
|
||||
|
||||
#[async_trait]
|
||||
impl TargetFactory for WebhookTargetFactory {
|
||||
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
||||
let args = build_webhook_args(config, AUDIT_DEFAULT_DIR, TargetType::AuditLog)?;
|
||||
let target = rustfs_targets::target::webhook::WebhookTarget::new(id, args)?;
|
||||
Ok(Box::new(target))
|
||||
}
|
||||
|
||||
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||
validate_webhook_config(config, AUDIT_DEFAULT_DIR)
|
||||
}
|
||||
|
||||
fn get_valid_fields(&self) -> HashSet<String> {
|
||||
AUDIT_WEBHOOK_KEYS.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
pub fn builtin_target_plugins() -> Vec<TargetPluginDescriptor<AuditEntry>> {
|
||||
builtin_target_descriptors()
|
||||
.into_iter()
|
||||
.map(|descriptor| descriptor.plugin().clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Factory for creating MQTT targets
|
||||
pub struct MQTTTargetFactory;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::builtin_target_descriptors;
|
||||
use rustfs_config::audit::AUDIT_AMQP_KEYS;
|
||||
use rustfs_config::{AMQP_EXCHANGE, AMQP_QUEUE_DIR, AMQP_ROUTING_KEY, AMQP_URL};
|
||||
use rustfs_ecstore::config::KVS;
|
||||
use rustfs_targets::target::ChannelTargetType;
|
||||
|
||||
#[async_trait]
|
||||
impl TargetFactory for MQTTTargetFactory {
|
||||
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
||||
let args = build_mqtt_args(config, AUDIT_DEFAULT_DIR, TargetType::AuditLog)?;
|
||||
let target = rustfs_targets::target::mqtt::MQTTTarget::new(id, args)?;
|
||||
Ok(Box::new(target))
|
||||
fn amqp_base_config() -> KVS {
|
||||
let mut config = KVS::new();
|
||||
config.insert(AMQP_URL.to_string(), "amqp://127.0.0.1:5672/%2f".to_string());
|
||||
config.insert(AMQP_EXCHANGE.to_string(), "rustfs.audit".to_string());
|
||||
config.insert(AMQP_ROUTING_KEY.to_string(), "audit".to_string());
|
||||
config.insert(AMQP_QUEUE_DIR.to_string(), String::new());
|
||||
config
|
||||
}
|
||||
|
||||
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||
validate_mqtt_config(config)
|
||||
#[test]
|
||||
fn builtin_plugins_include_amqp_descriptor() {
|
||||
let plugin = builtin_target_descriptors()
|
||||
.into_iter()
|
||||
.find(|plugin| plugin.plugin().target_type() == ChannelTargetType::Amqp.as_str())
|
||||
.expect("amqp plugin should exist");
|
||||
|
||||
assert!(plugin.plugin().valid_fields().contains(&AMQP_URL));
|
||||
assert!(plugin.plugin().valid_fields().contains(&AMQP_EXCHANGE));
|
||||
assert!(plugin.plugin().valid_fields().contains(&AMQP_ROUTING_KEY));
|
||||
assert_eq!(plugin.plugin().valid_fields().len(), AUDIT_AMQP_KEYS.len());
|
||||
}
|
||||
|
||||
fn get_valid_fields(&self) -> HashSet<String> {
|
||||
AUDIT_MQTT_KEYS.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NATSTargetFactory;
|
||||
|
||||
#[async_trait]
|
||||
impl TargetFactory for NATSTargetFactory {
|
||||
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
||||
let args = build_nats_args(config, AUDIT_DEFAULT_DIR, TargetType::AuditLog)?;
|
||||
let target = rustfs_targets::target::nats::NATSTarget::new(id, args)?;
|
||||
Ok(Box::new(target))
|
||||
}
|
||||
|
||||
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||
validate_nats_config(config, AUDIT_DEFAULT_DIR)
|
||||
}
|
||||
|
||||
fn get_valid_fields(&self) -> HashSet<String> {
|
||||
AUDIT_NATS_KEYS.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PulsarTargetFactory;
|
||||
|
||||
#[async_trait]
|
||||
impl TargetFactory for PulsarTargetFactory {
|
||||
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
||||
let args = build_pulsar_args(config, AUDIT_DEFAULT_DIR, TargetType::AuditLog)?;
|
||||
let target = rustfs_targets::target::pulsar::PulsarTarget::new(id, args)?;
|
||||
Ok(Box::new(target))
|
||||
}
|
||||
|
||||
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||
validate_pulsar_config(config, AUDIT_DEFAULT_DIR)
|
||||
}
|
||||
|
||||
fn get_valid_fields(&self) -> HashSet<String> {
|
||||
AUDIT_PULSAR_KEYS.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KafkaTargetFactory;
|
||||
|
||||
#[async_trait]
|
||||
impl TargetFactory for KafkaTargetFactory {
|
||||
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
||||
let args = build_kafka_args(config, AUDIT_DEFAULT_DIR, TargetType::AuditLog)?;
|
||||
let target = rustfs_targets::target::kafka::KafkaTarget::new(id, args)?;
|
||||
Ok(Box::new(target))
|
||||
}
|
||||
|
||||
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||
validate_kafka_config(config, AUDIT_DEFAULT_DIR)
|
||||
}
|
||||
|
||||
fn get_valid_fields(&self) -> HashSet<String> {
|
||||
AUDIT_KAFKA_KEYS.iter().map(|s| s.to_string()).collect()
|
||||
#[test]
|
||||
fn builtin_plugins_create_audit_amqp_target() {
|
||||
let plugin = builtin_target_descriptors()
|
||||
.into_iter()
|
||||
.find(|plugin| plugin.plugin().target_type() == ChannelTargetType::Amqp.as_str())
|
||||
.expect("amqp plugin should exist");
|
||||
|
||||
let target = plugin
|
||||
.plugin()
|
||||
.create_target("primary".to_string(), &amqp_base_config())
|
||||
.expect("AMQP audit target should be created");
|
||||
|
||||
let target_id = target.id();
|
||||
assert_eq!(target_id.id, "primary");
|
||||
assert_eq!(target_id.name, "amqp");
|
||||
assert!(target.store().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ pub mod error;
|
||||
pub mod factory;
|
||||
pub mod global;
|
||||
pub mod observability;
|
||||
pub mod pipeline;
|
||||
pub mod registry;
|
||||
pub mod system;
|
||||
|
||||
@@ -30,5 +31,6 @@ pub use entity::{ApiDetails, AuditEntry, ObjectVersion};
|
||||
pub use error::{AuditError, AuditResult};
|
||||
pub use global::*;
|
||||
pub use observability::{AuditMetrics, AuditMetricsReport, PerformanceValidation};
|
||||
pub use pipeline::{AuditPipeline, AuditRuntimeFacade, AuditRuntimeView};
|
||||
pub use registry::AuditRegistry;
|
||||
pub use system::{AuditSystem, AuditTargetMetricSnapshot};
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
use crate::{AuditEntry, AuditResult, observability, system::AuditTargetMetricSnapshot};
|
||||
use rustfs_targets::{
|
||||
BuiltinPluginRuntimeAdapter, PluginRuntimeAdapter, ReplayEvent, ReplayWorkerManager, RuntimeActivation, SharedTarget, Target,
|
||||
target::EntityTarget,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuditPipeline {
|
||||
registry: Arc<Mutex<crate::AuditRegistry>>,
|
||||
}
|
||||
|
||||
impl AuditPipeline {
|
||||
pub fn new(registry: Arc<Mutex<crate::AuditRegistry>>) -> Self {
|
||||
Self { registry }
|
||||
}
|
||||
|
||||
pub async fn dispatch(&self, entry: Arc<AuditEntry>) -> AuditResult<()> {
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
let targets: Vec<SharedTarget<AuditEntry>> = {
|
||||
let registry = self.registry.lock().await;
|
||||
let targets = registry.list_target_values();
|
||||
|
||||
if targets.is_empty() {
|
||||
warn!("No audit targets configured for dispatch");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
targets
|
||||
};
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for target in targets {
|
||||
let entity_target = EntityTarget {
|
||||
object_name: entry.api.name.clone().unwrap_or_default(),
|
||||
bucket_name: entry.api.bucket.clone().unwrap_or_default(),
|
||||
event_name: entry.event,
|
||||
data: (*entry).clone(),
|
||||
};
|
||||
|
||||
let task = async move {
|
||||
let result = target.save(Arc::new(entity_target)).await;
|
||||
(target.id().to_string(), result)
|
||||
};
|
||||
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
let results = futures::future::join_all(tasks).await;
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let mut success_count = 0;
|
||||
|
||||
for (target_key, result) in results {
|
||||
match result {
|
||||
Ok(_) => {
|
||||
success_count += 1;
|
||||
observability::record_target_success();
|
||||
}
|
||||
Err(e) => {
|
||||
error!(target_id = %target_key, error = %e, "Failed to dispatch audit log to target");
|
||||
errors.push(e);
|
||||
observability::record_target_failure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dispatch_time = start_time.elapsed();
|
||||
|
||||
if errors.is_empty() {
|
||||
observability::record_audit_success(dispatch_time);
|
||||
} else {
|
||||
observability::record_audit_failure(dispatch_time);
|
||||
warn!(
|
||||
error_count = errors.len(),
|
||||
success_count = success_count,
|
||||
"Some audit targets failed to receive log entry"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn dispatch_batch(&self, entries: Vec<Arc<AuditEntry>>) -> AuditResult<()> {
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
let targets: Vec<SharedTarget<AuditEntry>> = {
|
||||
let registry = self.registry.lock().await;
|
||||
let targets = registry.list_target_values();
|
||||
|
||||
if targets.is_empty() {
|
||||
warn!("No audit targets configured for batch dispatch");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
targets
|
||||
};
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
for target in targets {
|
||||
let entries_clone: Vec<_> = entries.iter().map(Arc::clone).collect();
|
||||
|
||||
let task = async move {
|
||||
let mut success_count = 0;
|
||||
let mut errors = Vec::new();
|
||||
for entry in entries_clone {
|
||||
let entity_target = EntityTarget {
|
||||
object_name: entry.api.name.clone().unwrap_or_default(),
|
||||
bucket_name: entry.api.bucket.clone().unwrap_or_default(),
|
||||
event_name: entry.event,
|
||||
data: (*entry).clone(),
|
||||
};
|
||||
match target.save(Arc::new(entity_target)).await {
|
||||
Ok(_) => success_count += 1,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
}
|
||||
(target.id().to_string(), success_count, errors)
|
||||
};
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
let results = futures::future::join_all(tasks).await;
|
||||
let mut total_success = 0;
|
||||
let mut total_errors = 0;
|
||||
for (_target_id, success_count, errors) in results {
|
||||
total_success += success_count;
|
||||
total_errors += errors.len();
|
||||
for e in errors {
|
||||
error!("Batch dispatch error: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let dispatch_time = start_time.elapsed();
|
||||
info!(
|
||||
"Batch dispatched {} entries, success: {}, errors: {}, time: {:?}",
|
||||
entries.len(),
|
||||
total_success,
|
||||
total_errors,
|
||||
dispatch_time
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn snapshot_target_metrics(&self) -> Vec<AuditTargetMetricSnapshot> {
|
||||
let registry = self.registry.lock().await;
|
||||
registry
|
||||
.list_target_values()
|
||||
.into_iter()
|
||||
.map(|target| {
|
||||
let delivery = target.delivery_snapshot();
|
||||
AuditTargetMetricSnapshot {
|
||||
failed_messages: delivery.failed_messages,
|
||||
queue_length: delivery.queue_length,
|
||||
target_id: target.id().to_string(),
|
||||
total_messages: delivery.total_messages,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn snapshot_target_health(&self) -> Vec<rustfs_targets::RuntimeTargetHealthSnapshot> {
|
||||
let registry = self.registry.lock().await;
|
||||
registry.runtime_manager().health_snapshots().await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuditRuntimeView {
|
||||
registry: Arc<Mutex<crate::AuditRegistry>>,
|
||||
}
|
||||
|
||||
impl AuditRuntimeView {
|
||||
pub fn new(registry: Arc<Mutex<crate::AuditRegistry>>) -> Self {
|
||||
Self { registry }
|
||||
}
|
||||
|
||||
pub async fn list_targets(&self) -> Vec<String> {
|
||||
let registry = self.registry.lock().await;
|
||||
registry.list_targets()
|
||||
}
|
||||
|
||||
pub async fn get_target_values(&self) -> Vec<rustfs_targets::SharedTarget<AuditEntry>> {
|
||||
let registry = self.registry.lock().await;
|
||||
registry.list_target_values()
|
||||
}
|
||||
|
||||
pub async fn get_target(&self, target_id: &str) -> Option<String> {
|
||||
let registry = self.registry.lock().await;
|
||||
registry.get_target(target_id).map(|target| target.id().to_string())
|
||||
}
|
||||
|
||||
pub async fn enable_target(&self, target_id: &str) -> AuditResult<()> {
|
||||
let registry = self.registry.lock().await;
|
||||
if registry.get_target(target_id).is_some() {
|
||||
info!(target_id = %target_id, "Target enabled");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(crate::AuditError::Configuration(format!("Target not found: {target_id}"), None))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn disable_target(&self, target_id: &str) -> AuditResult<()> {
|
||||
let registry = self.registry.lock().await;
|
||||
if registry.get_target(target_id).is_some() {
|
||||
info!(target_id = %target_id, "Target disabled");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(crate::AuditError::Configuration(format!("Target not found: {target_id}"), None))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remove_target(&self, target_id: &str) -> AuditResult<()> {
|
||||
let mut registry = self.registry.lock().await;
|
||||
if registry.remove_target(target_id).await.is_some() {
|
||||
info!(target_id = %target_id, "Target removed");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(crate::AuditError::Configuration(format!("Target not found: {target_id}"), None))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upsert_target(&self, target_id: String, target: Box<dyn Target<AuditEntry> + Send + Sync>) -> AuditResult<()> {
|
||||
if let Err(err) = target.init().await {
|
||||
return Err(crate::AuditError::Target(err));
|
||||
}
|
||||
|
||||
let shared_target: SharedTarget<AuditEntry> = Arc::from(target);
|
||||
let mut registry = self.registry.lock().await;
|
||||
let _ = registry.remove_target(&target_id).await;
|
||||
registry.add_shared_target(target_id.clone(), shared_target);
|
||||
info!(target_id = %target_id, "Target upserted");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuditRuntimeFacade {
|
||||
registry: Arc<Mutex<crate::AuditRegistry>>,
|
||||
replay_workers: Arc<RwLock<ReplayWorkerManager>>,
|
||||
runtime_adapter: Arc<dyn PluginRuntimeAdapter<AuditEntry>>,
|
||||
}
|
||||
|
||||
impl AuditRuntimeFacade {
|
||||
pub fn new(registry: Arc<Mutex<crate::AuditRegistry>>, replay_workers: Arc<RwLock<ReplayWorkerManager>>) -> Self {
|
||||
let runtime_adapter = BuiltinPluginRuntimeAdapter::new(
|
||||
Arc::new(move |event: ReplayEvent<AuditEntry>| {
|
||||
Box::pin(async move {
|
||||
match event {
|
||||
ReplayEvent::Delivered { key, target } => {
|
||||
info!("Successfully sent audit entry, target: {}, key: {}", target.id(), key.to_string());
|
||||
observability::record_target_success();
|
||||
}
|
||||
ReplayEvent::RetryableError { error, target, .. } => match error {
|
||||
rustfs_targets::TargetError::NotConnected => {
|
||||
warn!("Target {} not connected, retrying...", target.id());
|
||||
}
|
||||
rustfs_targets::TargetError::Timeout(_) => {
|
||||
warn!("Timeout sending to target {}, retrying...", target.id());
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ReplayEvent::Dropped { reason, target, .. } => {
|
||||
warn!("Dropped queued payload for target {}: {}", target.id(), reason);
|
||||
observability::record_target_failure();
|
||||
}
|
||||
ReplayEvent::PermanentFailure { error, target, .. } => {
|
||||
error!("Permanent error for target {}: {}", target.id(), error);
|
||||
target.record_final_failure();
|
||||
observability::record_target_failure();
|
||||
}
|
||||
ReplayEvent::RetryExhausted { key, target } => {
|
||||
warn!("Max retries exceeded for key {}, target: {}, skipping", key.to_string(), target.id());
|
||||
target.record_final_failure();
|
||||
observability::record_target_failure();
|
||||
}
|
||||
ReplayEvent::UnreadableEntry { key, error, target } => {
|
||||
warn!("Skipping unreadable audit store entry {} for target {}: {}", key, target.id(), error);
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
Arc::new(|target_id, has_replay| {
|
||||
if has_replay {
|
||||
info!(target_id = %target_id, "Audit stream processing started");
|
||||
} else {
|
||||
info!(target_id = %target_id, "No store configured, skip audit stream processing");
|
||||
}
|
||||
}),
|
||||
None,
|
||||
Duration::from_millis(500),
|
||||
Duration::from_millis(500),
|
||||
"Stopping audit stream",
|
||||
);
|
||||
|
||||
Self {
|
||||
registry,
|
||||
replay_workers,
|
||||
runtime_adapter: Arc::new(runtime_adapter),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn replace_targets(&self, activation: RuntimeActivation<AuditEntry>) -> AuditResult<()> {
|
||||
let mut registry = self.registry.lock().await;
|
||||
let mut replay_workers = self.replay_workers.write().await;
|
||||
self.runtime_adapter
|
||||
.replace_runtime_targets(registry.runtime_manager_mut(), &mut replay_workers, activation)
|
||||
.await
|
||||
.map_err(crate::AuditError::Target)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown_runtime(
|
||||
&self,
|
||||
registry: &mut crate::AuditRegistry,
|
||||
replay_workers: &mut ReplayWorkerManager,
|
||||
) -> AuditResult<()> {
|
||||
self.runtime_adapter
|
||||
.shutdown(registry.runtime_manager_mut(), replay_workers)
|
||||
.await
|
||||
.map_err(crate::AuditError::Target)
|
||||
}
|
||||
|
||||
pub async fn activate_targets_with_replay(
|
||||
&self,
|
||||
targets: Vec<Box<dyn Target<AuditEntry> + Send + Sync>>,
|
||||
) -> RuntimeActivation<AuditEntry> {
|
||||
self.runtime_adapter.activate_with_replay(targets).await
|
||||
}
|
||||
|
||||
pub async fn stop_replay_workers(&self) {
|
||||
let mut replay_workers = self.replay_workers.write().await;
|
||||
self.runtime_adapter.stop_replay_workers(&mut replay_workers).await;
|
||||
}
|
||||
}
|
||||
+155
-94
@@ -12,28 +12,19 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::{
|
||||
AuditEntry, AuditError, AuditResult,
|
||||
factory::{
|
||||
KafkaTargetFactory, MQTTTargetFactory, NATSTargetFactory, PulsarTargetFactory, TargetFactory, WebhookTargetFactory,
|
||||
},
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use hashbrown::HashMap;
|
||||
use crate::{AuditEntry, AuditError, AuditResult, factory::builtin_target_plugins};
|
||||
use rustfs_config::audit::AUDIT_ROUTE_PREFIX;
|
||||
use rustfs_ecstore::config::{Config, KVS};
|
||||
use rustfs_targets::arn::TargetID;
|
||||
use rustfs_targets::{Target, TargetError, config::collect_target_configs, target::ChannelTargetType};
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, info};
|
||||
use rustfs_targets::{SharedTarget, Target, TargetError, TargetPluginRegistry, TargetRuntimeManager};
|
||||
use tracing::info;
|
||||
|
||||
/// Registry for managing audit targets
|
||||
pub struct AuditRegistry {
|
||||
/// Storage for created targets
|
||||
targets: HashMap<String, Box<dyn Target<AuditEntry> + Send + Sync>>,
|
||||
/// Factories for creating targets
|
||||
factories: HashMap<String, Box<dyn TargetFactory>>,
|
||||
targets: TargetRuntimeManager<AuditEntry>,
|
||||
/// Registered plugins for creating targets
|
||||
plugins: TargetPluginRegistry<AuditEntry>,
|
||||
}
|
||||
|
||||
impl Default for AuditRegistry {
|
||||
@@ -45,28 +36,17 @@ impl Default for AuditRegistry {
|
||||
impl AuditRegistry {
|
||||
/// Creates a new AuditRegistry
|
||||
pub fn new() -> Self {
|
||||
let mut registry = AuditRegistry {
|
||||
factories: HashMap::new(),
|
||||
targets: HashMap::new(),
|
||||
};
|
||||
let mut plugins = TargetPluginRegistry::new();
|
||||
plugins.register_all(builtin_target_plugins());
|
||||
|
||||
// Register built-in factories
|
||||
registry.register(ChannelTargetType::Webhook.as_str(), Box::new(WebhookTargetFactory));
|
||||
registry.register(ChannelTargetType::Mqtt.as_str(), Box::new(MQTTTargetFactory));
|
||||
registry.register(ChannelTargetType::Nats.as_str(), Box::new(NATSTargetFactory));
|
||||
registry.register(ChannelTargetType::Pulsar.as_str(), Box::new(PulsarTargetFactory));
|
||||
registry.register(ChannelTargetType::Kafka.as_str(), Box::new(KafkaTargetFactory));
|
||||
|
||||
registry
|
||||
AuditRegistry {
|
||||
targets: TargetRuntimeManager::new(),
|
||||
plugins,
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a new factory for a target type
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `target_type` - The type of the target (e.g., "webhook", "mqtt").
|
||||
/// * `factory` - The factory instance to create targets of this type.
|
||||
pub fn register(&mut self, target_type: &str, factory: Box<dyn TargetFactory>) {
|
||||
self.factories.insert(target_type.to_string(), factory);
|
||||
pub fn supports_target_type(&self, target_type: &str) -> bool {
|
||||
self.plugins.supports_target_type(target_type)
|
||||
}
|
||||
|
||||
/// Creates a target of the specified type with the given ID and configuration
|
||||
@@ -84,16 +64,7 @@ impl AuditRegistry {
|
||||
id: String,
|
||||
config: &KVS,
|
||||
) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
||||
let factory = self
|
||||
.factories
|
||||
.get(target_type)
|
||||
.ok_or_else(|| TargetError::Configuration(format!("Unknown target type: {target_type}")))?;
|
||||
|
||||
// Validate configuration before creating target
|
||||
factory.validate_config(&id, config)?;
|
||||
|
||||
// Create target
|
||||
factory.create_target(id, config).await
|
||||
self.plugins.create_target(target_type, id, config)
|
||||
}
|
||||
|
||||
/// Creates all targets from a configuration
|
||||
@@ -109,37 +80,10 @@ impl AuditRegistry {
|
||||
&self,
|
||||
config: &Config,
|
||||
) -> AuditResult<Vec<Box<dyn Target<AuditEntry> + Send + Sync>>> {
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
for (target_type, factory) in &self.factories {
|
||||
tracing::Span::current().record("target_type", target_type.as_str());
|
||||
info!("Start working on target types...");
|
||||
let valid_fields = factory.get_valid_fields();
|
||||
for (id, merged_config) in collect_target_configs(config, AUDIT_ROUTE_PREFIX, target_type, &valid_fields) {
|
||||
info!(instance_id = %id, "Target is enabled, ready to create a task");
|
||||
let tid = id.clone();
|
||||
let merged_config_arc = Arc::new(merged_config);
|
||||
tasks.push(async move {
|
||||
let result = factory.create_target(tid.clone(), &merged_config_arc).await;
|
||||
(tid, result)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut successful_targets = Vec::new();
|
||||
while let Some((id, result)) = tasks.next().await {
|
||||
match result {
|
||||
Ok(target) => {
|
||||
info!(target_type = %target.id().name, instance_id = %id, "Create a target successfully");
|
||||
successful_targets.push(target);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(instance_id = %id, error = %e, "Failed to create a target");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(count = successful_targets.len(), "All target processing completed");
|
||||
Ok(successful_targets)
|
||||
self.plugins
|
||||
.create_targets_from_config(config, AUDIT_ROUTE_PREFIX)
|
||||
.await
|
||||
.map_err(AuditError::from)
|
||||
}
|
||||
|
||||
/// Adds a target to the registry
|
||||
@@ -147,8 +91,14 @@ impl AuditRegistry {
|
||||
/// # Arguments
|
||||
/// * `id` - The identifier for the target.
|
||||
/// * `target` - The target instance to be added.
|
||||
pub fn add_target(&mut self, id: String, target: Box<dyn Target<AuditEntry> + Send + Sync>) {
|
||||
self.targets.insert(id, target);
|
||||
pub fn add_target(&mut self, _id: String, target: Box<dyn Target<AuditEntry> + Send + Sync>) {
|
||||
debug_assert_eq!(_id, target.id().to_string());
|
||||
self.targets.add_boxed(target);
|
||||
}
|
||||
|
||||
pub fn add_shared_target(&mut self, _id: String, target: SharedTarget<AuditEntry>) {
|
||||
debug_assert_eq!(_id, target.id().to_string());
|
||||
self.targets.add_arc(target);
|
||||
}
|
||||
|
||||
/// Removes a target from the registry
|
||||
@@ -158,8 +108,8 @@ impl AuditRegistry {
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Option<Box<dyn Target<AuditEntry> + Send + Sync>>` - The removed target if it existed.
|
||||
pub fn remove_target(&mut self, id: &str) -> Option<Box<dyn Target<AuditEntry> + Send + Sync>> {
|
||||
self.targets.remove(id)
|
||||
pub async fn remove_target(&mut self, id: &str) -> Option<rustfs_targets::SharedTarget<AuditEntry>> {
|
||||
self.targets.remove_and_close(id).await
|
||||
}
|
||||
|
||||
/// Gets a target from the registry
|
||||
@@ -169,13 +119,21 @@ impl AuditRegistry {
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Option<&(dyn Target<AuditEntry> + Send + Sync)>` - The target if it exists.
|
||||
pub fn get_target(&self, id: &str) -> Option<&(dyn Target<AuditEntry> + Send + Sync)> {
|
||||
self.targets.get(id).map(|t| t.as_ref())
|
||||
pub fn get_target(&self, id: &str) -> Option<rustfs_targets::SharedTarget<AuditEntry>> {
|
||||
self.targets.get(id)
|
||||
}
|
||||
|
||||
/// Lists cloned target values for runtime inspection without exposing mutable registry access.
|
||||
pub fn list_target_values(&self) -> Vec<Box<dyn Target<AuditEntry> + Send + Sync>> {
|
||||
self.targets.values().map(|target| target.clone_dyn()).collect()
|
||||
pub fn list_target_values(&self) -> Vec<rustfs_targets::SharedTarget<AuditEntry>> {
|
||||
self.targets.values()
|
||||
}
|
||||
|
||||
pub fn runtime_manager(&self) -> &TargetRuntimeManager<AuditEntry> {
|
||||
&self.targets
|
||||
}
|
||||
|
||||
pub fn runtime_manager_mut(&mut self) -> &mut TargetRuntimeManager<AuditEntry> {
|
||||
&mut self.targets
|
||||
}
|
||||
|
||||
/// Lists all target IDs
|
||||
@@ -183,7 +141,7 @@ impl AuditRegistry {
|
||||
/// # Returns
|
||||
/// * `Vec<String>` - A vector of all target IDs in the registry.
|
||||
pub fn list_targets(&self) -> Vec<String> {
|
||||
self.targets.keys().cloned().collect()
|
||||
self.targets.keys()
|
||||
}
|
||||
|
||||
/// Closes all targets and clears the registry
|
||||
@@ -191,20 +149,23 @@ impl AuditRegistry {
|
||||
/// # Returns
|
||||
/// * `AuditResult<()>` - Result indicating success or failure.
|
||||
pub async fn close_all(&mut self) -> AuditResult<()> {
|
||||
let mut errors = Vec::new();
|
||||
let mut first_error = None;
|
||||
|
||||
for (id, target) in self.targets.drain() {
|
||||
if let Err(e) = target.close().await {
|
||||
error!(target_id = %id, error = %e, "Failed to close audit target");
|
||||
errors.push(e);
|
||||
for target_id in self.targets.keys() {
|
||||
if let Some(target) = self.targets.remove(&target_id)
|
||||
&& let Err(err) = target.close().await
|
||||
{
|
||||
tracing::error!(target_id = %target_id, error = %err, "Failed to close target during shutdown");
|
||||
if first_error.is_none() {
|
||||
first_error = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(error) = errors.into_iter().next() {
|
||||
return Err(AuditError::Target(error));
|
||||
match first_error {
|
||||
Some(err) => Err(AuditError::Target(err)),
|
||||
None => Ok(()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a unique key for a target based on its type and ID
|
||||
@@ -279,7 +240,107 @@ impl AuditRegistry {
|
||||
target: Box<dyn Target<AuditEntry> + Send + Sync>,
|
||||
) -> AuditResult<()> {
|
||||
let key = self.create_key(target_type, target_id);
|
||||
self.targets.insert(key, target);
|
||||
debug_assert_eq!(key, target.id().to_string());
|
||||
self.targets.add_boxed(target);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::AuditRegistry;
|
||||
use crate::{AuditEntry, AuditError};
|
||||
use rustfs_targets::arn::TargetID;
|
||||
use rustfs_targets::store::{Key, Store};
|
||||
use rustfs_targets::target::{ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta};
|
||||
use rustfs_targets::{StoreError, Target, TargetError};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CloseTestTarget {
|
||||
id: TargetID,
|
||||
close_calls: Arc<AtomicUsize>,
|
||||
fail_on_close: bool,
|
||||
}
|
||||
|
||||
impl CloseTestTarget {
|
||||
fn new(id: TargetID, close_calls: Arc<AtomicUsize>, fail_on_close: bool) -> Self {
|
||||
Self {
|
||||
id,
|
||||
close_calls,
|
||||
fail_on_close,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Target<AuditEntry> for CloseTestTarget {
|
||||
fn id(&self) -> TargetID {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
async fn is_active(&self) -> Result<bool, TargetError> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn save(&self, _event: Arc<EntityTarget<AuditEntry>>) -> Result<(), TargetError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_raw_from_store(&self, _key: Key, _body: Vec<u8>, _meta: QueuedPayloadMeta) -> Result<(), TargetError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn close(&self) -> Result<(), TargetError> {
|
||||
self.close_calls.fetch_add(1, Ordering::SeqCst);
|
||||
if self.fail_on_close {
|
||||
Err(TargetError::Unknown("close failed".to_string()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn store(&self) -> Option<&(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync)> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_dyn(&self) -> Box<dyn Target<AuditEntry> + Send + Sync> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_registers_amqp_factory() {
|
||||
let registry = AuditRegistry::new();
|
||||
|
||||
assert!(registry.supports_target_type(ChannelTargetType::Amqp.as_str()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn close_all_returns_first_error_and_clears_targets() {
|
||||
let mut registry = AuditRegistry::new();
|
||||
let ok_calls = Arc::new(AtomicUsize::new(0));
|
||||
let fail_calls = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let ok_id = TargetID::new("ok".to_string(), "webhook".to_string());
|
||||
let fail_id = TargetID::new("fail".to_string(), "webhook".to_string());
|
||||
|
||||
registry.add_target(ok_id.to_string(), Box::new(CloseTestTarget::new(ok_id, Arc::clone(&ok_calls), false)));
|
||||
registry.add_target(
|
||||
fail_id.to_string(),
|
||||
Box::new(CloseTestTarget::new(fail_id, Arc::clone(&fail_calls), true)),
|
||||
);
|
||||
|
||||
let result = registry.close_all().await;
|
||||
|
||||
assert!(matches!(result, Err(AuditError::Target(TargetError::Unknown(_)))));
|
||||
assert_eq!(ok_calls.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(fail_calls.load(Ordering::SeqCst), 1);
|
||||
assert!(registry.list_targets().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
+191
-422
@@ -12,16 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::{AuditEntry, AuditError, AuditRegistry, AuditResult, observability};
|
||||
use hashbrown::HashMap;
|
||||
use rustfs_ecstore::config::Config;
|
||||
use rustfs_targets::{
|
||||
StoreError, Target, TargetError,
|
||||
store::{Key, Store},
|
||||
target::{EntityTarget, QueuedPayload},
|
||||
use crate::{
|
||||
AuditEntry, AuditError, AuditRegistry, AuditResult, observability,
|
||||
pipeline::{AuditPipeline, AuditRuntimeFacade, AuditRuntimeView},
|
||||
};
|
||||
use rustfs_ecstore::config::Config;
|
||||
use rustfs_targets::{ReplayWorkerManager, Target};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Mutex, RwLock, mpsc};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
@@ -49,7 +47,7 @@ pub struct AuditSystem {
|
||||
state: Arc<RwLock<AuditSystemState>>,
|
||||
config: Arc<RwLock<Option<Config>>>,
|
||||
/// Cancellation senders for active audit stream tasks (target_id -> cancel tx)
|
||||
stream_cancellers: Arc<RwLock<HashMap<String, mpsc::Sender<()>>>>,
|
||||
stream_cancellers: Arc<RwLock<ReplayWorkerManager>>,
|
||||
}
|
||||
|
||||
impl Default for AuditSystem {
|
||||
@@ -59,16 +57,68 @@ impl Default for AuditSystem {
|
||||
}
|
||||
|
||||
impl AuditSystem {
|
||||
fn pipeline(&self) -> AuditPipeline {
|
||||
AuditPipeline::new(self.registry.clone())
|
||||
}
|
||||
|
||||
fn runtime_view(&self) -> AuditRuntimeView {
|
||||
AuditRuntimeView::new(self.registry.clone())
|
||||
}
|
||||
|
||||
fn runtime_facade(&self) -> AuditRuntimeFacade {
|
||||
AuditRuntimeFacade::new(self.registry.clone(), self.stream_cancellers.clone())
|
||||
}
|
||||
|
||||
/// Creates a new audit system
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
registry: Arc::new(Mutex::new(AuditRegistry::new())),
|
||||
state: Arc::new(RwLock::new(AuditSystemState::Stopped)),
|
||||
config: Arc::new(RwLock::new(None)),
|
||||
stream_cancellers: Arc::new(RwLock::new(HashMap::new())),
|
||||
stream_cancellers: Arc::new(RwLock::new(ReplayWorkerManager::new())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_targets_from_config(&self, config: &Config) -> AuditResult<Vec<Box<dyn Target<AuditEntry> + Send + Sync>>> {
|
||||
let registry = self.registry.lock().await;
|
||||
registry.create_audit_targets_from_config(config).await
|
||||
}
|
||||
|
||||
async fn clear_runtime_targets(&self) -> AuditResult<()> {
|
||||
{
|
||||
let mut registry = self.registry.lock().await;
|
||||
let mut replay_workers = self.stream_cancellers.write().await;
|
||||
self.runtime_facade()
|
||||
.shutdown_runtime(&mut registry, &mut replay_workers)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut state = self.state.write().await;
|
||||
*state = AuditSystemState::Stopped;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn commit_runtime_targets(
|
||||
&self,
|
||||
targets: Vec<Box<dyn Target<AuditEntry> + Send + Sync>>,
|
||||
final_state: AuditSystemState,
|
||||
) -> AuditResult<()> {
|
||||
if targets.is_empty() {
|
||||
info!("No enabled audit targets found, keeping audit system stopped");
|
||||
self.clear_runtime_targets().await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(target_count = targets.len(), "Created audit targets successfully");
|
||||
|
||||
let activation = self.runtime_facade().activate_targets_with_replay(targets).await;
|
||||
self.runtime_facade().replace_targets(activation).await?;
|
||||
|
||||
let mut state = self.state.write().await;
|
||||
*state = final_state;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Starts the audit system with the given configuration
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -103,31 +153,14 @@ impl AuditSystem {
|
||||
*config_guard = Some(config.clone());
|
||||
}
|
||||
|
||||
// Create targets from configuration
|
||||
let mut registry = self.registry.lock().await;
|
||||
match registry.create_audit_targets_from_config(&config).await {
|
||||
match self.create_targets_from_config(&config).await {
|
||||
Ok(targets) => {
|
||||
if targets.is_empty() {
|
||||
info!("No enabled audit targets found, keeping audit system stopped");
|
||||
drop(registry);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
*state = AuditSystemState::Starting;
|
||||
}
|
||||
|
||||
info!(target_count = targets.len(), "Created audit targets successfully");
|
||||
|
||||
// Initialize all targets
|
||||
for target in targets {
|
||||
self.init_and_register_target(target, &mut registry).await;
|
||||
}
|
||||
|
||||
// Update state to running
|
||||
let mut state = self.state.write().await;
|
||||
*state = AuditSystemState::Running;
|
||||
self.commit_runtime_targets(targets, AuditSystemState::Running).await?;
|
||||
info!("Audit system started successfully");
|
||||
Ok(())
|
||||
}
|
||||
@@ -207,18 +240,10 @@ impl AuditSystem {
|
||||
info!("Stopping audit system");
|
||||
|
||||
// Stop all stream tasks first
|
||||
self.stop_all_streams().await;
|
||||
|
||||
// Close all targets
|
||||
let mut registry = self.registry.lock().await;
|
||||
if let Err(e) = registry.close_all().await {
|
||||
if let Err(e) = self.clear_runtime_targets().await {
|
||||
error!(error = %e, "Failed to close some audit targets");
|
||||
}
|
||||
|
||||
// Update state to stopped
|
||||
let mut state = self.state.write().await;
|
||||
*state = AuditSystemState::Stopped;
|
||||
|
||||
// Clear configuration
|
||||
let mut config_guard = self.config.write().await;
|
||||
*config_guard = None;
|
||||
@@ -248,8 +273,6 @@ impl AuditSystem {
|
||||
/// # Returns
|
||||
/// * `AuditResult<()>` - Result indicating success or failure
|
||||
pub async fn dispatch(&self, entry: Arc<AuditEntry>) -> AuditResult<()> {
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
let state = self.state.read().await;
|
||||
|
||||
match *state {
|
||||
@@ -262,77 +285,7 @@ impl AuditSystem {
|
||||
}
|
||||
}
|
||||
drop(state);
|
||||
|
||||
// Collect cloned targets under lock, then dispatch without holding it
|
||||
let targets: Vec<(String, Box<dyn Target<AuditEntry> + Send + Sync>)> = {
|
||||
let registry = self.registry.lock().await;
|
||||
let target_keys = registry.list_targets();
|
||||
|
||||
if target_keys.is_empty() {
|
||||
warn!("No audit targets configured for dispatch");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
target_keys
|
||||
.into_iter()
|
||||
.filter_map(|key| registry.get_target(&key).map(|t| (key, t.clone_dyn())))
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Dispatch to all targets concurrently (no lock held)
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for (target_key, target) in targets {
|
||||
let entity_target = EntityTarget {
|
||||
object_name: entry.api.name.clone().unwrap_or_default(),
|
||||
bucket_name: entry.api.bucket.clone().unwrap_or_default(),
|
||||
event_name: entry.event,
|
||||
data: (*entry).clone(),
|
||||
};
|
||||
|
||||
let task = async move {
|
||||
let result = target.save(Arc::new(entity_target)).await;
|
||||
(target_key, result)
|
||||
};
|
||||
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
// Execute all dispatch tasks
|
||||
let results = futures::future::join_all(tasks).await;
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let mut success_count = 0;
|
||||
|
||||
for (target_key, result) in results {
|
||||
match result {
|
||||
Ok(_) => {
|
||||
success_count += 1;
|
||||
observability::record_target_success();
|
||||
}
|
||||
Err(e) => {
|
||||
error!(target_id = %target_key, error = %e, "Failed to dispatch audit log to target");
|
||||
errors.push(e);
|
||||
observability::record_target_failure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dispatch_time = start_time.elapsed();
|
||||
|
||||
if errors.is_empty() {
|
||||
observability::record_audit_success(dispatch_time);
|
||||
} else {
|
||||
observability::record_audit_failure(dispatch_time);
|
||||
// Log errors but don't fail the entire dispatch
|
||||
warn!(
|
||||
error_count = errors.len(),
|
||||
success_count = success_count,
|
||||
"Some audit targets failed to receive log entry"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
self.pipeline().dispatch(entry).await
|
||||
}
|
||||
|
||||
/// Dispatches a batch of audit log entries to all active targets
|
||||
@@ -343,239 +296,12 @@ impl AuditSystem {
|
||||
/// # Returns
|
||||
/// * `AuditResult<()>` - Result indicating success or failure
|
||||
pub async fn dispatch_batch(&self, entries: Vec<Arc<AuditEntry>>) -> AuditResult<()> {
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
let state = self.state.read().await;
|
||||
if *state != AuditSystemState::Running {
|
||||
return Err(AuditError::NotInitialized("Audit system is not running".to_string()));
|
||||
}
|
||||
drop(state);
|
||||
|
||||
// Collect targets under lock, then dispatch without holding it
|
||||
let targets: Vec<(String, Box<dyn Target<AuditEntry> + Send + Sync>)> = {
|
||||
let registry = self.registry.lock().await;
|
||||
let target_keys = registry.list_targets();
|
||||
|
||||
if target_keys.is_empty() {
|
||||
warn!("No audit targets configured for batch dispatch");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
target_keys
|
||||
.into_iter()
|
||||
.filter_map(|key| registry.get_target(&key).map(|t| (key, t.clone_dyn())))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
for (target_key, target) in targets {
|
||||
let entries_clone: Vec<_> = entries.iter().map(Arc::clone).collect();
|
||||
let target_key_clone = target_key.clone();
|
||||
|
||||
let task = async move {
|
||||
let mut success_count = 0;
|
||||
let mut errors = Vec::new();
|
||||
for entry in entries_clone {
|
||||
let entity_target = EntityTarget {
|
||||
object_name: entry.api.name.clone().unwrap_or_default(),
|
||||
bucket_name: entry.api.bucket.clone().unwrap_or_default(),
|
||||
event_name: entry.event,
|
||||
data: (*entry).clone(),
|
||||
};
|
||||
match target.save(Arc::new(entity_target)).await {
|
||||
Ok(_) => success_count += 1,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
}
|
||||
(target_key_clone, success_count, errors)
|
||||
};
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
let results = futures::future::join_all(tasks).await;
|
||||
let mut total_success = 0;
|
||||
let mut total_errors = 0;
|
||||
for (_target_id, success_count, errors) in results {
|
||||
total_success += success_count;
|
||||
total_errors += errors.len();
|
||||
for e in errors {
|
||||
error!("Batch dispatch error: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let dispatch_time = start_time.elapsed();
|
||||
info!(
|
||||
"Batch dispatched {} entries, success: {}, errors: {}, time: {:?}",
|
||||
entries.len(),
|
||||
total_success,
|
||||
total_errors,
|
||||
dispatch_time
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stops all active audit stream tasks by sending cancellation signals.
|
||||
async fn stop_all_streams(&self) {
|
||||
let mut cancellers = self.stream_cancellers.write().await;
|
||||
for (target_id, cancel_tx) in cancellers.drain() {
|
||||
info!(target_id = %target_id, "Stopping audit stream");
|
||||
let _ = cancel_tx.send(()).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes a single target: runs init(), starts stream if store is present,
|
||||
/// and adds it to the registry. For store-backed targets, registration and stream
|
||||
/// startup proceed even if init() fails so queued entries can be drained later.
|
||||
async fn init_and_register_target(
|
||||
&self,
|
||||
target: Box<dyn Target<AuditEntry> + Send + Sync>,
|
||||
registry: &mut AuditRegistry,
|
||||
) -> Option<String> {
|
||||
let target_id = target.id().to_string();
|
||||
let has_store = target.store().is_some();
|
||||
|
||||
if let Err(e) = target.init().await {
|
||||
error!(target_id = %target_id, error = %e, "Failed to initialize audit target");
|
||||
// Non-store targets: init failure is fatal.
|
||||
if !has_store {
|
||||
return None;
|
||||
}
|
||||
// Store-backed targets: still register and start the stream so queued
|
||||
// entries can be drained when connectivity recovers.
|
||||
warn!(
|
||||
target_id = %target_id,
|
||||
"Proceeding with store-backed audit target despite init failure"
|
||||
);
|
||||
}
|
||||
|
||||
if target.is_enabled() {
|
||||
if let Some(store) = target.store() {
|
||||
info!(target_id = %target_id, "Start audit stream processing for target");
|
||||
let store_clone: Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send> = store.boxed_clone();
|
||||
let target_arc: Arc<dyn Target<AuditEntry> + Send + Sync> = Arc::from(target.clone_dyn());
|
||||
let cancel_tx = self.start_audit_stream_with_batching(store_clone, target_arc);
|
||||
|
||||
self.stream_cancellers.write().await.insert(target_id.clone(), cancel_tx);
|
||||
info!(target_id = %target_id, "Audit stream processing started");
|
||||
} else {
|
||||
info!(target_id = %target_id, "No store configured, skip audit stream processing");
|
||||
}
|
||||
} else {
|
||||
info!(target_id = %target_id, "Target disabled, skip audit stream processing");
|
||||
}
|
||||
|
||||
registry.add_target(target_id.clone(), target);
|
||||
Some(target_id)
|
||||
}
|
||||
|
||||
/// Starts the audit stream processing for a target with batching and retry logic
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `store` - The store from which to read audit entries
|
||||
/// * `target` - The target to which audit entries will be sent
|
||||
///
|
||||
/// This function spawns a background task that continuously reads audit entries from the provided store
|
||||
/// and attempts to send them to the specified target. It implements retry logic with exponential backoff
|
||||
fn start_audit_stream_with_batching(
|
||||
&self,
|
||||
store: Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send>,
|
||||
target: Arc<dyn Target<AuditEntry> + Send + Sync>,
|
||||
) -> mpsc::Sender<()> {
|
||||
let (cancel_tx, mut cancel_rx) = mpsc::channel(1);
|
||||
let state = self.state.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
info!("Starting audit stream for target: {}", target.id());
|
||||
|
||||
const MAX_RETRIES: usize = 5;
|
||||
const BASE_RETRY_DELAY: Duration = Duration::from_secs(2);
|
||||
|
||||
loop {
|
||||
// Check for cancellation signal
|
||||
if cancel_rx.try_recv().is_ok() {
|
||||
info!("Audit stream cancelled for target: {}", target.id());
|
||||
break;
|
||||
}
|
||||
|
||||
match *state.read().await {
|
||||
AuditSystemState::Running | AuditSystemState::Paused | AuditSystemState::Starting => {}
|
||||
_ => {
|
||||
info!("Audit stream stopped for target: {}", target.id());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let keys: Vec<Key> = store.list();
|
||||
if keys.is_empty() {
|
||||
tokio::select! {
|
||||
_ = sleep(Duration::from_millis(500)) => {},
|
||||
_ = cancel_rx.recv() => {
|
||||
info!("Audit stream cancelled during idle for target: {}", target.id());
|
||||
return;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for key in keys {
|
||||
if cancel_rx.try_recv().is_ok() {
|
||||
info!("Audit stream cancelled during processing for target: {}", target.id());
|
||||
return;
|
||||
}
|
||||
|
||||
let mut retries = 0usize;
|
||||
let mut success = false;
|
||||
|
||||
while retries < MAX_RETRIES && !success {
|
||||
match target.send_from_store(key.clone()).await {
|
||||
Ok(_) => {
|
||||
info!("Successfully sent audit entry, target: {}, key: {}", target.id(), key.to_string());
|
||||
observability::record_target_success();
|
||||
success = true;
|
||||
}
|
||||
Err(e) => {
|
||||
match &e {
|
||||
TargetError::NotConnected => {
|
||||
warn!("Target {} not connected, retrying...", target.id());
|
||||
}
|
||||
TargetError::Timeout(_) => {
|
||||
warn!("Timeout sending to target {}, retrying...", target.id());
|
||||
}
|
||||
TargetError::Dropped(reason) => {
|
||||
warn!("Dropped queued payload for target {}: {}", target.id(), reason);
|
||||
observability::record_target_failure();
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
error!("Permanent error for target {}: {}", target.id(), e);
|
||||
target.record_final_failure();
|
||||
observability::record_target_failure();
|
||||
break;
|
||||
}
|
||||
}
|
||||
retries += 1;
|
||||
let backoff = BASE_RETRY_DELAY * (1 << retries);
|
||||
sleep(backoff).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if retries >= MAX_RETRIES && !success {
|
||||
warn!("Max retries exceeded for key {}, target: {}, skipping", key.to_string(), target.id());
|
||||
target.record_final_failure();
|
||||
observability::record_target_failure();
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
});
|
||||
|
||||
cancel_tx
|
||||
self.pipeline().dispatch_batch(entries).await
|
||||
}
|
||||
|
||||
/// Enables a specific target
|
||||
@@ -586,15 +312,7 @@ impl AuditSystem {
|
||||
/// # Returns
|
||||
/// * `AuditResult<()>` - Result indicating success or failure
|
||||
pub async fn enable_target(&self, target_id: &str) -> AuditResult<()> {
|
||||
// This would require storing enabled/disabled state per target
|
||||
// For now, just check if target exists
|
||||
let registry = self.registry.lock().await;
|
||||
if registry.get_target(target_id).is_some() {
|
||||
info!(target_id = %target_id, "Target enabled");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AuditError::Configuration(format!("Target not found: {target_id}"), None))
|
||||
}
|
||||
self.runtime_view().enable_target(target_id).await
|
||||
}
|
||||
|
||||
/// Disables a specific target
|
||||
@@ -605,15 +323,7 @@ impl AuditSystem {
|
||||
/// # Returns
|
||||
/// * `AuditResult<()>` - Result indicating success or failure
|
||||
pub async fn disable_target(&self, target_id: &str) -> AuditResult<()> {
|
||||
// This would require storing enabled/disabled state per target
|
||||
// For now, just check if target exists
|
||||
let registry = self.registry.lock().await;
|
||||
if registry.get_target(target_id).is_some() {
|
||||
info!(target_id = %target_id, "Target disabled");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AuditError::Configuration(format!("Target not found: {target_id}"), None))
|
||||
}
|
||||
self.runtime_view().disable_target(target_id).await
|
||||
}
|
||||
|
||||
/// Removes a target from the system
|
||||
@@ -624,16 +334,7 @@ impl AuditSystem {
|
||||
/// # Returns
|
||||
/// * `AuditResult<()>` - Result indicating success or failure
|
||||
pub async fn remove_target(&self, target_id: &str) -> AuditResult<()> {
|
||||
let mut registry = self.registry.lock().await;
|
||||
if let Some(target) = registry.remove_target(target_id) {
|
||||
if let Err(e) = target.close().await {
|
||||
error!(target_id = %target_id, error = %e, "Failed to close removed target");
|
||||
}
|
||||
info!(target_id = %target_id, "Target removed");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AuditError::Configuration(format!("Target not found: {target_id}"), None))
|
||||
}
|
||||
self.runtime_view().remove_target(target_id).await
|
||||
}
|
||||
|
||||
/// Updates or inserts a target
|
||||
@@ -645,23 +346,7 @@ impl AuditSystem {
|
||||
/// # Returns
|
||||
/// * `AuditResult<()>` - Result indicating success or failure
|
||||
pub async fn upsert_target(&self, target_id: String, target: Box<dyn Target<AuditEntry> + Send + Sync>) -> AuditResult<()> {
|
||||
let mut registry = self.registry.lock().await;
|
||||
|
||||
// Initialize the target
|
||||
if let Err(e) = target.init().await {
|
||||
return Err(AuditError::Target(e));
|
||||
}
|
||||
|
||||
// Remove existing target if present
|
||||
if let Some(old_target) = registry.remove_target(&target_id)
|
||||
&& let Err(e) = old_target.close().await
|
||||
{
|
||||
error!(target_id = %target_id, error = %e, "Failed to close old target during upsert");
|
||||
}
|
||||
|
||||
registry.add_target(target_id.clone(), target);
|
||||
info!(target_id = %target_id, "Target upserted");
|
||||
Ok(())
|
||||
self.runtime_view().upsert_target(target_id, target).await
|
||||
}
|
||||
|
||||
/// Lists all targets
|
||||
@@ -669,33 +354,27 @@ impl AuditSystem {
|
||||
/// # Returns
|
||||
/// * `Vec<String>` - List of target IDs
|
||||
pub async fn list_targets(&self) -> Vec<String> {
|
||||
let registry = self.registry.lock().await;
|
||||
registry.list_targets()
|
||||
self.runtime_view().list_targets().await
|
||||
}
|
||||
|
||||
/// Returns cloned target values for read-only runtime inspection.
|
||||
pub async fn get_target_values(&self) -> Vec<Box<dyn Target<AuditEntry> + Send + Sync>> {
|
||||
let registry = self.registry.lock().await;
|
||||
registry.list_target_values()
|
||||
pub async fn get_target_values(&self) -> Vec<rustfs_targets::SharedTarget<AuditEntry>> {
|
||||
self.runtime_view().get_target_values().await
|
||||
}
|
||||
|
||||
/// Returns per-target delivery metrics for Prometheus collection.
|
||||
pub async fn snapshot_target_metrics(&self) -> Vec<AuditTargetMetricSnapshot> {
|
||||
let targets = self.get_target_values().await;
|
||||
let mut snapshots = Vec::with_capacity(targets.len());
|
||||
self.pipeline().snapshot_target_metrics().await
|
||||
}
|
||||
|
||||
for target in targets {
|
||||
let delivery = target.delivery_snapshot();
|
||||
snapshots.push(AuditTargetMetricSnapshot {
|
||||
failed_messages: delivery.failed_messages,
|
||||
queue_length: delivery.queue_length,
|
||||
target_id: target.id().to_string(),
|
||||
total_messages: delivery.total_messages,
|
||||
});
|
||||
}
|
||||
pub async fn snapshot_target_health(&self) -> Vec<rustfs_targets::RuntimeTargetHealthSnapshot> {
|
||||
self.pipeline().snapshot_target_health().await
|
||||
}
|
||||
|
||||
snapshots.sort_by(|a, b| a.target_id.cmp(&b.target_id));
|
||||
snapshots
|
||||
pub async fn runtime_status_snapshot(&self) -> rustfs_targets::RuntimeStatusSnapshot {
|
||||
let replay_workers = self.stream_cancellers.read().await;
|
||||
let registry = self.registry.lock().await;
|
||||
registry.runtime_manager().status_snapshot(&replay_workers)
|
||||
}
|
||||
|
||||
/// Gets information about a specific target
|
||||
@@ -706,8 +385,7 @@ impl AuditSystem {
|
||||
/// # Returns
|
||||
/// * `Option<String>` - Target ID if found
|
||||
pub async fn get_target(&self, target_id: &str) -> Option<String> {
|
||||
let registry = self.registry.lock().await;
|
||||
registry.get_target(target_id).map(|target| target.id().to_string())
|
||||
self.runtime_view().get_target(target_id).await
|
||||
}
|
||||
|
||||
/// Reloads configuration and updates targets
|
||||
@@ -722,32 +400,20 @@ impl AuditSystem {
|
||||
|
||||
observability::record_config_reload();
|
||||
|
||||
// Stop all existing stream tasks first
|
||||
self.stop_all_streams().await;
|
||||
|
||||
// Store new configuration
|
||||
{
|
||||
let mut config_guard = self.config.write().await;
|
||||
*config_guard = Some(new_config.clone());
|
||||
}
|
||||
|
||||
// Close all existing targets
|
||||
let mut registry = self.registry.lock().await;
|
||||
if let Err(e) = registry.close_all().await {
|
||||
error!(error = %e, "Failed to close existing targets during reload");
|
||||
}
|
||||
let final_state = match self.get_state().await {
|
||||
AuditSystemState::Paused => AuditSystemState::Paused,
|
||||
_ => AuditSystemState::Running,
|
||||
};
|
||||
|
||||
// Create new targets from updated configuration
|
||||
match registry.create_audit_targets_from_config(&new_config).await {
|
||||
match self.create_targets_from_config(&new_config).await {
|
||||
Ok(targets) => {
|
||||
info!(target_count = targets.len(), "Reloaded audit targets successfully");
|
||||
|
||||
for target in targets {
|
||||
if let Some(target_id) = self.init_and_register_target(target, &mut registry).await {
|
||||
info!(target_id = %target_id, "Target initialized (reload)");
|
||||
}
|
||||
}
|
||||
|
||||
self.commit_runtime_targets(targets, final_state).await?;
|
||||
info!("Audit configuration reloaded successfully");
|
||||
Ok(())
|
||||
}
|
||||
@@ -779,3 +445,106 @@ impl AuditSystem {
|
||||
observability::reset_metrics().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AuditSystem, AuditSystemState};
|
||||
use async_trait::async_trait;
|
||||
use rustfs_targets::ReplayWorkerManager;
|
||||
use rustfs_targets::arn::TargetID;
|
||||
use rustfs_targets::store::{Key, Store};
|
||||
use rustfs_targets::target::{EntityTarget, QueuedPayload, QueuedPayloadMeta};
|
||||
use rustfs_targets::{StoreError, Target, TargetError};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestTarget {
|
||||
close_calls: Arc<AtomicUsize>,
|
||||
id: TargetID,
|
||||
}
|
||||
|
||||
impl TestTarget {
|
||||
fn new(id: &str, name: &str) -> Self {
|
||||
Self {
|
||||
close_calls: Arc::new(AtomicUsize::new(0)),
|
||||
id: TargetID::new(id.to_string(), name.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E> Target<E> for TestTarget
|
||||
where
|
||||
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
|
||||
{
|
||||
fn id(&self) -> TargetID {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
async fn is_active(&self) -> Result<bool, TargetError> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn save(&self, _event: Arc<EntityTarget<E>>) -> Result<(), TargetError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_raw_from_store(&self, _key: Key, _body: Vec<u8>, _meta: QueuedPayloadMeta) -> Result<(), TargetError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn close(&self) -> Result<(), TargetError> {
|
||||
self.close_calls.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn store(&self) -> Option<&(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync)> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_dyn(&self) -> Box<dyn Target<E> + Send + Sync> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reload_with_empty_config_stops_existing_runtime() {
|
||||
let system = AuditSystem::new();
|
||||
let target = TestTarget::new("primary", "webhook");
|
||||
let close_calls = Arc::clone(&target.close_calls);
|
||||
|
||||
{
|
||||
let mut registry = system.registry.lock().await;
|
||||
registry.add_target("primary:webhook".to_string(), Box::new(target));
|
||||
}
|
||||
{
|
||||
let mut state = system.state.write().await;
|
||||
*state = AuditSystemState::Running;
|
||||
}
|
||||
{
|
||||
let mut replay_workers = system.stream_cancellers.write().await;
|
||||
let (cancel_tx, _cancel_rx) = mpsc::channel(1);
|
||||
replay_workers.insert("primary:webhook".to_string(), cancel_tx);
|
||||
assert_eq!(replay_workers.len(), 1);
|
||||
}
|
||||
|
||||
system
|
||||
.reload_config(rustfs_ecstore::config::Config(HashMap::new()))
|
||||
.await
|
||||
.expect("reload with empty config should succeed");
|
||||
|
||||
assert_eq!(system.get_state().await, AuditSystemState::Stopped);
|
||||
assert!(system.list_targets().await.is_empty());
|
||||
assert_eq!(system.runtime_status_snapshot().await, ReplayWorkerManager::new().snapshot(0));
|
||||
assert_eq!(close_calls.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(*system.config.read().await, Some(rustfs_ecstore::config::Config(HashMap::new())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use rustfs_audit::{AuditPipeline, AuditRegistry, AuditRuntimeFacade, AuditRuntimeView};
|
||||
use rustfs_targets::arn::TargetID;
|
||||
use rustfs_targets::store::{Key, Store};
|
||||
use rustfs_targets::target::{EntityTarget, QueuedPayload, QueuedPayloadMeta};
|
||||
use rustfs_targets::{StoreError, Target, TargetError};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestTarget {
|
||||
close_calls: Arc<AtomicUsize>,
|
||||
id: TargetID,
|
||||
init_calls: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl TestTarget {
|
||||
fn new(id: &str, name: &str) -> Self {
|
||||
Self {
|
||||
close_calls: Arc::new(AtomicUsize::new(0)),
|
||||
id: TargetID::new(id.to_string(), name.to_string()),
|
||||
init_calls: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E> Target<E> for TestTarget
|
||||
where
|
||||
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
|
||||
{
|
||||
fn id(&self) -> TargetID {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
async fn is_active(&self) -> Result<bool, TargetError> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn save(&self, _event: Arc<EntityTarget<E>>) -> Result<(), TargetError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_raw_from_store(&self, _key: Key, _body: Vec<u8>, _meta: QueuedPayloadMeta) -> Result<(), TargetError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn close(&self) -> Result<(), TargetError> {
|
||||
self.close_calls.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn store(&self) -> Option<&(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync)> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_dyn(&self) -> Box<dyn Target<E> + Send + Sync> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
async fn init(&self) -> Result<(), TargetError> {
|
||||
self.init_calls.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn audit_runtime_view_lists_empty_targets() {
|
||||
let registry = Arc::new(Mutex::new(AuditRegistry::new()));
|
||||
let runtime_view = AuditRuntimeView::new(registry);
|
||||
|
||||
assert!(runtime_view.list_targets().await.is_empty());
|
||||
assert!(runtime_view.get_target_values().await.is_empty());
|
||||
assert!(runtime_view.get_target("missing").await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn audit_pipeline_reports_empty_runtime_snapshots() {
|
||||
let registry = Arc::new(Mutex::new(AuditRegistry::new()));
|
||||
let pipeline = AuditPipeline::new(registry);
|
||||
|
||||
assert!(pipeline.snapshot_target_metrics().await.is_empty());
|
||||
assert!(pipeline.snapshot_target_health().await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn audit_runtime_facade_stops_empty_replay_workers() {
|
||||
let registry = Arc::new(Mutex::new(AuditRegistry::new()));
|
||||
let replay_workers = Arc::new(RwLock::new(rustfs_targets::ReplayWorkerManager::new()));
|
||||
let facade = AuditRuntimeFacade::new(registry, replay_workers);
|
||||
|
||||
facade.stop_replay_workers().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn audit_runtime_facade_activates_empty_target_list() {
|
||||
let registry = Arc::new(Mutex::new(AuditRegistry::new()));
|
||||
let replay_workers = Arc::new(RwLock::new(rustfs_targets::ReplayWorkerManager::new()));
|
||||
let facade = AuditRuntimeFacade::new(registry, replay_workers);
|
||||
|
||||
let activation = facade.activate_targets_with_replay(Vec::new()).await;
|
||||
assert!(activation.targets.is_empty());
|
||||
assert_eq!(activation.replay_workers.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn audit_runtime_view_upsert_and_remove_target() {
|
||||
let registry = Arc::new(Mutex::new(AuditRegistry::new()));
|
||||
let runtime_view = AuditRuntimeView::new(registry.clone());
|
||||
let target = TestTarget::new("primary", "webhook");
|
||||
let init_calls = Arc::clone(&target.init_calls);
|
||||
let close_calls = Arc::clone(&target.close_calls);
|
||||
|
||||
runtime_view
|
||||
.upsert_target("primary:webhook".to_string(), Box::new(target))
|
||||
.await
|
||||
.expect("upsert should succeed");
|
||||
|
||||
assert_eq!(runtime_view.list_targets().await, vec!["primary:webhook".to_string()]);
|
||||
assert_eq!(init_calls.load(Ordering::SeqCst), 1);
|
||||
|
||||
runtime_view
|
||||
.remove_target("primary:webhook")
|
||||
.await
|
||||
.expect("remove should succeed");
|
||||
|
||||
assert!(runtime_view.list_targets().await.is_empty());
|
||||
assert_eq!(close_calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn audit_runtime_facade_replace_targets_commits_runtime_state() {
|
||||
let registry = Arc::new(Mutex::new(AuditRegistry::new()));
|
||||
let replay_workers = Arc::new(RwLock::new(rustfs_targets::ReplayWorkerManager::new()));
|
||||
let facade = AuditRuntimeFacade::new(registry.clone(), replay_workers.clone());
|
||||
let target = TestTarget::new("primary", "webhook");
|
||||
let activation = rustfs_targets::RuntimeActivation {
|
||||
replay_workers: rustfs_targets::ReplayWorkerManager::new(),
|
||||
targets: vec![Arc::new(target) as rustfs_targets::SharedTarget<rustfs_audit::AuditEntry>],
|
||||
};
|
||||
|
||||
facade
|
||||
.replace_targets(activation)
|
||||
.await
|
||||
.expect("replace_targets should succeed");
|
||||
|
||||
let runtime_view = AuditRuntimeView::new(registry);
|
||||
assert_eq!(runtime_view.list_targets().await, vec!["primary:webhook".to_string()]);
|
||||
assert_eq!(replay_workers.read().await.len(), 0);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ homepage.workspace = true
|
||||
description = "Checksum calculation and verification callbacks for HTTP request and response bodies sent by service clients generated by RustFS, ensuring data integrity and authenticity."
|
||||
keywords = ["checksum-calculation", "verification", "integrity", "authenticity", "rustfs"]
|
||||
categories = ["web-programming", "development-tools", "network-programming"]
|
||||
documentation = "https://docs.rs/rustfs-signer/latest/rustfs_checksum/"
|
||||
documentation = "https://docs.rs/rustfs-checksums/latest/rustfs_checksum/"
|
||||
|
||||
[dependencies]
|
||||
bytes = { workspace = true }
|
||||
|
||||
@@ -33,12 +33,8 @@ tonic = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
metrics = { workspace = true }
|
||||
rustfs-madmin = { workspace = true }
|
||||
rustfs-filemeta = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
path-clean = { workspace = true }
|
||||
rmp-serde = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
s3s = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use tokio::sync::RwLock;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
@@ -27,6 +28,7 @@ pub static GLOBAL_RUSTFS_ADDR: LazyLock<RwLock<String>> = LazyLock::new(|| RwLoc
|
||||
pub static GLOBAL_CONN_MAP: LazyLock<RwLock<HashMap<String, Channel>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||
pub static GLOBAL_ROOT_CERT: LazyLock<RwLock<Option<Vec<u8>>>> = LazyLock::new(|| RwLock::new(None));
|
||||
pub static GLOBAL_MTLS_IDENTITY: LazyLock<RwLock<Option<MtlsIdentityPem>>> = LazyLock::new(|| RwLock::new(None));
|
||||
pub static GLOBAL_OUTBOUND_TLS_GENERATION: LazyLock<AtomicU64> = LazyLock::new(|| AtomicU64::new(0));
|
||||
/// Global initialization time of the RustFS node.
|
||||
pub static GLOBAL_INIT_TIME: LazyLock<RwLock<Option<DateTime<Utc>>>> = LazyLock::new(|| RwLock::new(None));
|
||||
|
||||
@@ -88,6 +90,16 @@ pub async fn set_global_mtls_identity(identity: Option<MtlsIdentityPem>) {
|
||||
*GLOBAL_MTLS_IDENTITY.write().await = identity;
|
||||
}
|
||||
|
||||
/// Set the global outbound TLS generation.
|
||||
pub fn set_global_outbound_tls_generation(generation: u64) {
|
||||
GLOBAL_OUTBOUND_TLS_GENERATION.store(generation, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Get the global outbound TLS generation.
|
||||
pub fn get_global_outbound_tls_generation() -> u64 {
|
||||
GLOBAL_OUTBOUND_TLS_GENERATION.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Evict a stale/dead connection from the global connection cache.
|
||||
/// This is critical for cluster recovery when a node dies unexpectedly (e.g., power-off).
|
||||
/// By removing the cached connection, subsequent requests will establish a fresh connection.
|
||||
|
||||
@@ -260,9 +260,17 @@ pub enum HealChannelCommand {
|
||||
response_tx: oneshot::Sender<Result<HealAdmissionResult, String>>,
|
||||
},
|
||||
/// Query heal task status
|
||||
Query { heal_path: String, client_token: String },
|
||||
Query {
|
||||
heal_path: String,
|
||||
client_token: String,
|
||||
response_tx: oneshot::Sender<Result<HealChannelResponse, String>>,
|
||||
},
|
||||
/// Cancel heal task
|
||||
Cancel { heal_path: String },
|
||||
Cancel {
|
||||
heal_path: String,
|
||||
client_token: String,
|
||||
response_tx: oneshot::Sender<Result<HealChannelResponse, String>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Heal request from admin to ahm
|
||||
@@ -379,7 +387,9 @@ fn heal_response_sender() -> &'static HealResponseSender {
|
||||
|
||||
/// Publish a heal response to subscribers.
|
||||
pub fn publish_heal_response(response: HealChannelResponse) -> Result<(), broadcast::error::SendError<HealChannelResponse>> {
|
||||
heal_response_sender().send(response).map(|_| ())
|
||||
let sender = heal_response_sender();
|
||||
let _ = sender.send(response);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subscribe to heal responses.
|
||||
@@ -405,14 +415,36 @@ pub async fn send_heal_request(request: HealChannelRequest) -> Result<(), String
|
||||
}
|
||||
}
|
||||
|
||||
async fn receive_heal_channel_response(
|
||||
response_rx: oneshot::Receiver<Result<HealChannelResponse, String>>,
|
||||
) -> Result<HealChannelResponse, String> {
|
||||
response_rx
|
||||
.await
|
||||
.map_err(|e| format!("Failed to receive heal channel response: {e}"))?
|
||||
}
|
||||
|
||||
/// Send heal query request
|
||||
pub async fn query_heal_status(heal_path: String, client_token: String) -> Result<(), String> {
|
||||
send_heal_command(HealChannelCommand::Query { heal_path, client_token }).await
|
||||
pub async fn query_heal_status(heal_path: String, client_token: String) -> Result<HealChannelResponse, String> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
send_heal_command(HealChannelCommand::Query {
|
||||
heal_path,
|
||||
client_token,
|
||||
response_tx,
|
||||
})
|
||||
.await?;
|
||||
receive_heal_channel_response(response_rx).await
|
||||
}
|
||||
|
||||
/// Send heal cancel request
|
||||
pub async fn cancel_heal_task(heal_path: String) -> Result<(), String> {
|
||||
send_heal_command(HealChannelCommand::Cancel { heal_path }).await
|
||||
pub async fn cancel_heal_task(heal_path: String, client_token: String) -> Result<HealChannelResponse, String> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
send_heal_command(HealChannelCommand::Cancel {
|
||||
heal_path,
|
||||
client_token,
|
||||
response_tx,
|
||||
})
|
||||
.await?;
|
||||
receive_heal_channel_response(response_rx).await
|
||||
}
|
||||
|
||||
/// Create a new heal request
|
||||
@@ -626,5 +658,10 @@ mod tests {
|
||||
let received = receiver.recv().await.expect("should receive heal response");
|
||||
assert_eq!(received.request_id, response.request_id);
|
||||
assert!(received.success);
|
||||
|
||||
drop(receiver);
|
||||
let response = create_heal_response("req-no-subscriber".to_string(), true, None, None);
|
||||
|
||||
publish_heal_response(response).expect("publish without subscribers should be ignored");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
use metrics::{counter, gauge};
|
||||
use std::sync::{
|
||||
Arc, LazyLock,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct InternodeMetricsSnapshot {
|
||||
pub sent_bytes_total: u64,
|
||||
pub recv_bytes_total: u64,
|
||||
pub outgoing_requests_total: u64,
|
||||
pub incoming_requests_total: u64,
|
||||
pub errors_total: u64,
|
||||
pub dial_errors_total: u64,
|
||||
pub dial_avg_time_nanos: u64,
|
||||
pub last_dial_unix_millis: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InternodeMetrics {
|
||||
sent_bytes_total: AtomicU64,
|
||||
recv_bytes_total: AtomicU64,
|
||||
outgoing_requests_total: AtomicU64,
|
||||
incoming_requests_total: AtomicU64,
|
||||
errors_total: AtomicU64,
|
||||
dial_errors_total: AtomicU64,
|
||||
dial_total_time_nanos: AtomicU64,
|
||||
dial_samples_total: AtomicU64,
|
||||
last_dial_unix_millis: AtomicU64,
|
||||
}
|
||||
|
||||
impl InternodeMetrics {
|
||||
pub fn record_sent_bytes(&self, bytes: usize) {
|
||||
let bytes = bytes as u64;
|
||||
if bytes == 0 {
|
||||
return;
|
||||
}
|
||||
self.sent_bytes_total.fetch_add(bytes, Ordering::Relaxed);
|
||||
counter!("rustfs_system_network_internode_sent_bytes_total").increment(bytes);
|
||||
}
|
||||
|
||||
pub fn record_recv_bytes(&self, bytes: usize) {
|
||||
let bytes = bytes as u64;
|
||||
if bytes == 0 {
|
||||
return;
|
||||
}
|
||||
self.recv_bytes_total.fetch_add(bytes, Ordering::Relaxed);
|
||||
counter!("rustfs_system_network_internode_recv_bytes_total").increment(bytes);
|
||||
}
|
||||
|
||||
pub fn record_outgoing_request(&self) {
|
||||
self.outgoing_requests_total.fetch_add(1, Ordering::Relaxed);
|
||||
counter!("rustfs_system_network_internode_requests_outgoing_total").increment(1);
|
||||
}
|
||||
|
||||
pub fn record_incoming_request(&self) {
|
||||
self.incoming_requests_total.fetch_add(1, Ordering::Relaxed);
|
||||
counter!("rustfs_system_network_internode_requests_incoming_total").increment(1);
|
||||
}
|
||||
|
||||
pub fn record_error(&self) {
|
||||
self.errors_total.fetch_add(1, Ordering::Relaxed);
|
||||
counter!("rustfs_system_network_internode_errors_total").increment(1);
|
||||
}
|
||||
|
||||
pub fn record_dial_result(&self, duration: Duration, success: bool) {
|
||||
let elapsed_nanos = duration.as_nanos().min(u128::from(u64::MAX)) as u64;
|
||||
self.dial_total_time_nanos.fetch_add(elapsed_nanos, Ordering::Relaxed);
|
||||
let samples = self.dial_samples_total.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
let total = self.dial_total_time_nanos.load(Ordering::Relaxed);
|
||||
gauge!("rustfs_system_network_internode_dial_avg_time_nanos").set(total as f64 / samples as f64);
|
||||
|
||||
if !success {
|
||||
self.dial_errors_total.fetch_add(1, Ordering::Relaxed);
|
||||
counter!("rustfs_system_network_internode_dial_errors_total").increment(1);
|
||||
}
|
||||
|
||||
let now_ms = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
.min(u128::from(u64::MAX)) as u64;
|
||||
self.last_dial_unix_millis.store(now_ms, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> InternodeMetricsSnapshot {
|
||||
let dial_samples_total = self.dial_samples_total.load(Ordering::Relaxed);
|
||||
let dial_total_time_nanos = self.dial_total_time_nanos.load(Ordering::Relaxed);
|
||||
let dial_avg_time_nanos = dial_total_time_nanos.checked_div(dial_samples_total).unwrap_or(0);
|
||||
|
||||
InternodeMetricsSnapshot {
|
||||
sent_bytes_total: self.sent_bytes_total.load(Ordering::Relaxed),
|
||||
recv_bytes_total: self.recv_bytes_total.load(Ordering::Relaxed),
|
||||
outgoing_requests_total: self.outgoing_requests_total.load(Ordering::Relaxed),
|
||||
incoming_requests_total: self.incoming_requests_total.load(Ordering::Relaxed),
|
||||
errors_total: self.errors_total.load(Ordering::Relaxed),
|
||||
dial_errors_total: self.dial_errors_total.load(Ordering::Relaxed),
|
||||
dial_avg_time_nanos,
|
||||
last_dial_unix_millis: self.last_dial_unix_millis.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn reset_for_test(&self) {
|
||||
self.sent_bytes_total.store(0, Ordering::Relaxed);
|
||||
self.recv_bytes_total.store(0, Ordering::Relaxed);
|
||||
self.outgoing_requests_total.store(0, Ordering::Relaxed);
|
||||
self.incoming_requests_total.store(0, Ordering::Relaxed);
|
||||
self.errors_total.store(0, Ordering::Relaxed);
|
||||
self.dial_errors_total.store(0, Ordering::Relaxed);
|
||||
self.dial_total_time_nanos.store(0, Ordering::Relaxed);
|
||||
self.dial_samples_total.store(0, Ordering::Relaxed);
|
||||
self.last_dial_unix_millis.store(0, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn global_internode_metrics() -> &'static Arc<InternodeMetrics> {
|
||||
static GLOBAL_INTERNODE_METRICS: LazyLock<Arc<InternodeMetrics>> = LazyLock::new(|| Arc::new(InternodeMetrics::default()));
|
||||
&GLOBAL_INTERNODE_METRICS
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn snapshot_reports_recorded_values() {
|
||||
let metrics = global_internode_metrics();
|
||||
metrics.reset_for_test();
|
||||
|
||||
metrics.record_sent_bytes(64);
|
||||
metrics.record_recv_bytes(32);
|
||||
metrics.record_outgoing_request();
|
||||
metrics.record_incoming_request();
|
||||
metrics.record_error();
|
||||
metrics.record_dial_result(Duration::from_millis(9), true);
|
||||
metrics.record_dial_result(Duration::from_millis(3), false);
|
||||
|
||||
let snapshot = metrics.snapshot();
|
||||
assert_eq!(snapshot.sent_bytes_total, 64);
|
||||
assert_eq!(snapshot.recv_bytes_total, 32);
|
||||
assert_eq!(snapshot.outgoing_requests_total, 1);
|
||||
assert_eq!(snapshot.incoming_requests_total, 1);
|
||||
assert_eq!(snapshot.errors_total, 1);
|
||||
assert_eq!(snapshot.dial_errors_total, 1);
|
||||
assert_eq!(snapshot.dial_avg_time_nanos, 6_000_000);
|
||||
assert!(snapshot.last_dial_unix_millis > 0);
|
||||
|
||||
metrics.reset_for_test();
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
pub mod bucket_stats;
|
||||
pub mod capacity_scope;
|
||||
// pub mod error;
|
||||
pub mod data_usage;
|
||||
pub mod globals;
|
||||
pub mod heal_channel;
|
||||
pub mod internode_metrics;
|
||||
pub mod last_minute;
|
||||
pub mod metrics;
|
||||
mod readiness;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
use crate::last_minute::{AccElem, LastMinuteLatency};
|
||||
use chrono::{DateTime, Utc};
|
||||
use rustfs_madmin::metrics::{ScannerMetrics as M_ScannerMetrics, TimedAction};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -351,6 +350,32 @@ pub struct CurrentCycle {
|
||||
pub started: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ScannerTimedAction {
|
||||
pub count: u64,
|
||||
pub acc_time: u64,
|
||||
pub bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct ScannerLastMinute {
|
||||
pub actions: HashMap<String, ScannerTimedAction>,
|
||||
pub ilm: HashMap<String, ScannerTimedAction>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct ScannerMetricsReport {
|
||||
pub collected_at: DateTime<Utc>,
|
||||
pub current_cycle: u64,
|
||||
pub current_started: DateTime<Utc>,
|
||||
pub cycles_completed_at: Vec<DateTime<Utc>>,
|
||||
pub ongoing_buckets: usize,
|
||||
pub life_time_ops: HashMap<String, u64>,
|
||||
pub life_time_ilm: HashMap<String, u64>,
|
||||
pub last_minute: ScannerLastMinute,
|
||||
pub active_paths: Vec<String>,
|
||||
}
|
||||
|
||||
impl CurrentCycle {
|
||||
pub fn unmarshal(&mut self, buf: &[u8]) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
*self = rmp_serde::from_slice(buf)?;
|
||||
@@ -572,8 +597,8 @@ impl Metrics {
|
||||
}
|
||||
|
||||
/// Build a full metrics report snapshot.
|
||||
pub async fn report(&self) -> M_ScannerMetrics {
|
||||
let mut m = M_ScannerMetrics::default();
|
||||
pub async fn report(&self) -> ScannerMetricsReport {
|
||||
let mut m = ScannerMetricsReport::default();
|
||||
|
||||
if let Some(cycle) = self.get_cycle().await {
|
||||
m.current_cycle = cycle.current;
|
||||
@@ -606,7 +631,7 @@ impl Metrics {
|
||||
{
|
||||
m.last_minute.actions.insert(
|
||||
metric.as_str().to_string(),
|
||||
TimedAction {
|
||||
ScannerTimedAction {
|
||||
count: last_min.n,
|
||||
acc_time: last_min.total,
|
||||
bytes: last_min.size,
|
||||
@@ -633,7 +658,7 @@ impl Metrics {
|
||||
{
|
||||
m.last_minute.ilm.insert(
|
||||
action.as_str().to_string(),
|
||||
TimedAction {
|
||||
ScannerTimedAction {
|
||||
count: last_min.n,
|
||||
acc_time: last_min.total,
|
||||
bytes: last_min.size,
|
||||
|
||||
@@ -14,15 +14,17 @@
|
||||
|
||||
//! Backpressure management
|
||||
|
||||
use rustfs_io_core::{BackpressureMonitor as CoreBackpressureMonitor, BackpressureState};
|
||||
use rustfs_io_core::{
|
||||
BackpressureConfig as CoreBackpressureConfig, BackpressureMonitor as CoreBackpressureMonitor, BackpressureState,
|
||||
};
|
||||
use rustfs_io_metrics::backpressure_metrics;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::io::{DuplexStream, duplex};
|
||||
|
||||
/// Backpressure configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BackpressureConfig {
|
||||
/// Facade policy for duplex-pipe watermark backpressure.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PipeBackpressurePolicy {
|
||||
/// Buffer size in bytes
|
||||
pub buffer_size: usize,
|
||||
/// High watermark percentage
|
||||
@@ -31,7 +33,7 @@ pub struct BackpressureConfig {
|
||||
pub low_watermark: u32,
|
||||
}
|
||||
|
||||
impl Default for BackpressureConfig {
|
||||
impl Default for PipeBackpressurePolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buffer_size: 4 * 1024 * 1024, // 4MB
|
||||
@@ -41,7 +43,7 @@ impl Default for BackpressureConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl BackpressureConfig {
|
||||
impl PipeBackpressurePolicy {
|
||||
/// Calculate high watermark threshold in bytes
|
||||
pub fn high_watermark_bytes(&self) -> usize {
|
||||
(self.buffer_size as u64 * self.high_watermark as u64 / 100) as usize
|
||||
@@ -51,42 +53,59 @@ impl BackpressureConfig {
|
||||
pub fn low_watermark_bytes(&self) -> usize {
|
||||
(self.buffer_size as u64 * self.low_watermark as u64 / 100) as usize
|
||||
}
|
||||
|
||||
/// Convert the facade policy into the reusable io-core admission-pressure config.
|
||||
///
|
||||
/// The concurrency layer still owns duplex buffer sizing, but the shared
|
||||
/// overload/admission primitive lives in `io-core`.
|
||||
pub fn to_core_config(&self) -> CoreBackpressureConfig {
|
||||
CoreBackpressureConfig {
|
||||
max_concurrent: 32,
|
||||
high_water_mark: self.high_watermark as f64 / 100.0,
|
||||
low_water_mark: self.low_watermark as f64 / 100.0,
|
||||
cooldown: std::time::Duration::from_millis(100),
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Backpressure manager
|
||||
pub struct BackpressureManager {
|
||||
config: BackpressureConfig,
|
||||
config: PipeBackpressurePolicy,
|
||||
core_config: CoreBackpressureConfig,
|
||||
monitor: Arc<CoreBackpressureMonitor>,
|
||||
}
|
||||
|
||||
impl BackpressureManager {
|
||||
/// Create a new backpressure manager
|
||||
pub fn new(buffer_size: usize, high_watermark: u32, low_watermark: u32) -> Self {
|
||||
let config = BackpressureConfig {
|
||||
Self::from_policy(PipeBackpressurePolicy {
|
||||
buffer_size,
|
||||
high_watermark,
|
||||
low_watermark,
|
||||
};
|
||||
|
||||
let core_config = rustfs_io_core::BackpressureConfig {
|
||||
max_concurrent: 32,
|
||||
high_water_mark: high_watermark as f64 / 100.0,
|
||||
low_water_mark: low_watermark as f64 / 100.0,
|
||||
cooldown: std::time::Duration::from_millis(100),
|
||||
enabled: true,
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new backpressure manager from the facade policy type.
|
||||
pub fn from_policy(config: PipeBackpressurePolicy) -> Self {
|
||||
let core_config = config.to_core_config();
|
||||
Self {
|
||||
config,
|
||||
core_config: core_config.clone(),
|
||||
monitor: Arc::new(CoreBackpressureMonitor::new(core_config)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &BackpressureConfig {
|
||||
pub fn config(&self) -> &PipeBackpressurePolicy {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Get the derived io-core admission-pressure configuration.
|
||||
pub fn core_config(&self) -> &CoreBackpressureConfig {
|
||||
&self.core_config
|
||||
}
|
||||
|
||||
/// Get the monitor
|
||||
pub fn monitor(&self) -> Arc<CoreBackpressureMonitor> {
|
||||
self.monitor.clone()
|
||||
@@ -94,7 +113,7 @@ impl BackpressureManager {
|
||||
|
||||
/// Create a backpressure pipe
|
||||
pub fn create_pipe(&self) -> BackpressurePipe {
|
||||
BackpressurePipe::new(self.config.clone(), self.monitor.clone())
|
||||
BackpressurePipe::new(self.config, self.monitor.clone())
|
||||
}
|
||||
|
||||
/// Get current state
|
||||
@@ -112,13 +131,24 @@ impl BackpressureManager {
|
||||
pub struct BackpressurePipe {
|
||||
reader: DuplexStream,
|
||||
writer: DuplexStream,
|
||||
config: BackpressureConfig,
|
||||
config: PipeBackpressurePolicy,
|
||||
monitor: Arc<CoreBackpressureMonitor>,
|
||||
created_at: Instant,
|
||||
}
|
||||
|
||||
/// Shared pipe metadata snapshot for facade-level backpressure pipes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BackpressurePipeMeta {
|
||||
/// Configured duplex buffer capacity in bytes.
|
||||
pub buffer_capacity: usize,
|
||||
/// Current backpressure state reported by the shared core monitor.
|
||||
pub state: BackpressureState,
|
||||
/// Age of the pipe since creation.
|
||||
pub age: std::time::Duration,
|
||||
}
|
||||
|
||||
impl BackpressurePipe {
|
||||
fn new(config: BackpressureConfig, monitor: Arc<CoreBackpressureMonitor>) -> Self {
|
||||
fn new(config: PipeBackpressurePolicy, monitor: Arc<CoreBackpressureMonitor>) -> Self {
|
||||
let (reader, writer) = duplex(config.buffer_size);
|
||||
|
||||
Self {
|
||||
@@ -146,7 +176,7 @@ impl BackpressurePipe {
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &BackpressureConfig {
|
||||
pub fn config(&self) -> &PipeBackpressurePolicy {
|
||||
&self.config
|
||||
}
|
||||
|
||||
@@ -160,6 +190,15 @@ impl BackpressurePipe {
|
||||
self.created_at.elapsed()
|
||||
}
|
||||
|
||||
/// Get a compact metadata snapshot for the pipe.
|
||||
pub fn meta(&self) -> BackpressurePipeMeta {
|
||||
BackpressurePipeMeta {
|
||||
buffer_capacity: self.config.buffer_size,
|
||||
state: self.state(),
|
||||
age: self.age(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if should apply backpressure
|
||||
pub fn should_apply_backpressure(&self) -> bool {
|
||||
let should = self.monitor.should_apply_backpressure();
|
||||
@@ -170,45 +209,26 @@ impl BackpressurePipe {
|
||||
}
|
||||
}
|
||||
|
||||
/// Backpressure event
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BackpressureEvent {
|
||||
/// Event timestamp
|
||||
pub timestamp: Instant,
|
||||
/// Event type
|
||||
pub event_type: BackpressureEventType,
|
||||
/// Buffer usage
|
||||
pub buffer_usage: usize,
|
||||
/// Buffer capacity
|
||||
pub buffer_capacity: usize,
|
||||
}
|
||||
|
||||
/// Backpressure event type
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum BackpressureEventType {
|
||||
/// High watermark reached
|
||||
HighWatermarkReached,
|
||||
/// High watermark exited
|
||||
HighWatermarkExited,
|
||||
/// Backpressure applied
|
||||
BackpressureApplied,
|
||||
/// Backpressure released
|
||||
BackpressureReleased,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_backpressure_config() {
|
||||
let config = BackpressureConfig::default();
|
||||
let config = PipeBackpressurePolicy::default();
|
||||
assert_eq!(config.buffer_size, 4 * 1024 * 1024);
|
||||
assert!(config.high_watermark > config.low_watermark);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backpressure_policy_to_core_config() {
|
||||
let policy = PipeBackpressurePolicy::default();
|
||||
let core = policy.to_core_config();
|
||||
assert_eq!(core.high_water_mark, policy.high_watermark as f64 / 100.0);
|
||||
assert_eq!(core.low_water_mark, policy.low_watermark as f64 / 100.0);
|
||||
assert!(core.enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backpressure_manager() {
|
||||
let manager = BackpressureManager::new(1024, 80, 50);
|
||||
@@ -220,5 +240,6 @@ mod tests {
|
||||
let manager = BackpressureManager::new(1024, 80, 50);
|
||||
let pipe = manager.create_pipe();
|
||||
assert_eq!(pipe.state(), BackpressureState::Normal);
|
||||
assert_eq!(pipe.meta().buffer_capacity, 1024);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
|
||||
//! Configuration for concurrency management
|
||||
|
||||
use crate::{
|
||||
backpressure::PipeBackpressurePolicy, deadlock::DeadlockMonitorPolicy, scheduler::SchedulerPolicy,
|
||||
timeout::TimeoutManagerPolicy,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Feature flags for concurrency modules
|
||||
@@ -72,84 +76,39 @@ impl ConcurrencyFeatures {
|
||||
}
|
||||
}
|
||||
|
||||
/// Facade policy for lock manager behavior.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LockManagerPolicy {
|
||||
/// Enable lock optimization.
|
||||
pub enabled: bool,
|
||||
/// Lock acquisition timeout.
|
||||
pub acquire_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for LockManagerPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
acquire_timeout: Duration::from_secs(5),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Main configuration for concurrency management
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ConcurrencyConfig {
|
||||
/// Feature flags
|
||||
pub features: ConcurrencyFeatures,
|
||||
|
||||
// Timeout configuration
|
||||
/// Default timeout duration
|
||||
pub default_timeout: Duration,
|
||||
/// Maximum timeout duration
|
||||
pub max_timeout: Duration,
|
||||
/// Enable dynamic timeout
|
||||
pub enable_dynamic_timeout: bool,
|
||||
|
||||
// Lock configuration
|
||||
/// Enable lock optimization
|
||||
pub enable_lock_optimization: bool,
|
||||
/// Lock acquisition timeout
|
||||
pub lock_acquire_timeout: Duration,
|
||||
|
||||
// Deadlock configuration
|
||||
/// Enable deadlock detection
|
||||
pub enable_deadlock_detection: bool,
|
||||
/// Deadlock check interval
|
||||
pub deadlock_check_interval: Duration,
|
||||
/// Hang threshold
|
||||
pub hang_threshold: Duration,
|
||||
|
||||
// Backpressure configuration
|
||||
/// Buffer size for backpressure
|
||||
pub backpressure_buffer_size: usize,
|
||||
/// High watermark percentage
|
||||
pub high_watermark: u32,
|
||||
/// Low watermark percentage
|
||||
pub low_watermark: u32,
|
||||
|
||||
// Scheduler configuration
|
||||
/// Base buffer size for I/O
|
||||
pub io_buffer_size: usize,
|
||||
/// Maximum buffer size
|
||||
pub max_buffer_size: usize,
|
||||
/// High priority size threshold
|
||||
pub high_priority_threshold: usize,
|
||||
/// Low priority size threshold
|
||||
pub low_priority_threshold: usize,
|
||||
}
|
||||
|
||||
impl Default for ConcurrencyConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
features: ConcurrencyFeatures::default(),
|
||||
|
||||
// Timeout defaults
|
||||
default_timeout: Duration::from_secs(30),
|
||||
max_timeout: Duration::from_secs(300),
|
||||
enable_dynamic_timeout: true,
|
||||
|
||||
// Lock defaults
|
||||
enable_lock_optimization: true,
|
||||
lock_acquire_timeout: Duration::from_secs(5),
|
||||
|
||||
// Deadlock defaults
|
||||
enable_deadlock_detection: false,
|
||||
deadlock_check_interval: Duration::from_secs(10),
|
||||
hang_threshold: Duration::from_secs(60),
|
||||
|
||||
// Backpressure defaults
|
||||
backpressure_buffer_size: 4 * 1024 * 1024, // 4MB
|
||||
high_watermark: 80,
|
||||
low_watermark: 50,
|
||||
|
||||
// Scheduler defaults
|
||||
io_buffer_size: 64 * 1024, // 64KB
|
||||
max_buffer_size: 4 * 1024 * 1024, // 4MB
|
||||
high_priority_threshold: 1024 * 1024, // 1MB
|
||||
low_priority_threshold: 10 * 1024 * 1024, // 10MB
|
||||
}
|
||||
}
|
||||
/// Timeout facade policy.
|
||||
pub timeout_policy: TimeoutManagerPolicy,
|
||||
/// Lock facade policy.
|
||||
pub lock_policy: LockManagerPolicy,
|
||||
/// Deadlock facade policy.
|
||||
pub deadlock_policy: DeadlockMonitorPolicy,
|
||||
/// Backpressure facade policy.
|
||||
pub backpressure_policy: PipeBackpressurePolicy,
|
||||
/// Scheduler facade policy.
|
||||
pub scheduler_policy: SchedulerPolicy,
|
||||
}
|
||||
|
||||
impl ConcurrencyConfig {
|
||||
@@ -161,25 +120,25 @@ impl ConcurrencyConfig {
|
||||
if let Ok(val) = std::env::var("RUSTFS_TIMEOUT_DEFAULT")
|
||||
&& let Ok(secs) = val.parse::<u64>()
|
||||
{
|
||||
config.default_timeout = Duration::from_secs(secs);
|
||||
config.timeout_policy.default_timeout = Duration::from_secs(secs);
|
||||
}
|
||||
|
||||
if let Ok(val) = std::env::var("RUSTFS_TIMEOUT_MAX")
|
||||
&& let Ok(secs) = val.parse::<u64>()
|
||||
{
|
||||
config.max_timeout = Duration::from_secs(secs);
|
||||
config.timeout_policy.max_timeout = Duration::from_secs(secs);
|
||||
}
|
||||
|
||||
if let Ok(val) = std::env::var("RUSTFS_BACKPRESSURE_BUFFER_SIZE")
|
||||
&& let Ok(size) = val.parse::<usize>()
|
||||
{
|
||||
config.backpressure_buffer_size = size;
|
||||
config.backpressure_policy.buffer_size = size;
|
||||
}
|
||||
|
||||
if let Ok(val) = std::env::var("RUSTFS_IO_BUFFER_SIZE")
|
||||
&& let Ok(size) = val.parse::<usize>()
|
||||
{
|
||||
config.io_buffer_size = size;
|
||||
config.scheduler_policy.base_buffer_size = size;
|
||||
}
|
||||
|
||||
config
|
||||
@@ -187,18 +146,25 @@ impl ConcurrencyConfig {
|
||||
|
||||
/// Validate configuration
|
||||
pub fn validate(&self) -> Result<(), ConfigError> {
|
||||
if self.default_timeout > self.max_timeout {
|
||||
if self.timeout_policy.default_timeout > self.timeout_policy.max_timeout {
|
||||
return Err(ConfigError::InvalidTimeout("default_timeout cannot exceed max_timeout".to_string()));
|
||||
}
|
||||
if self.timeout_policy.min_timeout > self.timeout_policy.max_timeout {
|
||||
return Err(ConfigError::InvalidTimeout("min_timeout cannot exceed max_timeout".to_string()));
|
||||
}
|
||||
|
||||
if self.high_watermark <= self.low_watermark || self.high_watermark > 100 {
|
||||
if self.backpressure_policy.high_watermark <= self.backpressure_policy.low_watermark
|
||||
|| self.backpressure_policy.high_watermark > 100
|
||||
{
|
||||
return Err(ConfigError::InvalidBackpressure(
|
||||
"high_watermark must be > low_watermark and <= 100".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if self.io_buffer_size > self.max_buffer_size {
|
||||
return Err(ConfigError::InvalidScheduler("io_buffer_size cannot exceed max_buffer_size".to_string()));
|
||||
if self.scheduler_policy.base_buffer_size > self.scheduler_policy.max_buffer_size {
|
||||
return Err(ConfigError::InvalidScheduler(
|
||||
"base_buffer_size cannot exceed max_buffer_size".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -235,8 +201,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_invalid_timeout() {
|
||||
let config = ConcurrencyConfig {
|
||||
default_timeout: Duration::from_secs(100),
|
||||
max_timeout: Duration::from_secs(50),
|
||||
timeout_policy: TimeoutManagerPolicy {
|
||||
default_timeout: Duration::from_secs(100),
|
||||
max_timeout: Duration::from_secs(50),
|
||||
enable_dynamic: true,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
@@ -245,6 +215,22 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_min_timeout() {
|
||||
let config = ConcurrencyConfig {
|
||||
timeout_policy: TimeoutManagerPolicy {
|
||||
min_timeout: Duration::from_secs(100),
|
||||
max_timeout: Duration::from_secs(50),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
config.validate().is_err(),
|
||||
"validate() should return an error when min_timeout > max_timeout"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_features() {
|
||||
let features = ConcurrencyFeatures::all();
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
|
||||
//! Deadlock detection management
|
||||
|
||||
use rustfs_io_core::{DeadlockDetector as CoreDeadlockDetector, LockType};
|
||||
use rustfs_io_core::{DeadlockDetector as CoreDeadlockDetector, DeadlockDetectorConfig as CoreDeadlockConfig, LockType};
|
||||
use rustfs_io_metrics::deadlock_metrics;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Deadlock configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeadlockConfig {
|
||||
/// Facade policy for the concurrency-layer deadlock monitor.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DeadlockMonitorPolicy {
|
||||
/// Enable deadlock detection
|
||||
pub enabled: bool,
|
||||
/// Check interval
|
||||
@@ -31,7 +31,7 @@ pub struct DeadlockConfig {
|
||||
pub hang_threshold: Duration,
|
||||
}
|
||||
|
||||
impl Default for DeadlockConfig {
|
||||
impl Default for DeadlockMonitorPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
@@ -41,9 +41,20 @@ impl Default for DeadlockConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl DeadlockMonitorPolicy {
|
||||
/// Convert the facade policy into the reusable io-core deadlock config.
|
||||
pub fn to_core_config(&self) -> CoreDeadlockConfig {
|
||||
CoreDeadlockConfig {
|
||||
enabled: self.enabled,
|
||||
detection_interval: self.check_interval,
|
||||
max_hold_time: self.hang_threshold,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deadlock manager
|
||||
pub struct DeadlockManager {
|
||||
config: DeadlockConfig,
|
||||
config: DeadlockMonitorPolicy,
|
||||
detector: Arc<CoreDeadlockDetector>,
|
||||
running: Arc<tokio::sync::Mutex<bool>>,
|
||||
}
|
||||
@@ -51,18 +62,16 @@ pub struct DeadlockManager {
|
||||
impl DeadlockManager {
|
||||
/// Create a new deadlock manager
|
||||
pub fn new(enabled: bool, check_interval: Duration, hang_threshold: Duration) -> Self {
|
||||
let config = DeadlockConfig {
|
||||
Self::from_policy(DeadlockMonitorPolicy {
|
||||
enabled,
|
||||
check_interval,
|
||||
hang_threshold,
|
||||
};
|
||||
|
||||
let core_config = rustfs_io_core::DeadlockDetectorConfig {
|
||||
enabled,
|
||||
detection_interval: check_interval,
|
||||
max_hold_time: hang_threshold,
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new deadlock manager from the facade policy type.
|
||||
pub fn from_policy(config: DeadlockMonitorPolicy) -> Self {
|
||||
let core_config = config.to_core_config();
|
||||
Self {
|
||||
config,
|
||||
detector: Arc::new(CoreDeadlockDetector::new(core_config)),
|
||||
@@ -71,7 +80,7 @@ impl DeadlockManager {
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &DeadlockConfig {
|
||||
pub fn config(&self) -> &DeadlockMonitorPolicy {
|
||||
&self.config
|
||||
}
|
||||
|
||||
@@ -129,7 +138,11 @@ impl DeadlockManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Request tracker for tracking resources
|
||||
/// Lightweight compatibility wrapper for request-scoped deadlock bookkeeping.
|
||||
///
|
||||
/// This type intentionally stays minimal in the concurrency layer. Rich
|
||||
/// request-level lock/resource diagnostics belong to
|
||||
/// `rustfs::storage::deadlock_detector::RequestResourceTracker`.
|
||||
pub struct RequestTracker {
|
||||
request_id: String,
|
||||
description: String,
|
||||
@@ -174,6 +187,11 @@ impl RequestTracker {
|
||||
deadlock_metrics::record_lock_acquisition("read");
|
||||
}
|
||||
|
||||
/// Return a read-only view of tracked resource names.
|
||||
pub fn resources(&self) -> &HashMap<String, Vec<String>> {
|
||||
&self.resources
|
||||
}
|
||||
|
||||
/// Record a lock release
|
||||
pub fn record_lock_release(&mut self, lock_id: u64) {
|
||||
self.detector.record_release(lock_id);
|
||||
@@ -196,12 +214,24 @@ mod tests {
|
||||
assert!(!manager.config().enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deadlock_policy_to_core_config() {
|
||||
let policy = DeadlockMonitorPolicy::default();
|
||||
let core = policy.to_core_config();
|
||||
assert_eq!(core.enabled, policy.enabled);
|
||||
assert_eq!(core.detection_interval, policy.check_interval);
|
||||
assert_eq!(core.max_hold_time, policy.hang_threshold);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_tracker() {
|
||||
let manager = DeadlockManager::new(true, Duration::from_secs(10), Duration::from_secs(60));
|
||||
let tracker = manager.track_request("req-1".to_string(), "test request".to_string());
|
||||
let mut tracker = manager.track_request("req-1".to_string(), "test request".to_string());
|
||||
let lock_id = manager.register_lock(LockType::Mutex);
|
||||
tracker.record_lock_acquire(lock_id, "bucket/key".to_string());
|
||||
|
||||
assert_eq!(tracker.request_id(), "req-1");
|
||||
assert_eq!(tracker.description(), "test request");
|
||||
assert_eq!(tracker.resources().get("locks").map(Vec::len), Some(1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,21 +124,23 @@ mod backpressure;
|
||||
#[cfg(feature = "scheduler")]
|
||||
mod scheduler;
|
||||
|
||||
pub mod workers;
|
||||
|
||||
// Public module exports with feature gates
|
||||
#[cfg(feature = "timeout")]
|
||||
pub use timeout::{TimeoutConfig, TimeoutGuard, TimeoutManager};
|
||||
pub use timeout::{TimeoutGuard, TimeoutManager, TimeoutManagerPolicy};
|
||||
|
||||
#[cfg(feature = "lock")]
|
||||
pub use lock::{LockConfig, LockManager, LockScopeGuard, OptimizedLockGuard};
|
||||
|
||||
#[cfg(feature = "deadlock")]
|
||||
pub use deadlock::{DeadlockConfig, DeadlockManager, RequestTracker};
|
||||
pub use deadlock::{DeadlockManager, DeadlockMonitorPolicy, RequestTracker};
|
||||
|
||||
#[cfg(feature = "backpressure")]
|
||||
pub use backpressure::{BackpressureConfig, BackpressureManager, BackpressurePipe};
|
||||
pub use backpressure::{BackpressureManager, BackpressurePipe, PipeBackpressurePolicy};
|
||||
|
||||
#[cfg(feature = "scheduler")]
|
||||
pub use scheduler::{IoStrategy, SchedulerConfig, SchedulerManager};
|
||||
pub use scheduler::{IoStrategy, SchedulerManager, SchedulerPolicy};
|
||||
|
||||
// Configuration
|
||||
mod config;
|
||||
@@ -153,19 +155,19 @@ pub mod prelude {
|
||||
//! Prelude module for convenient imports
|
||||
|
||||
#[cfg(feature = "timeout")]
|
||||
pub use crate::timeout::{TimeoutConfig, TimeoutGuard, TimeoutManager};
|
||||
pub use crate::timeout::{TimeoutGuard, TimeoutManager, TimeoutManagerPolicy};
|
||||
|
||||
#[cfg(feature = "lock")]
|
||||
pub use crate::lock::{LockConfig, LockManager, LockScopeGuard, OptimizedLockGuard};
|
||||
|
||||
#[cfg(feature = "deadlock")]
|
||||
pub use crate::deadlock::{DeadlockConfig, DeadlockManager, RequestTracker};
|
||||
pub use crate::deadlock::{DeadlockManager, DeadlockMonitorPolicy, RequestTracker};
|
||||
|
||||
#[cfg(feature = "backpressure")]
|
||||
pub use crate::backpressure::{BackpressureConfig, BackpressureManager, BackpressurePipe};
|
||||
pub use crate::backpressure::{BackpressureManager, BackpressurePipe, PipeBackpressurePolicy};
|
||||
|
||||
#[cfg(feature = "scheduler")]
|
||||
pub use crate::scheduler::{IoStrategy, SchedulerConfig, SchedulerManager};
|
||||
pub use crate::scheduler::{IoStrategy, SchedulerManager, SchedulerPolicy};
|
||||
|
||||
pub use crate::{ConcurrencyConfig, ConcurrencyFeatures, ConcurrencyManager};
|
||||
}
|
||||
|
||||
@@ -85,39 +85,22 @@ impl ConcurrencyManager {
|
||||
|
||||
Self {
|
||||
#[cfg(feature = "timeout")]
|
||||
timeout: Arc::new(crate::timeout::TimeoutManager::new(
|
||||
config.default_timeout,
|
||||
config.max_timeout,
|
||||
config.enable_dynamic_timeout,
|
||||
)),
|
||||
timeout: Arc::new(crate::timeout::TimeoutManager::from_policy(config.timeout_policy)),
|
||||
|
||||
#[cfg(feature = "lock")]
|
||||
lock: Arc::new(crate::lock::LockManager::new(
|
||||
config.enable_lock_optimization,
|
||||
config.lock_acquire_timeout,
|
||||
config.lock_policy.enabled,
|
||||
config.lock_policy.acquire_timeout,
|
||||
)),
|
||||
|
||||
#[cfg(feature = "deadlock")]
|
||||
deadlock: Arc::new(crate::deadlock::DeadlockManager::new(
|
||||
config.enable_deadlock_detection,
|
||||
config.deadlock_check_interval,
|
||||
config.hang_threshold,
|
||||
)),
|
||||
deadlock: Arc::new(crate::deadlock::DeadlockManager::from_policy(config.deadlock_policy)),
|
||||
|
||||
#[cfg(feature = "backpressure")]
|
||||
backpressure: Arc::new(crate::backpressure::BackpressureManager::new(
|
||||
config.backpressure_buffer_size,
|
||||
config.high_watermark,
|
||||
config.low_watermark,
|
||||
)),
|
||||
backpressure: Arc::new(crate::backpressure::BackpressureManager::from_policy(config.backpressure_policy)),
|
||||
|
||||
#[cfg(feature = "scheduler")]
|
||||
scheduler: Arc::new(crate::scheduler::SchedulerManager::new(
|
||||
config.io_buffer_size,
|
||||
config.max_buffer_size,
|
||||
config.high_priority_threshold,
|
||||
config.low_priority_threshold,
|
||||
)),
|
||||
scheduler: Arc::new(crate::scheduler::SchedulerManager::from_policy(config.scheduler_policy)),
|
||||
|
||||
config,
|
||||
}
|
||||
@@ -244,7 +227,7 @@ impl ConcurrencyManager {
|
||||
pub async fn start(&self) {
|
||||
#[cfg(feature = "deadlock")]
|
||||
{
|
||||
if self.config.enable_deadlock_detection {
|
||||
if self.config.deadlock_policy.enabled {
|
||||
self.deadlock.start().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ use rustfs_io_metrics::io_metrics;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Scheduler configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SchedulerConfig {
|
||||
/// Facade policy for the concurrency-layer scheduler manager.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SchedulerPolicy {
|
||||
/// Base buffer size
|
||||
pub base_buffer_size: usize,
|
||||
/// Maximum buffer size
|
||||
@@ -35,7 +35,7 @@ pub struct SchedulerConfig {
|
||||
pub low_priority_threshold: usize,
|
||||
}
|
||||
|
||||
impl Default for SchedulerConfig {
|
||||
impl Default for SchedulerPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_buffer_size: 64 * 1024, // 64KB
|
||||
@@ -46,9 +46,23 @@ impl Default for SchedulerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl SchedulerPolicy {
|
||||
/// Convert facade policy to io-core scheduler config.
|
||||
pub fn to_core_config(&self) -> rustfs_io_core::IoSchedulerConfig {
|
||||
rustfs_io_core::IoSchedulerConfig {
|
||||
base_buffer_size: self.base_buffer_size,
|
||||
max_buffer_size: self.max_buffer_size,
|
||||
high_priority_size_threshold: self.high_priority_threshold,
|
||||
low_priority_size_threshold: self.low_priority_threshold,
|
||||
..rustfs_io_core::IoSchedulerConfig::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scheduler manager
|
||||
pub struct SchedulerManager {
|
||||
config: SchedulerConfig,
|
||||
config: SchedulerPolicy,
|
||||
core_config: rustfs_io_core::IoSchedulerConfig,
|
||||
scheduler: Arc<CoreIoScheduler>,
|
||||
}
|
||||
|
||||
@@ -60,26 +74,35 @@ impl SchedulerManager {
|
||||
high_priority_threshold: usize,
|
||||
low_priority_threshold: usize,
|
||||
) -> Self {
|
||||
let config = SchedulerConfig {
|
||||
Self::from_policy(SchedulerPolicy {
|
||||
base_buffer_size,
|
||||
max_buffer_size,
|
||||
high_priority_threshold,
|
||||
low_priority_threshold,
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
let core_config = rustfs_io_core::IoSchedulerConfig::default();
|
||||
/// Create a scheduler manager from facade policy.
|
||||
pub fn from_policy(config: SchedulerPolicy) -> Self {
|
||||
let core_config = config.to_core_config();
|
||||
|
||||
Self {
|
||||
config,
|
||||
core_config: core_config.clone(),
|
||||
scheduler: Arc::new(CoreIoScheduler::new(core_config)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &SchedulerConfig {
|
||||
pub fn config(&self) -> &SchedulerPolicy {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Get the derived io-core scheduler config.
|
||||
pub fn core_config(&self) -> &rustfs_io_core::IoSchedulerConfig {
|
||||
&self.core_config
|
||||
}
|
||||
|
||||
/// Get the scheduler
|
||||
pub fn scheduler(&self) -> Arc<CoreIoScheduler> {
|
||||
self.scheduler.clone()
|
||||
@@ -87,7 +110,7 @@ impl SchedulerManager {
|
||||
|
||||
/// Create an I/O strategy
|
||||
pub fn create_strategy(&self) -> IoStrategy {
|
||||
IoStrategy::new(self.config.clone(), self.scheduler.clone())
|
||||
IoStrategy::new(self.config, self.scheduler.clone())
|
||||
}
|
||||
|
||||
/// Calculate buffer size
|
||||
@@ -111,12 +134,12 @@ impl SchedulerManager {
|
||||
|
||||
/// I/O strategy
|
||||
pub struct IoStrategy {
|
||||
config: SchedulerConfig,
|
||||
config: SchedulerPolicy,
|
||||
scheduler: Arc<CoreIoScheduler>,
|
||||
}
|
||||
|
||||
impl IoStrategy {
|
||||
fn new(config: SchedulerConfig, scheduler: Arc<CoreIoScheduler>) -> Self {
|
||||
fn new(config: SchedulerPolicy, scheduler: Arc<CoreIoScheduler>) -> Self {
|
||||
Self { config, scheduler }
|
||||
}
|
||||
|
||||
@@ -191,7 +214,7 @@ impl IoStrategy {
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &SchedulerConfig {
|
||||
pub fn config(&self) -> &SchedulerPolicy {
|
||||
&self.config
|
||||
}
|
||||
}
|
||||
@@ -202,10 +225,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_scheduler_config() {
|
||||
let config = SchedulerConfig::default();
|
||||
let config = SchedulerPolicy::default();
|
||||
assert!(config.base_buffer_size < config.max_buffer_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scheduler_policy_to_core_config() {
|
||||
let policy = SchedulerPolicy::default();
|
||||
let core = policy.to_core_config();
|
||||
assert_eq!(core.base_buffer_size, policy.base_buffer_size);
|
||||
assert_eq!(core.max_buffer_size, policy.max_buffer_size);
|
||||
assert_eq!(core.high_priority_size_threshold, policy.high_priority_threshold);
|
||||
assert_eq!(core.low_priority_size_threshold, policy.low_priority_threshold);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scheduler_manager() {
|
||||
let manager = SchedulerManager::new(1024, 4096, 512, 2048);
|
||||
|
||||
@@ -14,60 +14,101 @@
|
||||
|
||||
//! Timeout management for operations
|
||||
|
||||
use rustfs_io_core::{TimeoutError, calculate_adaptive_timeout};
|
||||
use rustfs_io_core::{TimeoutConfig as CoreTimeoutConfig, TimeoutError, calculate_adaptive_timeout};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
/// Timeout configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TimeoutConfig {
|
||||
/// Facade policy for the concurrency-layer timeout manager.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TimeoutManagerPolicy {
|
||||
/// Default timeout duration
|
||||
pub default_timeout: Duration,
|
||||
/// Maximum timeout duration
|
||||
pub max_timeout: Duration,
|
||||
/// Minimum timeout floor (prevents dynamic calculation from going too low).
|
||||
pub min_timeout: Duration,
|
||||
/// Enable dynamic timeout calculation
|
||||
pub enable_dynamic: bool,
|
||||
}
|
||||
|
||||
impl Default for TimeoutConfig {
|
||||
impl Default for TimeoutManagerPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_timeout: Duration::from_secs(30),
|
||||
max_timeout: Duration::from_secs(300),
|
||||
min_timeout: Duration::from_secs(5),
|
||||
enable_dynamic: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TimeoutManagerPolicy {
|
||||
/// Convert the facade policy into the reusable io-core timeout configuration.
|
||||
///
|
||||
/// This keeps the concurrency layer explicitly wired to the shared core
|
||||
/// timeout primitives without changing the facade's public behavior.
|
||||
pub fn to_core_config(&self) -> CoreTimeoutConfig {
|
||||
CoreTimeoutConfig {
|
||||
base_timeout: self.default_timeout,
|
||||
timeout_per_mb: Duration::ZERO,
|
||||
max_timeout: self.max_timeout,
|
||||
min_timeout: self.min_timeout,
|
||||
get_object_timeout: self.default_timeout,
|
||||
put_object_timeout: self.max_timeout,
|
||||
list_objects_timeout: self.default_timeout,
|
||||
enable_dynamic_timeout: self.enable_dynamic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Timeout manager
|
||||
pub struct TimeoutManager {
|
||||
config: TimeoutConfig,
|
||||
config: TimeoutManagerPolicy,
|
||||
core_config: CoreTimeoutConfig,
|
||||
}
|
||||
|
||||
impl TimeoutManager {
|
||||
/// Create a new timeout manager
|
||||
pub fn new(default_timeout: Duration, max_timeout: Duration, enable_dynamic: bool) -> Self {
|
||||
Self {
|
||||
config: TimeoutConfig {
|
||||
default_timeout,
|
||||
max_timeout,
|
||||
enable_dynamic,
|
||||
},
|
||||
}
|
||||
let min_timeout = default_timeout.min(max_timeout);
|
||||
Self::from_policy(TimeoutManagerPolicy {
|
||||
default_timeout,
|
||||
max_timeout,
|
||||
min_timeout,
|
||||
enable_dynamic,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new timeout manager from the facade policy type.
|
||||
pub fn from_policy(config: TimeoutManagerPolicy) -> Self {
|
||||
let config = TimeoutManagerPolicy {
|
||||
// Guard clamp(min, max) from panic when callers provide an
|
||||
// out-of-order policy (or very small max_timeout).
|
||||
min_timeout: config.min_timeout.min(config.max_timeout),
|
||||
..config
|
||||
};
|
||||
let core_config = config.to_core_config();
|
||||
Self { config, core_config }
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &TimeoutConfig {
|
||||
pub fn config(&self) -> &TimeoutManagerPolicy {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Get the derived io-core timeout configuration.
|
||||
pub fn core_config(&self) -> &CoreTimeoutConfig {
|
||||
&self.core_config
|
||||
}
|
||||
|
||||
/// Calculate timeout for a given size
|
||||
pub fn calculate_timeout(&self, size: u64, _history: &[Duration]) -> Duration {
|
||||
if !self.config.enable_dynamic {
|
||||
return self.config.default_timeout;
|
||||
}
|
||||
|
||||
calculate_adaptive_timeout(self.config.default_timeout, None, 0, size).min(self.config.max_timeout)
|
||||
calculate_adaptive_timeout(self.core_config.base_timeout, None, 0, size)
|
||||
.clamp(self.core_config.min_timeout, self.core_config.max_timeout)
|
||||
}
|
||||
|
||||
/// Wrap an operation with timeout control
|
||||
@@ -87,7 +128,7 @@ impl TimeoutManager {
|
||||
|
||||
/// Create a timeout guard for manual timeout control
|
||||
pub fn create_guard(&self, timeout: Option<Duration>) -> TimeoutGuard {
|
||||
TimeoutGuard::new(timeout.unwrap_or(self.config.default_timeout))
|
||||
TimeoutGuard::new(timeout.unwrap_or(self.core_config.base_timeout))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,10 +175,41 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_timeout_config() {
|
||||
let config = TimeoutConfig::default();
|
||||
let config = TimeoutManagerPolicy::default();
|
||||
assert!(config.default_timeout < config.max_timeout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeout_policy_to_core_config() {
|
||||
let policy = TimeoutManagerPolicy::default();
|
||||
let core = policy.to_core_config();
|
||||
assert_eq!(core.base_timeout, policy.default_timeout);
|
||||
assert_eq!(core.max_timeout, policy.max_timeout);
|
||||
assert_eq!(core.min_timeout, policy.min_timeout);
|
||||
assert_eq!(core.get_object_timeout, policy.default_timeout);
|
||||
assert!(core.enable_dynamic_timeout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeout_manager_new_sanitizes_min_timeout_with_small_max_timeout() {
|
||||
let manager = TimeoutManager::new(Duration::from_secs(1), Duration::from_secs(1), true);
|
||||
let timeout = manager.calculate_timeout(1024, &[]);
|
||||
assert_eq!(timeout, Duration::from_secs(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeout_manager_from_policy_sanitizes_min_timeout() {
|
||||
let manager = TimeoutManager::from_policy(TimeoutManagerPolicy {
|
||||
default_timeout: Duration::from_secs(30),
|
||||
max_timeout: Duration::from_secs(1),
|
||||
min_timeout: Duration::from_secs(5),
|
||||
enable_dynamic: true,
|
||||
});
|
||||
|
||||
assert_eq!(manager.config().min_timeout, Duration::from_secs(1));
|
||||
assert_eq!(manager.core_config().min_timeout, Duration::from_secs(1));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wrap_operation_success() {
|
||||
let manager = TimeoutManager::new(Duration::from_secs(5), Duration::from_secs(10), true);
|
||||
|
||||
@@ -12,10 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Worker slot limiter used by long-running background workflows.
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
use tracing::info;
|
||||
|
||||
/// Cooperative worker-slot controller for async tasks.
|
||||
pub struct Workers {
|
||||
available: Mutex<usize>, // Available working slots
|
||||
notify: Notify, // Used to notify waiting tasks
|
||||
@@ -23,20 +26,20 @@ pub struct Workers {
|
||||
}
|
||||
|
||||
impl Workers {
|
||||
// Create a Workers object that allows up to n jobs to execute concurrently
|
||||
pub fn new(n: usize) -> Result<Arc<Workers>, &'static str> {
|
||||
/// Create a [`Workers`] object that allows up to `n` jobs to execute concurrently.
|
||||
pub fn new(n: usize) -> Result<Arc<Self>, &'static str> {
|
||||
if n == 0 {
|
||||
return Err("n must be > 0");
|
||||
}
|
||||
|
||||
Ok(Arc::new(Workers {
|
||||
Ok(Arc::new(Self {
|
||||
available: Mutex::new(n),
|
||||
notify: Notify::new(),
|
||||
limit: n,
|
||||
}))
|
||||
}
|
||||
|
||||
// Give a job a chance to be executed
|
||||
/// Acquire a worker slot, waiting until one becomes available.
|
||||
pub async fn take(&self) {
|
||||
loop {
|
||||
let mut available = self.available.lock().await;
|
||||
@@ -51,7 +54,7 @@ impl Workers {
|
||||
}
|
||||
}
|
||||
|
||||
// Release a job's slot
|
||||
/// Release a worker slot.
|
||||
pub async fn give(&self) {
|
||||
let mut available = self.available.lock().await;
|
||||
info!("worker give, {}", *available);
|
||||
@@ -59,7 +62,7 @@ impl Workers {
|
||||
self.notify.notify_one(); // Notify a waiting task
|
||||
}
|
||||
|
||||
// Wait for all concurrent jobs to complete
|
||||
/// Wait until all worker slots are released.
|
||||
pub async fn wait(&self) {
|
||||
loop {
|
||||
{
|
||||
@@ -74,6 +77,7 @@ impl Workers {
|
||||
info!("worker wait end");
|
||||
}
|
||||
|
||||
/// Return the current number of available worker slots.
|
||||
pub async fn available(&self) -> usize {
|
||||
*self.available.lock().await
|
||||
}
|
||||
@@ -83,6 +83,28 @@ Legacy compatibility fallback:
|
||||
- `RUSTFS_DRIVE_MAX_TIMEOUT_DURATION`
|
||||
This legacy variable is treated as a deprecated fallback for the operation-specific drive timeout variables above when a canonical variable is unset.
|
||||
|
||||
Drive timeout health-action policy:
|
||||
- `RUSTFS_DRIVE_TIMEOUT_HEALTH_ACTION`
|
||||
- `mark_failure` (default): timeout marks failure and may transition drive runtime state.
|
||||
- `ignore_scanner`: timeout does not mark failure for scanner-sensitive operations (`walk_dir`, `read_metadata`, `list_dir`, `disk_info`).
|
||||
|
||||
Drive timeout profile preset:
|
||||
- `RUSTFS_DRIVE_TIMEOUT_PROFILE`
|
||||
- `default` (default): keep current timeout defaults.
|
||||
- `high_latency`: use 60s default timeout for scanner-sensitive operations when no per-operation timeout override is set (`read_metadata`, `disk_info`, `list_dir`, `walk_dir`, `walk_dir_stall`).
|
||||
- Precedence:
|
||||
- Explicit per-operation timeout env (`RUSTFS_DRIVE_*_TIMEOUT_SECS`) takes highest precedence.
|
||||
- Then `RUSTFS_DRIVE_MAX_TIMEOUT_DURATION` legacy fallback.
|
||||
- Then the profile-derived default (`default` or `high_latency`).
|
||||
|
||||
## Startup filesystem boundary policy
|
||||
|
||||
- `RUSTFS_UNSUPPORTED_FS_POLICY` controls startup behavior when RustFS detects local endpoint filesystems that are outside the supported production boundary.
|
||||
- `warn` (default): log warning and continue startup.
|
||||
- `fail`: abort startup with an error.
|
||||
|
||||
RustFS production guidance remains direct-attached local POSIX filesystems. Network-mounted filesystems (for example `nfs`, `cifs`, `smb2`, and `fuse.*`) are treated as unsupported by this startup guard.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the Apache License 2.0 - see the [LICENSE](../../LICENSE) file for details.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
pub const AUDIT_AMQP_KEYS: &[&str] = &[
|
||||
crate::ENABLE_KEY,
|
||||
crate::AMQP_URL,
|
||||
crate::AMQP_EXCHANGE,
|
||||
crate::AMQP_ROUTING_KEY,
|
||||
crate::AMQP_MANDATORY,
|
||||
crate::AMQP_PERSISTENT,
|
||||
crate::AMQP_USERNAME,
|
||||
crate::AMQP_PASSWORD,
|
||||
crate::AMQP_TLS_CA,
|
||||
crate::AMQP_TLS_CLIENT_CERT,
|
||||
crate::AMQP_TLS_CLIENT_KEY,
|
||||
crate::AMQP_QUEUE_DIR,
|
||||
crate::AMQP_QUEUE_LIMIT,
|
||||
crate::COMMENT_KEY,
|
||||
];
|
||||
|
||||
pub const ENV_AUDIT_AMQP_ENABLE: &str = "RUSTFS_AUDIT_AMQP_ENABLE";
|
||||
pub const ENV_AUDIT_AMQP_URL: &str = "RUSTFS_AUDIT_AMQP_URL";
|
||||
pub const ENV_AUDIT_AMQP_EXCHANGE: &str = "RUSTFS_AUDIT_AMQP_EXCHANGE";
|
||||
pub const ENV_AUDIT_AMQP_ROUTING_KEY: &str = "RUSTFS_AUDIT_AMQP_ROUTING_KEY";
|
||||
pub const ENV_AUDIT_AMQP_MANDATORY: &str = "RUSTFS_AUDIT_AMQP_MANDATORY";
|
||||
pub const ENV_AUDIT_AMQP_PERSISTENT: &str = "RUSTFS_AUDIT_AMQP_PERSISTENT";
|
||||
pub const ENV_AUDIT_AMQP_USERNAME: &str = "RUSTFS_AUDIT_AMQP_USERNAME";
|
||||
pub const ENV_AUDIT_AMQP_PASSWORD: &str = "RUSTFS_AUDIT_AMQP_PASSWORD";
|
||||
pub const ENV_AUDIT_AMQP_TLS_CA: &str = "RUSTFS_AUDIT_AMQP_TLS_CA";
|
||||
pub const ENV_AUDIT_AMQP_TLS_CLIENT_CERT: &str = "RUSTFS_AUDIT_AMQP_TLS_CLIENT_CERT";
|
||||
pub const ENV_AUDIT_AMQP_TLS_CLIENT_KEY: &str = "RUSTFS_AUDIT_AMQP_TLS_CLIENT_KEY";
|
||||
pub const ENV_AUDIT_AMQP_QUEUE_DIR: &str = "RUSTFS_AUDIT_AMQP_QUEUE_DIR";
|
||||
pub const ENV_AUDIT_AMQP_QUEUE_LIMIT: &str = "RUSTFS_AUDIT_AMQP_QUEUE_LIMIT";
|
||||
|
||||
pub const ENV_AUDIT_AMQP_KEYS: &[&str; 13] = &[
|
||||
ENV_AUDIT_AMQP_ENABLE,
|
||||
ENV_AUDIT_AMQP_URL,
|
||||
ENV_AUDIT_AMQP_EXCHANGE,
|
||||
ENV_AUDIT_AMQP_ROUTING_KEY,
|
||||
ENV_AUDIT_AMQP_MANDATORY,
|
||||
ENV_AUDIT_AMQP_PERSISTENT,
|
||||
ENV_AUDIT_AMQP_USERNAME,
|
||||
ENV_AUDIT_AMQP_PASSWORD,
|
||||
ENV_AUDIT_AMQP_TLS_CA,
|
||||
ENV_AUDIT_AMQP_TLS_CLIENT_CERT,
|
||||
ENV_AUDIT_AMQP_TLS_CLIENT_KEY,
|
||||
ENV_AUDIT_AMQP_QUEUE_DIR,
|
||||
ENV_AUDIT_AMQP_QUEUE_LIMIT,
|
||||
];
|
||||
@@ -16,16 +16,24 @@
|
||||
//! This module defines the configuration for audit systems, including
|
||||
//! webhook and MQTT audit-related settings.
|
||||
|
||||
mod amqp;
|
||||
mod kafka;
|
||||
mod mqtt;
|
||||
mod mysql;
|
||||
mod nats;
|
||||
mod postgres;
|
||||
mod pulsar;
|
||||
mod redis;
|
||||
mod webhook;
|
||||
|
||||
pub use amqp::*;
|
||||
pub use kafka::*;
|
||||
pub use mqtt::*;
|
||||
pub use mysql::*;
|
||||
pub use nats::*;
|
||||
pub use postgres::*;
|
||||
pub use pulsar::*;
|
||||
pub use redis::*;
|
||||
pub use webhook::*;
|
||||
|
||||
use crate::DEFAULT_DELIMITER;
|
||||
@@ -35,17 +43,24 @@ pub const AUDIT_PREFIX: &str = "audit";
|
||||
pub const AUDIT_ROUTE_PREFIX: &str = const_str::concat!(AUDIT_PREFIX, DEFAULT_DELIMITER);
|
||||
|
||||
pub const AUDIT_WEBHOOK_SUB_SYS: &str = "audit_webhook";
|
||||
pub const AUDIT_AMQP_SUB_SYS: &str = "audit_amqp";
|
||||
pub const AUDIT_KAFKA_SUB_SYS: &str = "audit_kafka";
|
||||
pub const AUDIT_MQTT_SUB_SYS: &str = "audit_mqtt";
|
||||
pub const AUDIT_MYSQL_SUB_SYS: &str = "audit_mysql";
|
||||
pub const AUDIT_NATS_SUB_SYS: &str = "audit_nats";
|
||||
pub const AUDIT_POSTGRES_SUB_SYS: &str = "audit_postgres";
|
||||
pub const AUDIT_PULSAR_SUB_SYS: &str = "audit_pulsar";
|
||||
|
||||
pub const AUDIT_REDIS_SUB_SYS: &str = "audit_redis";
|
||||
pub const AUDIT_REDIS_DEFAULT_CHANNEL: &str = "rustfs_audit_channel";
|
||||
pub const AUDIT_STORE_EXTENSION: &str = ".audit";
|
||||
#[allow(dead_code)]
|
||||
pub const AUDIT_SUB_SYSTEMS: &[&str] = &[
|
||||
AUDIT_AMQP_SUB_SYS,
|
||||
AUDIT_KAFKA_SUB_SYS,
|
||||
AUDIT_MQTT_SUB_SYS,
|
||||
AUDIT_MYSQL_SUB_SYS,
|
||||
AUDIT_NATS_SUB_SYS,
|
||||
AUDIT_POSTGRES_SUB_SYS,
|
||||
AUDIT_PULSAR_SUB_SYS,
|
||||
AUDIT_REDIS_SUB_SYS,
|
||||
AUDIT_WEBHOOK_SUB_SYS,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
/// A list of all valid configuration keys for an audit MySQL target.
|
||||
pub const AUDIT_MYSQL_KEYS: &[&str] = &[
|
||||
crate::ENABLE_KEY,
|
||||
crate::MYSQL_DSN_STRING,
|
||||
crate::MYSQL_TABLE,
|
||||
crate::MYSQL_FORMAT,
|
||||
crate::MYSQL_TLS_CA,
|
||||
crate::MYSQL_TLS_CLIENT_CERT,
|
||||
crate::MYSQL_TLS_CLIENT_KEY,
|
||||
crate::MYSQL_QUEUE_DIR,
|
||||
crate::MYSQL_QUEUE_LIMIT,
|
||||
crate::MYSQL_MAX_OPEN_CONNECTIONS,
|
||||
crate::COMMENT_KEY,
|
||||
];
|
||||
|
||||
// MySQL environment variables for audit target.
|
||||
pub const ENV_AUDIT_MYSQL_ENABLE: &str = "RUSTFS_AUDIT_MYSQL_ENABLE";
|
||||
pub const ENV_AUDIT_MYSQL_DSN_STRING: &str = "RUSTFS_AUDIT_MYSQL_DSN_STRING";
|
||||
pub const ENV_AUDIT_MYSQL_TABLE: &str = "RUSTFS_AUDIT_MYSQL_TABLE";
|
||||
pub const ENV_AUDIT_MYSQL_FORMAT: &str = "RUSTFS_AUDIT_MYSQL_FORMAT";
|
||||
pub const ENV_AUDIT_MYSQL_TLS_CA: &str = "RUSTFS_AUDIT_MYSQL_TLS_CA";
|
||||
pub const ENV_AUDIT_MYSQL_TLS_CLIENT_CERT: &str = "RUSTFS_AUDIT_MYSQL_TLS_CLIENT_CERT";
|
||||
pub const ENV_AUDIT_MYSQL_TLS_CLIENT_KEY: &str = "RUSTFS_AUDIT_MYSQL_TLS_CLIENT_KEY";
|
||||
pub const ENV_AUDIT_MYSQL_QUEUE_DIR: &str = "RUSTFS_AUDIT_MYSQL_QUEUE_DIR";
|
||||
pub const ENV_AUDIT_MYSQL_QUEUE_LIMIT: &str = "RUSTFS_AUDIT_MYSQL_QUEUE_LIMIT";
|
||||
pub const ENV_AUDIT_MYSQL_MAX_OPEN_CONNECTIONS: &str = "RUSTFS_AUDIT_MYSQL_MAX_OPEN_CONNECTIONS";
|
||||
|
||||
pub const ENV_AUDIT_MYSQL_KEYS: &[&str; 10] = &[
|
||||
ENV_AUDIT_MYSQL_ENABLE,
|
||||
ENV_AUDIT_MYSQL_DSN_STRING,
|
||||
ENV_AUDIT_MYSQL_TABLE,
|
||||
ENV_AUDIT_MYSQL_FORMAT,
|
||||
ENV_AUDIT_MYSQL_TLS_CA,
|
||||
ENV_AUDIT_MYSQL_TLS_CLIENT_CERT,
|
||||
ENV_AUDIT_MYSQL_TLS_CLIENT_KEY,
|
||||
ENV_AUDIT_MYSQL_QUEUE_DIR,
|
||||
ENV_AUDIT_MYSQL_QUEUE_LIMIT,
|
||||
ENV_AUDIT_MYSQL_MAX_OPEN_CONNECTIONS,
|
||||
];
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
pub const AUDIT_POSTGRES_KEYS: &[&str] = &[
|
||||
crate::ENABLE_KEY,
|
||||
crate::POSTGRES_DSN_STRING,
|
||||
crate::POSTGRES_TABLE,
|
||||
crate::POSTGRES_FORMAT,
|
||||
crate::POSTGRES_TLS_REQUIRED,
|
||||
crate::POSTGRES_TLS_CA,
|
||||
crate::POSTGRES_TLS_CLIENT_CERT,
|
||||
crate::POSTGRES_TLS_CLIENT_KEY,
|
||||
crate::POSTGRES_QUEUE_DIR,
|
||||
crate::POSTGRES_QUEUE_LIMIT,
|
||||
crate::COMMENT_KEY,
|
||||
];
|
||||
|
||||
pub const ENV_AUDIT_POSTGRES_ENABLE: &str = "RUSTFS_AUDIT_POSTGRES_ENABLE";
|
||||
pub const ENV_AUDIT_POSTGRES_DSN_STRING: &str = "RUSTFS_AUDIT_POSTGRES_DSN_STRING";
|
||||
pub const ENV_AUDIT_POSTGRES_TABLE: &str = "RUSTFS_AUDIT_POSTGRES_TABLE";
|
||||
pub const ENV_AUDIT_POSTGRES_FORMAT: &str = "RUSTFS_AUDIT_POSTGRES_FORMAT";
|
||||
pub const ENV_AUDIT_POSTGRES_TLS_REQUIRED: &str = "RUSTFS_AUDIT_POSTGRES_TLS_REQUIRED";
|
||||
pub const ENV_AUDIT_POSTGRES_TLS_CA: &str = "RUSTFS_AUDIT_POSTGRES_TLS_CA";
|
||||
pub const ENV_AUDIT_POSTGRES_TLS_CLIENT_CERT: &str = "RUSTFS_AUDIT_POSTGRES_TLS_CLIENT_CERT";
|
||||
pub const ENV_AUDIT_POSTGRES_TLS_CLIENT_KEY: &str = "RUSTFS_AUDIT_POSTGRES_TLS_CLIENT_KEY";
|
||||
pub const ENV_AUDIT_POSTGRES_QUEUE_DIR: &str = "RUSTFS_AUDIT_POSTGRES_QUEUE_DIR";
|
||||
pub const ENV_AUDIT_POSTGRES_QUEUE_LIMIT: &str = "RUSTFS_AUDIT_POSTGRES_QUEUE_LIMIT";
|
||||
|
||||
pub const ENV_AUDIT_POSTGRES_KEYS: &[&str; 10] = &[
|
||||
ENV_AUDIT_POSTGRES_ENABLE,
|
||||
ENV_AUDIT_POSTGRES_DSN_STRING,
|
||||
ENV_AUDIT_POSTGRES_TABLE,
|
||||
ENV_AUDIT_POSTGRES_FORMAT,
|
||||
ENV_AUDIT_POSTGRES_TLS_REQUIRED,
|
||||
ENV_AUDIT_POSTGRES_TLS_CA,
|
||||
ENV_AUDIT_POSTGRES_TLS_CLIENT_CERT,
|
||||
ENV_AUDIT_POSTGRES_TLS_CLIENT_KEY,
|
||||
ENV_AUDIT_POSTGRES_QUEUE_DIR,
|
||||
ENV_AUDIT_POSTGRES_QUEUE_LIMIT,
|
||||
];
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
pub const ENV_AUDIT_REDIS_ENABLE: &str = "RUSTFS_AUDIT_REDIS_ENABLE";
|
||||
pub const ENV_AUDIT_REDIS_URL: &str = "RUSTFS_AUDIT_REDIS_URL";
|
||||
pub const ENV_AUDIT_REDIS_CHANNEL: &str = "RUSTFS_AUDIT_REDIS_CHANNEL";
|
||||
pub const ENV_AUDIT_REDIS_USERNAME: &str = "RUSTFS_AUDIT_REDIS_USERNAME";
|
||||
pub const ENV_AUDIT_REDIS_PASSWORD: &str = "RUSTFS_AUDIT_REDIS_PASSWORD";
|
||||
pub const ENV_AUDIT_REDIS_KEEP_ALIVE_INTERVAL: &str = "RUSTFS_AUDIT_REDIS_KEEP_ALIVE_INTERVAL";
|
||||
pub const ENV_AUDIT_REDIS_QUEUE_DIR: &str = "RUSTFS_AUDIT_REDIS_QUEUE_DIR";
|
||||
pub const ENV_AUDIT_REDIS_QUEUE_LIMIT: &str = "RUSTFS_AUDIT_REDIS_QUEUE_LIMIT";
|
||||
pub const ENV_AUDIT_REDIS_MAX_RETRY_ATTEMPTS: &str = "RUSTFS_AUDIT_REDIS_MAX_RETRY_ATTEMPTS";
|
||||
pub const ENV_AUDIT_REDIS_RECONNECT_RETRY_ATTEMPTS: &str = "RUSTFS_AUDIT_REDIS_RECONNECT_RETRY_ATTEMPTS";
|
||||
pub const ENV_AUDIT_REDIS_MIN_RETRY_DELAY: &str = "RUSTFS_AUDIT_REDIS_MIN_RETRY_DELAY";
|
||||
pub const ENV_AUDIT_REDIS_MAX_RETRY_DELAY: &str = "RUSTFS_AUDIT_REDIS_MAX_RETRY_DELAY";
|
||||
pub const ENV_AUDIT_REDIS_CONNECTION_TIMEOUT: &str = "RUSTFS_AUDIT_REDIS_CONNECTION_TIMEOUT";
|
||||
pub const ENV_AUDIT_REDIS_RESPONSE_TIMEOUT: &str = "RUSTFS_AUDIT_REDIS_RESPONSE_TIMEOUT";
|
||||
pub const ENV_AUDIT_REDIS_PIPELINE_BUFFER_SIZE: &str = "RUSTFS_AUDIT_REDIS_PIPELINE_BUFFER_SIZE";
|
||||
pub const ENV_AUDIT_REDIS_TLS_POLICY: &str = "RUSTFS_AUDIT_REDIS_TLS_POLICY";
|
||||
pub const ENV_AUDIT_REDIS_TLS_CA: &str = "RUSTFS_AUDIT_REDIS_TLS_CA";
|
||||
pub const ENV_AUDIT_REDIS_TLS_CLIENT_CERT: &str = "RUSTFS_AUDIT_REDIS_TLS_CLIENT_CERT";
|
||||
pub const ENV_AUDIT_REDIS_TLS_CLIENT_KEY: &str = "RUSTFS_AUDIT_REDIS_TLS_CLIENT_KEY";
|
||||
pub const ENV_AUDIT_REDIS_TLS_ALLOW_INSECURE: &str = "RUSTFS_AUDIT_REDIS_TLS_ALLOW_INSECURE";
|
||||
|
||||
pub const ENV_AUDIT_REDIS_KEYS: &[&str; 20] = &[
|
||||
ENV_AUDIT_REDIS_ENABLE,
|
||||
ENV_AUDIT_REDIS_URL,
|
||||
ENV_AUDIT_REDIS_CHANNEL,
|
||||
ENV_AUDIT_REDIS_USERNAME,
|
||||
ENV_AUDIT_REDIS_PASSWORD,
|
||||
ENV_AUDIT_REDIS_KEEP_ALIVE_INTERVAL,
|
||||
ENV_AUDIT_REDIS_QUEUE_DIR,
|
||||
ENV_AUDIT_REDIS_QUEUE_LIMIT,
|
||||
ENV_AUDIT_REDIS_MAX_RETRY_ATTEMPTS,
|
||||
ENV_AUDIT_REDIS_RECONNECT_RETRY_ATTEMPTS,
|
||||
ENV_AUDIT_REDIS_MIN_RETRY_DELAY,
|
||||
ENV_AUDIT_REDIS_MAX_RETRY_DELAY,
|
||||
ENV_AUDIT_REDIS_CONNECTION_TIMEOUT,
|
||||
ENV_AUDIT_REDIS_RESPONSE_TIMEOUT,
|
||||
ENV_AUDIT_REDIS_PIPELINE_BUFFER_SIZE,
|
||||
ENV_AUDIT_REDIS_TLS_POLICY,
|
||||
ENV_AUDIT_REDIS_TLS_CA,
|
||||
ENV_AUDIT_REDIS_TLS_CLIENT_CERT,
|
||||
ENV_AUDIT_REDIS_TLS_CLIENT_KEY,
|
||||
ENV_AUDIT_REDIS_TLS_ALLOW_INSECURE,
|
||||
];
|
||||
|
||||
pub const AUDIT_REDIS_KEYS: &[&str] = &[
|
||||
crate::ENABLE_KEY,
|
||||
crate::REDIS_URL,
|
||||
crate::REDIS_CHANNEL,
|
||||
crate::REDIS_USERNAME,
|
||||
crate::REDIS_PASSWORD,
|
||||
crate::REDIS_KEEP_ALIVE_INTERVAL,
|
||||
crate::REDIS_QUEUE_DIR,
|
||||
crate::REDIS_QUEUE_LIMIT,
|
||||
crate::REDIS_MAX_RETRY_ATTEMPTS,
|
||||
crate::REDIS_RECONNECT_RETRY_ATTEMPTS,
|
||||
crate::REDIS_MIN_RETRY_DELAY,
|
||||
crate::REDIS_MAX_RETRY_DELAY,
|
||||
crate::REDIS_CONNECTION_TIMEOUT,
|
||||
crate::REDIS_RESPONSE_TIMEOUT,
|
||||
crate::REDIS_PIPELINE_BUFFER_SIZE,
|
||||
crate::REDIS_TLS_POLICY,
|
||||
crate::REDIS_TLS_CA,
|
||||
crate::REDIS_TLS_CLIENT_CERT,
|
||||
crate::REDIS_TLS_CLIENT_KEY,
|
||||
crate::REDIS_TLS_ALLOW_INSECURE,
|
||||
crate::COMMENT_KEY,
|
||||
];
|
||||
@@ -155,6 +155,22 @@ pub const ENV_RUSTFS_SECRET_KEY: &str = "RUSTFS_SECRET_KEY";
|
||||
/// Environment variable for server secret key file.
|
||||
pub const ENV_RUSTFS_SECRET_KEY_FILE: &str = "RUSTFS_SECRET_KEY_FILE";
|
||||
|
||||
/// Environment variable to explicitly allow public default root credentials.
|
||||
///
|
||||
/// This is intended for local development only. Production startup paths should
|
||||
/// provide non-default `RUSTFS_ACCESS_KEY` and `RUSTFS_SECRET_KEY` values.
|
||||
pub const ENV_RUSTFS_ALLOW_INSECURE_DEFAULT_CREDENTIALS: &str = "RUSTFS_ALLOW_INSECURE_DEFAULT_CREDENTIALS";
|
||||
|
||||
/// Environment variable controlling startup behavior when unsupported filesystem types are detected.
|
||||
///
|
||||
/// Accepted values:
|
||||
/// - "warn" (default): log a warning and continue startup
|
||||
/// - "fail": abort startup with an error
|
||||
pub const ENV_RUSTFS_UNSUPPORTED_FS_POLICY: &str = "RUSTFS_UNSUPPORTED_FS_POLICY";
|
||||
pub const RUSTFS_UNSUPPORTED_FS_POLICY_WARN: &str = "warn";
|
||||
pub const RUSTFS_UNSUPPORTED_FS_POLICY_FAIL: &str = "fail";
|
||||
pub const DEFAULT_RUSTFS_UNSUPPORTED_FS_POLICY: &str = RUSTFS_UNSUPPORTED_FS_POLICY_WARN;
|
||||
|
||||
/// Environment variable for server OBS endpoint.
|
||||
pub const ENV_RUSTFS_OBS_ENDPOINT: &str = "RUSTFS_OBS_ENDPOINT";
|
||||
|
||||
@@ -293,9 +309,9 @@ pub const DEFAULT_OBS_LOGS_EXPORT_ENABLED: bool = true;
|
||||
|
||||
/// Default profiling export enabled
|
||||
/// It is used to enable or disable exporting profiles
|
||||
/// Default value: false
|
||||
/// Default value: true
|
||||
/// Environment variable: RUSTFS_OBS_PROFILING_EXPORT_ENABLED
|
||||
pub const DEFAULT_OBS_PROFILING_EXPORT_ENABLED: bool = false;
|
||||
pub const DEFAULT_OBS_PROFILING_EXPORT_ENABLED: bool = true;
|
||||
|
||||
/// Default log local logging enabled for rustfs
|
||||
/// This is the default log local logging enabled for rustfs.
|
||||
|
||||
@@ -47,6 +47,16 @@ pub const DEFAULT_DRIVE_ACTIVE_CHECK_INTERVAL_SECS: u64 = 15;
|
||||
pub const ENV_DRIVE_ACTIVE_CHECK_TIMEOUT_SECS: &str = "RUSTFS_DRIVE_ACTIVE_CHECK_TIMEOUT_SECS";
|
||||
pub const DEFAULT_DRIVE_ACTIVE_CHECK_TIMEOUT_SECS: u64 = 5;
|
||||
|
||||
/// Timeout-to-health transition policy for drive operations.
|
||||
///
|
||||
/// Accepted values:
|
||||
/// - "mark_failure" (default): timeout marks failure and may transition runtime state.
|
||||
/// - "ignore_scanner": timeout does not mark failure for scanner-sensitive operations.
|
||||
pub const ENV_DRIVE_TIMEOUT_HEALTH_ACTION: &str = "RUSTFS_DRIVE_TIMEOUT_HEALTH_ACTION";
|
||||
pub const DRIVE_TIMEOUT_HEALTH_ACTION_MARK_FAILURE: &str = "mark_failure";
|
||||
pub const DRIVE_TIMEOUT_HEALTH_ACTION_IGNORE_SCANNER: &str = "ignore_scanner";
|
||||
pub const DEFAULT_DRIVE_TIMEOUT_HEALTH_ACTION: &str = DRIVE_TIMEOUT_HEALTH_ACTION_MARK_FAILURE;
|
||||
|
||||
/// Number of consecutive failures before a suspect drive is classified as offline.
|
||||
pub const ENV_DRIVE_SUSPECT_FAILURE_THRESHOLD: &str = "RUSTFS_DRIVE_SUSPECT_FAILURE_THRESHOLD";
|
||||
pub const DEFAULT_DRIVE_SUSPECT_FAILURE_THRESHOLD: u64 = 2;
|
||||
@@ -66,3 +76,19 @@ pub const DEFAULT_DRIVE_OFFLINE_GRACE_PERIOD_SECS: u64 = 30;
|
||||
/// Duration in seconds after which a recovered drive is classified as long offline.
|
||||
pub const ENV_DRIVE_LONG_OFFLINE_THRESHOLD_SECS: &str = "RUSTFS_DRIVE_LONG_OFFLINE_THRESHOLD_SECS";
|
||||
pub const DEFAULT_DRIVE_LONG_OFFLINE_THRESHOLD_SECS: u64 = 172_800;
|
||||
|
||||
/// Drive timeout profile preset.
|
||||
///
|
||||
/// Accepted values:
|
||||
/// - "default": keep current timeout defaults.
|
||||
/// - "high_latency": use a higher default timeout preset for scanner-sensitive and metadata operations.
|
||||
///
|
||||
/// Explicit per-operation overrides (`RUSTFS_DRIVE_*_TIMEOUT_SECS`) still take precedence.
|
||||
pub const ENV_DRIVE_TIMEOUT_PROFILE: &str = "RUSTFS_DRIVE_TIMEOUT_PROFILE";
|
||||
pub const DRIVE_TIMEOUT_PROFILE_DEFAULT: &str = "default";
|
||||
pub const DRIVE_TIMEOUT_PROFILE_HIGH_LATENCY: &str = "high_latency";
|
||||
pub const DEFAULT_DRIVE_TIMEOUT_PROFILE: &str = DRIVE_TIMEOUT_PROFILE_DEFAULT;
|
||||
|
||||
/// Timeout preset (seconds) used when `RUSTFS_DRIVE_TIMEOUT_PROFILE=high_latency`
|
||||
/// and no per-operation timeout override is provided.
|
||||
pub const DRIVE_TIMEOUT_PROFILE_HIGH_LATENCY_SECS: u64 = 60;
|
||||
|
||||
@@ -34,6 +34,12 @@ pub const ENV_NOTIFY_ENABLE: &str = "RUSTFS_NOTIFY_ENABLE";
|
||||
pub const DEFAULT_AUDIT_ENABLE: bool = false;
|
||||
/// Default global notify switch (disabled by default).
|
||||
pub const DEFAULT_NOTIFY_ENABLE: bool = false;
|
||||
/// Canonical ILM process boundary env key (seconds).
|
||||
pub const ENV_ILM_PROCESS_TIME: &str = "RUSTFS_ILM_PROCESS_TIME";
|
||||
/// Deprecated ILM process boundary env key kept for compatibility.
|
||||
pub const ENV_ILM_PROCESS_TIME_DEPRECATED: &str = "_RUSTFS_ILM_PROCESS_TIME";
|
||||
/// Default ILM process boundary in seconds (24h).
|
||||
pub const DEFAULT_ILM_PROCESS_TIME_SECS: i32 = 86400;
|
||||
|
||||
/// Medium-drawn lines separator
|
||||
/// This is used to separate words in environment variable names.
|
||||
|
||||
@@ -32,6 +32,16 @@ pub const DEFAULT_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS: u64 = 3;
|
||||
pub const ENV_INTERNODE_RPC_TIMEOUT_SECS: &str = "RUSTFS_INTERNODE_RPC_TIMEOUT_SECS";
|
||||
pub const DEFAULT_INTERNODE_RPC_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
/// Environment variable for selecting the internode data-plane transport backend.
|
||||
pub const ENV_RUSTFS_INTERNODE_DATA_TRANSPORT: &str = "RUSTFS_INTERNODE_DATA_TRANSPORT";
|
||||
pub const DEFAULT_INTERNODE_DATA_TRANSPORT: &str = "tcp-http";
|
||||
|
||||
/// Legacy alias for "tcp-http". Both values select the TCP/HTTP transport backend.
|
||||
pub const INTERNODE_DATA_TRANSPORT_TCP: &str = "tcp";
|
||||
|
||||
/// Known internode transport backend names accepted by the config parser.
|
||||
pub const KNOWN_INTERNODE_DATA_TRANSPORT_BACKENDS: &[&str] = &[DEFAULT_INTERNODE_DATA_TRANSPORT, INTERNODE_DATA_TRANSPORT_TCP];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -58,5 +68,9 @@ mod tests {
|
||||
"RUSTFS_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS"
|
||||
);
|
||||
assert_eq!(ENV_INTERNODE_RPC_TIMEOUT_SECS, "RUSTFS_INTERNODE_RPC_TIMEOUT_SECS");
|
||||
assert_eq!(ENV_RUSTFS_INTERNODE_DATA_TRANSPORT, "RUSTFS_INTERNODE_DATA_TRANSPORT");
|
||||
assert_eq!(DEFAULT_INTERNODE_DATA_TRANSPORT, "tcp-http");
|
||||
assert_eq!(INTERNODE_DATA_TRANSPORT_TCP, "tcp");
|
||||
assert_eq!(KNOWN_INTERNODE_DATA_TRANSPORT_BACKENDS, &["tcp-http", "tcp"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,3 +57,114 @@ pub const ENV_WEBDAV_CERTS_DIR: &str = "RUSTFS_WEBDAV_CERTS_DIR";
|
||||
pub const ENV_WEBDAV_CA_FILE: &str = "RUSTFS_WEBDAV_CA_FILE";
|
||||
pub const ENV_WEBDAV_MAX_BODY_SIZE: &str = "RUSTFS_WEBDAV_MAX_BODY_SIZE";
|
||||
pub const ENV_WEBDAV_REQUEST_TIMEOUT: &str = "RUSTFS_WEBDAV_REQUEST_TIMEOUT";
|
||||
|
||||
/// Default SFTP server bind address.
|
||||
pub const DEFAULT_SFTP_ADDRESS: &str = "0.0.0.0:2222";
|
||||
|
||||
/// Default for SFTP host-key directory. None means no default. Operators
|
||||
/// must set RUSTFS_SFTP_HOST_KEY_DIR explicitly when SFTP is enabled.
|
||||
pub const DEFAULT_SFTP_HOST_KEY_DIR: Option<&str> = None;
|
||||
|
||||
/// SFTP environment variable names.
|
||||
pub const ENV_SFTP_ENABLE: &str = "RUSTFS_SFTP_ENABLE";
|
||||
pub const ENV_SFTP_ADDRESS: &str = "RUSTFS_SFTP_ADDRESS";
|
||||
pub const ENV_SFTP_HOST_KEY_DIR: &str = "RUSTFS_SFTP_HOST_KEY_DIR";
|
||||
pub const ENV_SFTP_HOST_KEY_RELOAD_ENABLE: &str = "RUSTFS_SFTP_HOST_KEY_RELOAD_ENABLE";
|
||||
pub const ENV_SFTP_HOST_KEY_RELOAD_INTERVAL: &str = "RUSTFS_SFTP_HOST_KEY_RELOAD_INTERVAL";
|
||||
pub const ENV_SFTP_IDLE_TIMEOUT: &str = "RUSTFS_SFTP_IDLE_TIMEOUT";
|
||||
/// S3 multipart part size in bytes. Default DEFAULT_SFTP_PART_SIZE (16 MiB).
|
||||
/// Valid range 5 MiB to 5 GiB (S3 protocol bounds), enforced at startup.
|
||||
///
|
||||
/// The per-upload size ceiling is part_size * 10_000 (the S3 parts cap),
|
||||
/// so the default caps single uploads at 160 GiB. Deployments expecting
|
||||
/// larger single files must raise this: 64 MiB -> 640 GiB, 128 MiB ->
|
||||
/// 1.25 TiB, 512 MiB -> 5 TiB (S3 object max). Rename is not affected;
|
||||
/// multipart_copy scales the per-part size dynamically and handles up
|
||||
/// to the 5 TiB S3 object limit regardless of this setting.
|
||||
pub const ENV_SFTP_PART_SIZE: &str = "RUSTFS_SFTP_PART_SIZE";
|
||||
pub const ENV_SFTP_READ_ONLY: &str = "RUSTFS_SFTP_READ_ONLY";
|
||||
pub const ENV_SFTP_BANNER: &str = "RUSTFS_SFTP_BANNER";
|
||||
/// Optional environment variable. If RUSTFS_SFTP_HANDLES_PER_SESSION
|
||||
/// is not set in the process environment, the server uses the default
|
||||
/// of 64 handles per session and emits no warning. If set, the value
|
||||
/// must be in the inclusive range 8 to 1024. Out-of-range values fall
|
||||
/// back to the default of 64 with a warn-level log naming the
|
||||
/// requested value and the bounds.
|
||||
///
|
||||
/// Caps the maximum number of simultaneously-open SFTP handles per
|
||||
/// session. A handle is the server-side identifier returned by
|
||||
/// SSH_FXP_OPEN and SSH_FXP_OPENDIR. One client typically uses one
|
||||
/// handle per file in flight plus one per directory listing.
|
||||
/// Operators running clients with deep pipelining may raise this.
|
||||
pub const ENV_SFTP_HANDLES_PER_SESSION: &str = "RUSTFS_SFTP_HANDLES_PER_SESSION";
|
||||
|
||||
/// Optional environment variable. If RUSTFS_SFTP_BACKEND_OP_TIMEOUT_SECS
|
||||
/// is not set in the process environment, the server uses the default
|
||||
/// of 60 seconds and emits no warning. If set, the value must be in
|
||||
/// the inclusive range 5 to 600 seconds. Out-of-range values fall
|
||||
/// back to the default with a warn-level log naming the requested
|
||||
/// value and the bounds.
|
||||
///
|
||||
/// Bounds every storage backend call issued by the SFTP driver. A
|
||||
/// backend that does not respond within this many seconds returns
|
||||
/// Failure to the client and emits a warn log naming the backend
|
||||
/// method. This catches a backend that accepted the request and never
|
||||
/// returned a body, which the SSH keepalive cannot detect because the
|
||||
/// transport itself remains live.
|
||||
pub const ENV_SFTP_BACKEND_OP_TIMEOUT_SECS: &str = "RUSTFS_SFTP_BACKEND_OP_TIMEOUT_SECS";
|
||||
|
||||
/// Optional environment variable. If RUSTFS_SFTP_READ_CACHE_WINDOW_BYTES
|
||||
/// is not set in the process environment, the server uses a 4 MiB
|
||||
/// default and emits no warning. If set, the value must be in the
|
||||
/// inclusive range MAX_READ_LEN (256 KiB) to 64 MiB. Out-of-range
|
||||
/// values fall back to the default with a warn-level log naming the
|
||||
/// requested value and the bounds.
|
||||
///
|
||||
/// Per-handle byte window the SFTP read path fetches in one backend
|
||||
/// call on a cache miss. Subsequent FXP_READs within that window are
|
||||
/// served from the buffer without a backend round trip. For
|
||||
/// sequential downloads the backend round-trip count drops by
|
||||
/// window_bytes / MAX_READ_LEN. Random-access workloads should set
|
||||
/// the window equal to MAX_READ_LEN to opt out of read-ahead.
|
||||
pub const ENV_SFTP_READ_CACHE_WINDOW_BYTES: &str = "RUSTFS_SFTP_READ_CACHE_WINDOW_BYTES";
|
||||
|
||||
/// Optional environment variable. If
|
||||
/// RUSTFS_SFTP_READ_CACHE_TOTAL_MEM_BYTES is not set in the process
|
||||
/// environment, the server uses a 256 MiB default and emits no
|
||||
/// warning. If set, the value must be at least 16 MiB. Below-min
|
||||
/// values fall back to the default with a warn-level log naming the
|
||||
/// requested value and the bound.
|
||||
///
|
||||
/// Process-wide ceiling on cumulative read cache memory across every
|
||||
/// live SFTP handle. Once the accumulator plus a new window would
|
||||
/// exceed this value, the populate call on the per-handle cache is
|
||||
/// skipped. The read still completes from the freshly-fetched bytes
|
||||
/// without storing them in the cache, at the cost of one backend
|
||||
/// call per FXP_READ. High-concurrency deployments expecting many
|
||||
/// parallel downloads should raise this in step with the per-session
|
||||
/// handle cap.
|
||||
pub const ENV_SFTP_READ_CACHE_TOTAL_MEM_BYTES: &str = "RUSTFS_SFTP_READ_CACHE_TOTAL_MEM_BYTES";
|
||||
|
||||
/// Default idle session timeout in seconds.
|
||||
pub const DEFAULT_SFTP_IDLE_TIMEOUT: u64 = 600;
|
||||
|
||||
/// Default SFTP host key hot reload enabled state.
|
||||
pub const DEFAULT_SFTP_HOST_KEY_RELOAD_ENABLE: bool = false;
|
||||
|
||||
/// Default SFTP host key hot reload interval in seconds.
|
||||
pub const DEFAULT_SFTP_HOST_KEY_RELOAD_INTERVAL: u64 = 30;
|
||||
|
||||
/// Default S3 multipart upload part size in bytes (16 MiB).
|
||||
///
|
||||
/// The per-upload size ceiling is part_size * 10_000 (the S3 parts cap),
|
||||
/// so the default gives a 160 GiB single-upload limit. Deployments that
|
||||
/// expect single files larger than this must raise part_size:
|
||||
/// 64 MiB -> 640 GiB, 128 MiB -> 1.25 TiB, 512 MiB -> 5 TiB (S3 max).
|
||||
/// The minimum is 5 MiB and the maximum is 5 GiB (S3 protocol bounds).
|
||||
pub const DEFAULT_SFTP_PART_SIZE: u64 = 16_777_216;
|
||||
|
||||
/// Default read-only mode (disabled).
|
||||
pub const DEFAULT_SFTP_READ_ONLY: bool = false;
|
||||
|
||||
/// Default SSH identification string (no version disclosure).
|
||||
pub const DEFAULT_SFTP_BANNER: &str = "SSH-2.0-RustFS";
|
||||
|
||||
@@ -59,6 +59,25 @@ pub const DEFAULT_RUNTIME_DIAL9_ROTATION_COUNT: usize = 10;
|
||||
pub const DEFAULT_RUNTIME_DIAL9_SAMPLING_RATE: f64 = 1.0; // 100% sampling
|
||||
// Note: S3 bucket/prefix have no default; absence means upload is disabled (modeled as Option<String>)
|
||||
|
||||
/// Maximum transition workers used as a local fallback when runtime env is unset.
|
||||
pub const DEFAULT_TRANSITION_WORKERS_CAP: i64 = 16;
|
||||
/// Absolute upper bound for transition workers accepted from runtime env.
|
||||
pub const DEFAULT_TRANSITION_WORKERS_ABSOLUTE_MAX: i64 = 32;
|
||||
/// Default capacity for the transition queue.
|
||||
pub const DEFAULT_TRANSITION_QUEUE_CAPACITY: usize = 1000;
|
||||
/// Default send timeout for transition queue enqueue attempts, in milliseconds.
|
||||
pub const DEFAULT_TRANSITION_QUEUE_SEND_TIMEOUT_MS: usize = 100;
|
||||
/// Test-only fault injection env var that forces the immediate transition enqueue timeout path.
|
||||
pub const ENV_TEST_FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT: &str = "RUSTFS_TEST_FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT";
|
||||
/// Runtime env var controlling the transition worker count.
|
||||
pub const ENV_TRANSITION_WORKERS: &str = "RUSTFS_MAX_TRANSITION_WORKERS";
|
||||
/// Runtime env var controlling the absolute maximum transition workers.
|
||||
pub const ENV_TRANSITION_WORKERS_ABSOLUTE_MAX: &str = "RUSTFS_ABSOLUTE_MAX_WORKERS";
|
||||
/// Runtime env var controlling the transition queue capacity.
|
||||
pub const ENV_TRANSITION_QUEUE_CAPACITY: &str = "RUSTFS_TRANSITION_QUEUE_CAPACITY";
|
||||
/// Runtime env var controlling the transition queue send timeout in milliseconds.
|
||||
pub const ENV_TRANSITION_QUEUE_SEND_TIMEOUT_MS: &str = "RUSTFS_TRANSITION_QUEUE_SEND_TIMEOUT_MS";
|
||||
|
||||
// Allocator reclaim configuration
|
||||
pub const ENV_ALLOCATOR_RECLAIM_ENABLED: &str = "RUSTFS_ALLOCATOR_RECLAIM_ENABLED";
|
||||
pub const ENV_ALLOCATOR_RECLAIM_INTERVAL_SECS: &str = "RUSTFS_ALLOCATOR_RECLAIM_INTERVAL_SECS";
|
||||
|
||||
@@ -40,6 +40,37 @@ pub const ENV_SCANNER_SPEED: &str = "RUSTFS_SCANNER_SPEED";
|
||||
/// Default scanner speed preset.
|
||||
pub const DEFAULT_SCANNER_SPEED: &str = "default";
|
||||
|
||||
/// Environment variable that specifies the periodic bitrot scan cycle in seconds.
|
||||
/// When set to `0`, `true`, `on`, or `yes`, every scanner cycle runs in deep mode.
|
||||
/// When set to `false`, `off`, `no`, or `disabled`, periodic deep scans are disabled.
|
||||
/// - Unit: seconds (u64).
|
||||
/// - Example: `export RUSTFS_SCANNER_BITROT_CYCLE_SECS=2592000` (30 days)
|
||||
pub const ENV_SCANNER_BITROT_CYCLE_SECS: &str = "RUSTFS_SCANNER_BITROT_CYCLE_SECS";
|
||||
|
||||
/// Default bitrot scan cycle used by the scanner.
|
||||
pub const DEFAULT_SCANNER_BITROT_CYCLE_SECS: u64 = 30 * 24 * 60 * 60;
|
||||
|
||||
/// Environment variable that controls how many object versions trigger scanner alerts.
|
||||
/// - Example: `export RUSTFS_SCANNER_ALERT_EXCESS_VERSIONS=100`
|
||||
pub const ENV_SCANNER_ALERT_EXCESS_VERSIONS: &str = "RUSTFS_SCANNER_ALERT_EXCESS_VERSIONS";
|
||||
|
||||
/// Default object version count that triggers scanner alerts.
|
||||
pub const DEFAULT_SCANNER_ALERT_EXCESS_VERSIONS: u64 = 100;
|
||||
|
||||
/// Environment variable that controls how many cumulative bytes across object versions trigger scanner alerts.
|
||||
/// - Example: `export RUSTFS_SCANNER_ALERT_EXCESS_VERSION_SIZE=1099511627776`
|
||||
pub const ENV_SCANNER_ALERT_EXCESS_VERSION_SIZE: &str = "RUSTFS_SCANNER_ALERT_EXCESS_VERSION_SIZE";
|
||||
|
||||
/// Default cumulative object version bytes that trigger scanner alerts.
|
||||
pub const DEFAULT_SCANNER_ALERT_EXCESS_VERSION_SIZE: u64 = 1024 * 1024 * 1024 * 1024;
|
||||
|
||||
/// Environment variable that controls how many subfolders trigger scanner alerts.
|
||||
/// - Example: `export RUSTFS_SCANNER_ALERT_EXCESS_FOLDERS=50000`
|
||||
pub const ENV_SCANNER_ALERT_EXCESS_FOLDERS: &str = "RUSTFS_SCANNER_ALERT_EXCESS_FOLDERS";
|
||||
|
||||
/// Default subfolder count that triggers scanner alerts.
|
||||
pub const DEFAULT_SCANNER_ALERT_EXCESS_FOLDERS: u64 = 50_000;
|
||||
|
||||
/// Environment variable that controls whether the scanner sleeps between operations.
|
||||
/// When `true` (default), the scanner throttles itself. When `false`, it runs at full speed.
|
||||
/// - Example: `export RUSTFS_SCANNER_IDLE_MODE=false`
|
||||
|
||||
@@ -50,6 +50,19 @@ pub const KAFKA_TLS_CA: &str = "tls_ca";
|
||||
pub const KAFKA_TLS_CLIENT_CERT: &str = "tls_client_cert";
|
||||
pub const KAFKA_TLS_CLIENT_KEY: &str = "tls_client_key";
|
||||
|
||||
pub const AMQP_URL: &str = "url";
|
||||
pub const AMQP_EXCHANGE: &str = "exchange";
|
||||
pub const AMQP_ROUTING_KEY: &str = "routing_key";
|
||||
pub const AMQP_MANDATORY: &str = "mandatory";
|
||||
pub const AMQP_PERSISTENT: &str = "persistent";
|
||||
pub const AMQP_USERNAME: &str = "username";
|
||||
pub const AMQP_PASSWORD: &str = "password";
|
||||
pub const AMQP_TLS_CA: &str = "tls_ca";
|
||||
pub const AMQP_TLS_CLIENT_CERT: &str = "tls_client_cert";
|
||||
pub const AMQP_TLS_CLIENT_KEY: &str = "tls_client_key";
|
||||
pub const AMQP_QUEUE_DIR: &str = "queue_dir";
|
||||
pub const AMQP_QUEUE_LIMIT: &str = "queue_limit";
|
||||
|
||||
pub const NATS_ADDRESS: &str = "address";
|
||||
pub const NATS_SUBJECT: &str = "subject";
|
||||
pub const NATS_USERNAME: &str = "username";
|
||||
@@ -74,6 +87,48 @@ pub const PULSAR_TLS_HOSTNAME_VERIFICATION: &str = "tls_hostname_verification";
|
||||
pub const PULSAR_QUEUE_DIR: &str = "queue_dir";
|
||||
pub const PULSAR_QUEUE_LIMIT: &str = "queue_limit";
|
||||
|
||||
pub const BASE_DSN_STRING: &str = "dsn_string";
|
||||
|
||||
pub const MYSQL_DSN_STRING: &str = BASE_DSN_STRING;
|
||||
pub const MYSQL_TABLE: &str = "table";
|
||||
pub const MYSQL_FORMAT: &str = "format";
|
||||
pub const MYSQL_TLS_CA: &str = "tls_ca";
|
||||
pub const MYSQL_TLS_CLIENT_CERT: &str = "tls_client_cert";
|
||||
pub const MYSQL_TLS_CLIENT_KEY: &str = "tls_client_key";
|
||||
pub const MYSQL_QUEUE_DIR: &str = "queue_dir";
|
||||
pub const MYSQL_QUEUE_LIMIT: &str = "queue_limit";
|
||||
pub const MYSQL_MAX_OPEN_CONNECTIONS: &str = "max_open_connections";
|
||||
|
||||
pub const REDIS_URL: &str = "url";
|
||||
pub const REDIS_CHANNEL: &str = "channel";
|
||||
pub const REDIS_USERNAME: &str = "username";
|
||||
pub const REDIS_PASSWORD: &str = "password";
|
||||
pub const REDIS_KEEP_ALIVE_INTERVAL: &str = "keep_alive_interval";
|
||||
pub const REDIS_QUEUE_DIR: &str = "queue_dir";
|
||||
pub const REDIS_QUEUE_LIMIT: &str = "queue_limit";
|
||||
pub const REDIS_MAX_RETRY_ATTEMPTS: &str = "max_retry_attempts";
|
||||
pub const REDIS_RECONNECT_RETRY_ATTEMPTS: &str = "reconnect_retry_attempts";
|
||||
pub const REDIS_MIN_RETRY_DELAY: &str = "min_retry_delay";
|
||||
pub const REDIS_MAX_RETRY_DELAY: &str = "max_retry_delay";
|
||||
pub const REDIS_CONNECTION_TIMEOUT: &str = "connection_timeout";
|
||||
pub const REDIS_RESPONSE_TIMEOUT: &str = "response_timeout";
|
||||
pub const REDIS_PIPELINE_BUFFER_SIZE: &str = "pipeline_buffer_size";
|
||||
pub const REDIS_TLS_POLICY: &str = "tls_policy";
|
||||
pub const REDIS_TLS_CA: &str = "tls_ca";
|
||||
pub const REDIS_TLS_CLIENT_CERT: &str = "tls_client_cert";
|
||||
pub const REDIS_TLS_CLIENT_KEY: &str = "tls_client_key";
|
||||
pub const REDIS_TLS_ALLOW_INSECURE: &str = "tls_allow_insecure";
|
||||
|
||||
pub const POSTGRES_DSN_STRING: &str = BASE_DSN_STRING;
|
||||
pub const POSTGRES_TABLE: &str = "table";
|
||||
pub const POSTGRES_FORMAT: &str = "format";
|
||||
pub const POSTGRES_TLS_REQUIRED: &str = "tls_required";
|
||||
pub const POSTGRES_TLS_CA: &str = "tls_ca";
|
||||
pub const POSTGRES_TLS_CLIENT_CERT: &str = "tls_client_cert";
|
||||
pub const POSTGRES_TLS_CLIENT_KEY: &str = "tls_client_key";
|
||||
pub const POSTGRES_QUEUE_DIR: &str = "queue_dir";
|
||||
pub const POSTGRES_QUEUE_LIMIT: &str = "queue_limit";
|
||||
|
||||
/// Environment variable controlling whether target queue files are Snappy-compressed.
|
||||
/// Applies to both notify and audit target queue stores.
|
||||
pub const ENV_TARGET_STORE_COMPRESS: &str = "RUSTFS_TARGET_STORE_COMPRESS";
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
pub const NOTIFY_AMQP_KEYS: &[&str] = &[
|
||||
crate::ENABLE_KEY,
|
||||
crate::AMQP_URL,
|
||||
crate::AMQP_EXCHANGE,
|
||||
crate::AMQP_ROUTING_KEY,
|
||||
crate::AMQP_MANDATORY,
|
||||
crate::AMQP_PERSISTENT,
|
||||
crate::AMQP_USERNAME,
|
||||
crate::AMQP_PASSWORD,
|
||||
crate::AMQP_TLS_CA,
|
||||
crate::AMQP_TLS_CLIENT_CERT,
|
||||
crate::AMQP_TLS_CLIENT_KEY,
|
||||
crate::AMQP_QUEUE_DIR,
|
||||
crate::AMQP_QUEUE_LIMIT,
|
||||
crate::COMMENT_KEY,
|
||||
];
|
||||
|
||||
pub const ENV_NOTIFY_AMQP_ENABLE: &str = "RUSTFS_NOTIFY_AMQP_ENABLE";
|
||||
pub const ENV_NOTIFY_AMQP_URL: &str = "RUSTFS_NOTIFY_AMQP_URL";
|
||||
pub const ENV_NOTIFY_AMQP_EXCHANGE: &str = "RUSTFS_NOTIFY_AMQP_EXCHANGE";
|
||||
pub const ENV_NOTIFY_AMQP_ROUTING_KEY: &str = "RUSTFS_NOTIFY_AMQP_ROUTING_KEY";
|
||||
pub const ENV_NOTIFY_AMQP_MANDATORY: &str = "RUSTFS_NOTIFY_AMQP_MANDATORY";
|
||||
pub const ENV_NOTIFY_AMQP_PERSISTENT: &str = "RUSTFS_NOTIFY_AMQP_PERSISTENT";
|
||||
pub const ENV_NOTIFY_AMQP_USERNAME: &str = "RUSTFS_NOTIFY_AMQP_USERNAME";
|
||||
pub const ENV_NOTIFY_AMQP_PASSWORD: &str = "RUSTFS_NOTIFY_AMQP_PASSWORD";
|
||||
pub const ENV_NOTIFY_AMQP_TLS_CA: &str = "RUSTFS_NOTIFY_AMQP_TLS_CA";
|
||||
pub const ENV_NOTIFY_AMQP_TLS_CLIENT_CERT: &str = "RUSTFS_NOTIFY_AMQP_TLS_CLIENT_CERT";
|
||||
pub const ENV_NOTIFY_AMQP_TLS_CLIENT_KEY: &str = "RUSTFS_NOTIFY_AMQP_TLS_CLIENT_KEY";
|
||||
pub const ENV_NOTIFY_AMQP_QUEUE_DIR: &str = "RUSTFS_NOTIFY_AMQP_QUEUE_DIR";
|
||||
pub const ENV_NOTIFY_AMQP_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_AMQP_QUEUE_LIMIT";
|
||||
|
||||
pub const ENV_NOTIFY_AMQP_KEYS: &[&str; 13] = &[
|
||||
ENV_NOTIFY_AMQP_ENABLE,
|
||||
ENV_NOTIFY_AMQP_URL,
|
||||
ENV_NOTIFY_AMQP_EXCHANGE,
|
||||
ENV_NOTIFY_AMQP_ROUTING_KEY,
|
||||
ENV_NOTIFY_AMQP_MANDATORY,
|
||||
ENV_NOTIFY_AMQP_PERSISTENT,
|
||||
ENV_NOTIFY_AMQP_USERNAME,
|
||||
ENV_NOTIFY_AMQP_PASSWORD,
|
||||
ENV_NOTIFY_AMQP_TLS_CA,
|
||||
ENV_NOTIFY_AMQP_TLS_CLIENT_CERT,
|
||||
ENV_NOTIFY_AMQP_TLS_CLIENT_KEY,
|
||||
ENV_NOTIFY_AMQP_QUEUE_DIR,
|
||||
ENV_NOTIFY_AMQP_QUEUE_LIMIT,
|
||||
];
|
||||
@@ -12,19 +12,27 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod amqp;
|
||||
mod arn;
|
||||
mod kafka;
|
||||
mod mqtt;
|
||||
mod mysql;
|
||||
mod nats;
|
||||
mod postgres;
|
||||
mod pulsar;
|
||||
mod redis;
|
||||
mod store;
|
||||
mod webhook;
|
||||
|
||||
pub use amqp::*;
|
||||
pub use arn::*;
|
||||
pub use kafka::*;
|
||||
pub use mqtt::*;
|
||||
pub use mysql::*;
|
||||
pub use nats::*;
|
||||
pub use postgres::*;
|
||||
pub use pulsar::*;
|
||||
pub use redis::*;
|
||||
pub use store::*;
|
||||
pub use webhook::*;
|
||||
|
||||
@@ -69,30 +77,31 @@ pub const ENV_NOTIFY_SEND_CONCURRENCY: &str = "RUSTFS_NOTIFY_SEND_CONCURRENCY";
|
||||
/// Adjust this value based on your system's capabilities and expected load.
|
||||
pub const DEFAULT_NOTIFY_SEND_CONCURRENCY: usize = 64;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const NOTIFY_SUB_SYSTEMS: &[&str] = &[
|
||||
NOTIFY_AMQP_SUB_SYS,
|
||||
NOTIFY_KAFKA_SUB_SYS,
|
||||
NOTIFY_MQTT_SUB_SYS,
|
||||
NOTIFY_MYSQL_SUB_SYS,
|
||||
NOTIFY_NATS_SUB_SYS,
|
||||
NOTIFY_POSTGRES_SUB_SYS,
|
||||
NOTIFY_PULSAR_SUB_SYS,
|
||||
NOTIFY_REDIS_SUB_SYS,
|
||||
NOTIFY_WEBHOOK_SUB_SYS,
|
||||
];
|
||||
|
||||
pub const NOTIFY_KAFKA_SUB_SYS: &str = "notify_kafka";
|
||||
pub const NOTIFY_MQTT_SUB_SYS: &str = "notify_mqtt";
|
||||
#[allow(dead_code)]
|
||||
pub const NOTIFY_MY_SQL_SUB_SYS: &str = "notify_mysql";
|
||||
pub const NOTIFY_MYSQL_SUB_SYS: &str = "notify_mysql";
|
||||
#[allow(dead_code)]
|
||||
pub const NOTIFY_NATS_SUB_SYS: &str = "notify_nats";
|
||||
#[allow(dead_code)]
|
||||
pub const NOTIFY_NSQ_SUB_SYS: &str = "notify_nsq";
|
||||
#[allow(dead_code)]
|
||||
pub const NOTIFY_ES_SUB_SYS: &str = "notify_elasticsearch";
|
||||
#[allow(dead_code)]
|
||||
pub const NOTIFY_AMQP_SUB_SYS: &str = "notify_amqp";
|
||||
#[allow(dead_code)]
|
||||
pub const NOTIFY_POSTGRES_SUB_SYS: &str = "notify_postgres";
|
||||
#[allow(dead_code)]
|
||||
pub const NOTIFY_REDIS_SUB_SYS: &str = "notify_redis";
|
||||
pub const NOTIFY_REDIS_DEFAULT_CHANNEL: &str = "rustfs_notify_channel";
|
||||
pub const NOTIFY_PULSAR_SUB_SYS: &str = "notify_pulsar";
|
||||
pub const NOTIFY_WEBHOOK_SUB_SYS: &str = "notify_webhook";
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
/// A list of all valid configuration keys for a MySQL target.
|
||||
pub const NOTIFY_MYSQL_KEYS: &[&str] = &[
|
||||
crate::ENABLE_KEY,
|
||||
crate::MYSQL_DSN_STRING,
|
||||
crate::MYSQL_TABLE,
|
||||
crate::MYSQL_FORMAT,
|
||||
crate::MYSQL_TLS_CA,
|
||||
crate::MYSQL_TLS_CLIENT_CERT,
|
||||
crate::MYSQL_TLS_CLIENT_KEY,
|
||||
crate::MYSQL_QUEUE_DIR,
|
||||
crate::MYSQL_QUEUE_LIMIT,
|
||||
crate::MYSQL_MAX_OPEN_CONNECTIONS,
|
||||
crate::COMMENT_KEY,
|
||||
];
|
||||
|
||||
// MySQL Environment Variables
|
||||
pub const ENV_NOTIFY_MYSQL_ENABLE: &str = "RUSTFS_NOTIFY_MYSQL_ENABLE";
|
||||
pub const ENV_NOTIFY_MYSQL_DSN_STRING: &str = "RUSTFS_NOTIFY_MYSQL_DSN_STRING";
|
||||
pub const ENV_NOTIFY_MYSQL_TABLE: &str = "RUSTFS_NOTIFY_MYSQL_TABLE";
|
||||
pub const ENV_NOTIFY_MYSQL_FORMAT: &str = "RUSTFS_NOTIFY_MYSQL_FORMAT";
|
||||
pub const ENV_NOTIFY_MYSQL_TLS_CA: &str = "RUSTFS_NOTIFY_MYSQL_TLS_CA";
|
||||
pub const ENV_NOTIFY_MYSQL_TLS_CLIENT_CERT: &str = "RUSTFS_NOTIFY_MYSQL_TLS_CLIENT_CERT";
|
||||
pub const ENV_NOTIFY_MYSQL_TLS_CLIENT_KEY: &str = "RUSTFS_NOTIFY_MYSQL_TLS_CLIENT_KEY";
|
||||
pub const ENV_NOTIFY_MYSQL_QUEUE_DIR: &str = "RUSTFS_NOTIFY_MYSQL_QUEUE_DIR";
|
||||
pub const ENV_NOTIFY_MYSQL_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_MYSQL_QUEUE_LIMIT";
|
||||
pub const ENV_NOTIFY_MYSQL_MAX_OPEN_CONNECTIONS: &str = "RUSTFS_NOTIFY_MYSQL_MAX_OPEN_CONNECTIONS";
|
||||
|
||||
pub const ENV_NOTIFY_MYSQL_KEYS: &[&str; 10] = &[
|
||||
ENV_NOTIFY_MYSQL_ENABLE,
|
||||
ENV_NOTIFY_MYSQL_DSN_STRING,
|
||||
ENV_NOTIFY_MYSQL_TABLE,
|
||||
ENV_NOTIFY_MYSQL_FORMAT,
|
||||
ENV_NOTIFY_MYSQL_TLS_CA,
|
||||
ENV_NOTIFY_MYSQL_TLS_CLIENT_CERT,
|
||||
ENV_NOTIFY_MYSQL_TLS_CLIENT_KEY,
|
||||
ENV_NOTIFY_MYSQL_QUEUE_DIR,
|
||||
ENV_NOTIFY_MYSQL_QUEUE_LIMIT,
|
||||
ENV_NOTIFY_MYSQL_MAX_OPEN_CONNECTIONS,
|
||||
];
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
pub const NOTIFY_POSTGRES_KEYS: &[&str] = &[
|
||||
crate::ENABLE_KEY,
|
||||
crate::POSTGRES_DSN_STRING,
|
||||
crate::POSTGRES_TABLE,
|
||||
crate::POSTGRES_FORMAT,
|
||||
crate::POSTGRES_TLS_REQUIRED,
|
||||
crate::POSTGRES_TLS_CA,
|
||||
crate::POSTGRES_TLS_CLIENT_CERT,
|
||||
crate::POSTGRES_TLS_CLIENT_KEY,
|
||||
crate::POSTGRES_QUEUE_DIR,
|
||||
crate::POSTGRES_QUEUE_LIMIT,
|
||||
crate::COMMENT_KEY,
|
||||
];
|
||||
|
||||
pub const ENV_NOTIFY_POSTGRES_ENABLE: &str = "RUSTFS_NOTIFY_POSTGRES_ENABLE";
|
||||
pub const ENV_NOTIFY_POSTGRES_DSN_STRING: &str = "RUSTFS_NOTIFY_POSTGRES_DSN_STRING";
|
||||
pub const ENV_NOTIFY_POSTGRES_TABLE: &str = "RUSTFS_NOTIFY_POSTGRES_TABLE";
|
||||
pub const ENV_NOTIFY_POSTGRES_FORMAT: &str = "RUSTFS_NOTIFY_POSTGRES_FORMAT";
|
||||
pub const ENV_NOTIFY_POSTGRES_TLS_REQUIRED: &str = "RUSTFS_NOTIFY_POSTGRES_TLS_REQUIRED";
|
||||
pub const ENV_NOTIFY_POSTGRES_TLS_CA: &str = "RUSTFS_NOTIFY_POSTGRES_TLS_CA";
|
||||
pub const ENV_NOTIFY_POSTGRES_TLS_CLIENT_CERT: &str = "RUSTFS_NOTIFY_POSTGRES_TLS_CLIENT_CERT";
|
||||
pub const ENV_NOTIFY_POSTGRES_TLS_CLIENT_KEY: &str = "RUSTFS_NOTIFY_POSTGRES_TLS_CLIENT_KEY";
|
||||
pub const ENV_NOTIFY_POSTGRES_QUEUE_DIR: &str = "RUSTFS_NOTIFY_POSTGRES_QUEUE_DIR";
|
||||
pub const ENV_NOTIFY_POSTGRES_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_POSTGRES_QUEUE_LIMIT";
|
||||
|
||||
pub const ENV_NOTIFY_POSTGRES_KEYS: &[&str; 10] = &[
|
||||
ENV_NOTIFY_POSTGRES_ENABLE,
|
||||
ENV_NOTIFY_POSTGRES_DSN_STRING,
|
||||
ENV_NOTIFY_POSTGRES_TABLE,
|
||||
ENV_NOTIFY_POSTGRES_FORMAT,
|
||||
ENV_NOTIFY_POSTGRES_TLS_REQUIRED,
|
||||
ENV_NOTIFY_POSTGRES_TLS_CA,
|
||||
ENV_NOTIFY_POSTGRES_TLS_CLIENT_CERT,
|
||||
ENV_NOTIFY_POSTGRES_TLS_CLIENT_KEY,
|
||||
ENV_NOTIFY_POSTGRES_QUEUE_DIR,
|
||||
ENV_NOTIFY_POSTGRES_QUEUE_LIMIT,
|
||||
];
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
/// A list of all valid configuration keys for a Redis target.
|
||||
pub const NOTIFY_REDIS_KEYS: &[&str] = &[
|
||||
crate::ENABLE_KEY,
|
||||
crate::REDIS_URL,
|
||||
crate::REDIS_CHANNEL,
|
||||
crate::REDIS_USERNAME,
|
||||
crate::REDIS_PASSWORD,
|
||||
crate::REDIS_KEEP_ALIVE_INTERVAL,
|
||||
crate::REDIS_QUEUE_DIR,
|
||||
crate::REDIS_QUEUE_LIMIT,
|
||||
crate::REDIS_MAX_RETRY_ATTEMPTS,
|
||||
crate::REDIS_RECONNECT_RETRY_ATTEMPTS,
|
||||
crate::REDIS_MIN_RETRY_DELAY,
|
||||
crate::REDIS_MAX_RETRY_DELAY,
|
||||
crate::REDIS_CONNECTION_TIMEOUT,
|
||||
crate::REDIS_RESPONSE_TIMEOUT,
|
||||
crate::REDIS_PIPELINE_BUFFER_SIZE,
|
||||
crate::REDIS_TLS_POLICY,
|
||||
crate::REDIS_TLS_CA,
|
||||
crate::REDIS_TLS_CLIENT_CERT,
|
||||
crate::REDIS_TLS_CLIENT_KEY,
|
||||
crate::REDIS_TLS_ALLOW_INSECURE,
|
||||
crate::COMMENT_KEY,
|
||||
];
|
||||
|
||||
pub const ENV_NOTIFY_REDIS_ENABLE: &str = "RUSTFS_NOTIFY_REDIS_ENABLE";
|
||||
pub const ENV_NOTIFY_REDIS_URL: &str = "RUSTFS_NOTIFY_REDIS_URL";
|
||||
pub const ENV_NOTIFY_REDIS_CHANNEL: &str = "RUSTFS_NOTIFY_REDIS_CHANNEL";
|
||||
pub const ENV_NOTIFY_REDIS_USERNAME: &str = "RUSTFS_NOTIFY_REDIS_USERNAME";
|
||||
pub const ENV_NOTIFY_REDIS_PASSWORD: &str = "RUSTFS_NOTIFY_REDIS_PASSWORD";
|
||||
pub const ENV_NOTIFY_REDIS_KEEP_ALIVE_INTERVAL: &str = "RUSTFS_NOTIFY_REDIS_KEEP_ALIVE_INTERVAL";
|
||||
pub const ENV_NOTIFY_REDIS_QUEUE_DIR: &str = "RUSTFS_NOTIFY_REDIS_QUEUE_DIR";
|
||||
pub const ENV_NOTIFY_REDIS_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_REDIS_QUEUE_LIMIT";
|
||||
pub const ENV_NOTIFY_REDIS_MAX_RETRY_ATTEMPTS: &str = "RUSTFS_NOTIFY_REDIS_MAX_RETRY_ATTEMPTS";
|
||||
pub const ENV_NOTIFY_REDIS_RECONNECT_RETRY_ATTEMPTS: &str = "RUSTFS_NOTIFY_REDIS_RECONNECT_RETRY_ATTEMPTS";
|
||||
pub const ENV_NOTIFY_REDIS_MIN_RETRY_DELAY: &str = "RUSTFS_NOTIFY_REDIS_MIN_RETRY_DELAY";
|
||||
pub const ENV_NOTIFY_REDIS_MAX_RETRY_DELAY: &str = "RUSTFS_NOTIFY_REDIS_MAX_RETRY_DELAY";
|
||||
pub const ENV_NOTIFY_REDIS_CONNECTION_TIMEOUT: &str = "RUSTFS_NOTIFY_REDIS_CONNECTION_TIMEOUT";
|
||||
pub const ENV_NOTIFY_REDIS_RESPONSE_TIMEOUT: &str = "RUSTFS_NOTIFY_REDIS_RESPONSE_TIMEOUT";
|
||||
pub const ENV_NOTIFY_REDIS_PIPELINE_BUFFER_SIZE: &str = "RUSTFS_NOTIFY_REDIS_PIPELINE_BUFFER_SIZE";
|
||||
pub const ENV_NOTIFY_REDIS_TLS_POLICY: &str = "RUSTFS_NOTIFY_REDIS_TLS_POLICY";
|
||||
pub const ENV_NOTIFY_REDIS_TLS_CA: &str = "RUSTFS_NOTIFY_REDIS_TLS_CA";
|
||||
pub const ENV_NOTIFY_REDIS_TLS_CLIENT_CERT: &str = "RUSTFS_NOTIFY_REDIS_TLS_CLIENT_CERT";
|
||||
pub const ENV_NOTIFY_REDIS_TLS_CLIENT_KEY: &str = "RUSTFS_NOTIFY_REDIS_TLS_CLIENT_KEY";
|
||||
pub const ENV_NOTIFY_REDIS_TLS_ALLOW_INSECURE: &str = "RUSTFS_NOTIFY_REDIS_TLS_ALLOW_INSECURE";
|
||||
|
||||
pub const ENV_NOTIFY_REDIS_KEYS: &[&str; 20] = &[
|
||||
ENV_NOTIFY_REDIS_ENABLE,
|
||||
ENV_NOTIFY_REDIS_URL,
|
||||
ENV_NOTIFY_REDIS_CHANNEL,
|
||||
ENV_NOTIFY_REDIS_USERNAME,
|
||||
ENV_NOTIFY_REDIS_PASSWORD,
|
||||
ENV_NOTIFY_REDIS_KEEP_ALIVE_INTERVAL,
|
||||
ENV_NOTIFY_REDIS_QUEUE_DIR,
|
||||
ENV_NOTIFY_REDIS_QUEUE_LIMIT,
|
||||
ENV_NOTIFY_REDIS_MAX_RETRY_ATTEMPTS,
|
||||
ENV_NOTIFY_REDIS_RECONNECT_RETRY_ATTEMPTS,
|
||||
ENV_NOTIFY_REDIS_MIN_RETRY_DELAY,
|
||||
ENV_NOTIFY_REDIS_MAX_RETRY_DELAY,
|
||||
ENV_NOTIFY_REDIS_CONNECTION_TIMEOUT,
|
||||
ENV_NOTIFY_REDIS_RESPONSE_TIMEOUT,
|
||||
ENV_NOTIFY_REDIS_PIPELINE_BUFFER_SIZE,
|
||||
ENV_NOTIFY_REDIS_TLS_POLICY,
|
||||
ENV_NOTIFY_REDIS_TLS_CA,
|
||||
ENV_NOTIFY_REDIS_TLS_CLIENT_CERT,
|
||||
ENV_NOTIFY_REDIS_TLS_CLIENT_KEY,
|
||||
ENV_NOTIFY_REDIS_TLS_ALLOW_INSECURE,
|
||||
];
|
||||
@@ -1,3 +1,17 @@
|
||||
# Copyright 2024 RustFS Team
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# 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.
|
||||
|
||||
[package]
|
||||
name = "rustfs-credentials"
|
||||
edition.workspace = true
|
||||
@@ -12,9 +26,11 @@ categories = ["web-programming", "development-tools", "data-structures", "securi
|
||||
|
||||
[dependencies]
|
||||
base64-simd = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json.workspace = true
|
||||
sha2 = { workspace = true }
|
||||
time = { workspace = true, features = ["serde", "parsing", "formatting", "macros"] }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -12,10 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::{DEFAULT_SECRET_KEY, ENV_RPC_SECRET, IAM_POLICY_CLAIM_NAME_SA, INHERITED_POLICY_TYPE};
|
||||
use crate::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY, ENV_RPC_SECRET, IAM_POLICY_CLAIM_NAME_SA, INHERITED_POLICY_TYPE};
|
||||
use base64_simd::URL_SAFE_NO_PAD;
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use rand::{Rng, RngExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use sha2::Sha256;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
@@ -33,7 +36,12 @@ pub static GLOBAL_RUSTFS_RPC_SECRET: OnceLock<String> = OnceLock::new();
|
||||
pub const RPC_SECRET_REQUIRED_MESSAGE: &str = "RPC authentication secret is not configured";
|
||||
|
||||
/// Operator-facing guidance for configuring RPC authentication safely.
|
||||
pub const RPC_SECRET_REQUIRED_OPERATOR_MESSAGE: &str = "RUSTFS_RPC_SECRET must be set to a non-default value or RUSTFS_SECRET_KEY must be changed from the default for RPC authentication";
|
||||
pub const RPC_SECRET_REQUIRED_OPERATOR_MESSAGE: &str =
|
||||
"RUSTFS_RPC_SECRET can be set explicitly; otherwise the RPC secret is derived from the active access/secret key pair";
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
const RPC_SECRET_DERIVATION_CONTEXT: &[u8] = b"rustfs-rpc-secret:v1";
|
||||
|
||||
/// Error type for credentials operations
|
||||
#[derive(Debug)]
|
||||
@@ -217,37 +225,59 @@ pub fn gen_secret_key(length: usize) -> std::io::Result<String> {
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
/// Get the RPC authentication token from environment variable
|
||||
/// Get the RPC authentication token from the environment or derive it from the active credentials.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `String` - The RPC authentication token
|
||||
///
|
||||
fn resolve_rpc_secret(env_secret: Option<&str>, global_secret: Option<&str>) -> Option<String> {
|
||||
if let Some(secret) = env_secret.map(str::trim).filter(|secret| !secret.is_empty()) {
|
||||
return (secret != DEFAULT_SECRET_KEY).then(|| secret.to_string());
|
||||
fn normalize_rpc_secret(secret: &str) -> Option<String> {
|
||||
let secret = secret.trim();
|
||||
(!secret.is_empty() && secret != DEFAULT_SECRET_KEY && secret != DEFAULT_ACCESS_KEY).then(|| secret.to_string())
|
||||
}
|
||||
|
||||
fn derive_rpc_secret(access_key: &str, secret_key: &str) -> Option<String> {
|
||||
let access_key = access_key.trim();
|
||||
let secret_key = secret_key.trim();
|
||||
|
||||
if access_key.is_empty() || secret_key.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
global_secret
|
||||
.map(str::trim)
|
||||
.filter(|secret| !secret.is_empty() && *secret != DEFAULT_SECRET_KEY)
|
||||
.map(ToOwned::to_owned)
|
||||
let mut mac = <HmacSha256 as KeyInit>::new_from_slice(secret_key.as_bytes()).expect("HMAC can take key of any size");
|
||||
mac.update(RPC_SECRET_DERIVATION_CONTEXT);
|
||||
mac.update(&[0]);
|
||||
mac.update(access_key.as_bytes());
|
||||
|
||||
Some(URL_SAFE_NO_PAD.encode_to_string(mac.finalize().into_bytes()))
|
||||
}
|
||||
|
||||
fn resolve_rpc_secret(env_secret: Option<&str>, global_access: Option<&str>, global_secret: Option<&str>) -> Option<String> {
|
||||
if let Some(secret) = env_secret.map(str::trim).filter(|secret| !secret.is_empty()) {
|
||||
return normalize_rpc_secret(secret);
|
||||
}
|
||||
|
||||
match (global_access, global_secret) {
|
||||
(Some(access_key), Some(secret_key)) => derive_rpc_secret(access_key, secret_key),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_rpc_token() -> std::io::Result<String> {
|
||||
if let Some(secret) = GLOBAL_RUSTFS_RPC_SECRET.get() {
|
||||
return resolve_rpc_secret(None, Some(secret)).ok_or_else(|| Error::other(RPC_SECRET_REQUIRED_MESSAGE));
|
||||
return normalize_rpc_secret(secret).ok_or_else(|| Error::other(RPC_SECRET_REQUIRED_MESSAGE));
|
||||
}
|
||||
|
||||
let env_secret = env::var(ENV_RPC_SECRET).ok();
|
||||
let global_access = get_global_access_key_opt();
|
||||
let global_secret = get_global_secret_key_opt();
|
||||
let secret = resolve_rpc_secret(env_secret.as_deref(), global_secret.as_deref())
|
||||
let secret = resolve_rpc_secret(env_secret.as_deref(), global_access.as_deref(), global_secret.as_deref())
|
||||
.ok_or_else(|| Error::other(RPC_SECRET_REQUIRED_MESSAGE))?;
|
||||
|
||||
match GLOBAL_RUSTFS_RPC_SECRET.set(secret.clone()) {
|
||||
Ok(()) => Ok(secret),
|
||||
Err(_) => GLOBAL_RUSTFS_RPC_SECRET
|
||||
.get()
|
||||
.and_then(|stored| resolve_rpc_secret(None, Some(stored)))
|
||||
.and_then(|stored| normalize_rpc_secret(stored))
|
||||
.ok_or_else(|| Error::other(RPC_SECRET_REQUIRED_MESSAGE)),
|
||||
}
|
||||
}
|
||||
@@ -407,7 +437,7 @@ impl Credentials {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{IAM_POLICY_CLAIM_NAME_SA, INHERITED_POLICY_TYPE};
|
||||
use crate::{DEFAULT_ACCESS_KEY, IAM_POLICY_CLAIM_NAME_SA, INHERITED_POLICY_TYPE};
|
||||
use time::Duration;
|
||||
|
||||
#[test]
|
||||
@@ -510,11 +540,12 @@ mod tests {
|
||||
// If it hasn't already been initialized, the test automatically generates logic
|
||||
if get_global_action_cred().is_none() {
|
||||
init_global_action_credentials(None, None).ok();
|
||||
let ak = get_global_access_key();
|
||||
let sk = get_global_secret_key();
|
||||
assert_eq!(ak.len(), 20);
|
||||
assert_eq!(sk.len(), 32);
|
||||
}
|
||||
|
||||
let ak = get_global_access_key();
|
||||
let sk = get_global_secret_key();
|
||||
assert!(ak.len() >= 3);
|
||||
assert!(sk.len() >= 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -528,10 +559,26 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_rpc_secret_rejects_default_fallback() {
|
||||
assert!(resolve_rpc_secret(None, None).is_none());
|
||||
assert!(resolve_rpc_secret(None, Some(DEFAULT_SECRET_KEY)).is_none());
|
||||
assert!(resolve_rpc_secret(Some(DEFAULT_SECRET_KEY), Some("custom-global-secret")).is_none());
|
||||
fn test_gen_access_key_length_and_charset() {
|
||||
let err = gen_access_key(2).expect_err("length below 3 should fail");
|
||||
assert_eq!(err.to_string(), "access key length is too short");
|
||||
|
||||
let key = gen_access_key(20).expect("access key should generate");
|
||||
assert_eq!(key.len(), 20);
|
||||
assert!(key.chars().all(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_rpc_secret_allows_default_credentials_for_derivation() {
|
||||
assert!(resolve_rpc_secret(None, None, None).is_none());
|
||||
|
||||
let expected = derive_rpc_secret(DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY).expect("secret should derive");
|
||||
assert_eq!(
|
||||
resolve_rpc_secret(None, Some(DEFAULT_ACCESS_KEY), Some(DEFAULT_SECRET_KEY)).as_deref(),
|
||||
Some(expected.as_str())
|
||||
);
|
||||
|
||||
assert!(resolve_rpc_secret(Some(DEFAULT_SECRET_KEY), Some("custom-access"), Some("custom-global-secret")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -551,37 +598,64 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_resolve_rpc_secret_accepts_non_default_secret() {
|
||||
assert_eq!(resolve_rpc_secret(Some("custom-rpc-secret"), None).as_deref(), Some("custom-rpc-secret"));
|
||||
assert_eq!(
|
||||
resolve_rpc_secret(None, Some("custom-global-secret")).as_deref(),
|
||||
Some("custom-global-secret")
|
||||
resolve_rpc_secret(Some("custom-rpc-secret"), None, None).as_deref(),
|
||||
Some("custom-rpc-secret")
|
||||
);
|
||||
let expected = derive_rpc_secret("custom-access", "custom-global-secret").expect("secret should derive");
|
||||
assert_eq!(
|
||||
resolve_rpc_secret(None, Some("custom-access"), Some("custom-global-secret")).as_deref(),
|
||||
Some(expected.as_str())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_rpc_secret_trims_and_falls_back_from_blank_env() {
|
||||
let expected = derive_rpc_secret("custom-access", "custom-global-secret").expect("secret should derive");
|
||||
assert_eq!(
|
||||
resolve_rpc_secret(Some(" custom-rpc-secret "), None).as_deref(),
|
||||
resolve_rpc_secret(Some(" custom-rpc-secret "), None, None).as_deref(),
|
||||
Some("custom-rpc-secret")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_rpc_secret(Some(""), Some("custom-global-secret")).as_deref(),
|
||||
Some("custom-global-secret")
|
||||
resolve_rpc_secret(Some(""), Some("custom-access"), Some("custom-global-secret")).as_deref(),
|
||||
Some(expected.as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_rpc_secret(Some(" "), Some(" custom-global-secret ")).as_deref(),
|
||||
Some("custom-global-secret")
|
||||
resolve_rpc_secret(Some(" "), Some(" custom-access "), Some(" custom-global-secret ")).as_deref(),
|
||||
Some(expected.as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_rpc_secret(Some(" "), Some("custom-global-secret")).as_deref(),
|
||||
Some("custom-global-secret")
|
||||
resolve_rpc_secret(Some(" "), Some("custom-access"), Some("custom-global-secret")).as_deref(),
|
||||
Some(expected.as_str())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_rpc_secret_returns_none_for_trimmed_default_secret() {
|
||||
let padded_default_secret = format!(" {} ", DEFAULT_SECRET_KEY);
|
||||
assert!(resolve_rpc_secret(Some(padded_default_secret.as_str()), Some("custom-global-secret")).is_none());
|
||||
assert!(
|
||||
resolve_rpc_secret(Some(padded_default_secret.as_str()), Some("custom-access"), Some("custom-global-secret"))
|
||||
.is_none()
|
||||
);
|
||||
|
||||
let padded_default_access = format!(" {} ", DEFAULT_ACCESS_KEY);
|
||||
assert!(
|
||||
resolve_rpc_secret(Some(padded_default_access.as_str()), Some("custom-access"), Some("custom-global-secret"))
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_derive_rpc_secret_is_stable_and_not_plaintext() {
|
||||
let first = derive_rpc_secret("custom-access", "custom-secret").expect("secret should derive");
|
||||
let second = derive_rpc_secret("custom-access", "custom-secret").expect("secret should derive");
|
||||
|
||||
assert_eq!(first, second);
|
||||
assert_ne!(first, "custom-access");
|
||||
assert_ne!(first, "custom-secret");
|
||||
assert_ne!(first, format!("{}{}", "custom-access", "custom-secret"));
|
||||
assert!(!first.contains("custom-access"));
|
||||
assert!(!first.contains("custom-secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -31,11 +31,13 @@ workspace = true
|
||||
[dependencies]
|
||||
aes-gcm = { workspace = true, optional = true }
|
||||
argon2 = { workspace = true, optional = true }
|
||||
cfg-if = { workspace = true }
|
||||
chacha20poly1305 = { workspace = true, optional = true }
|
||||
jsonwebtoken = { workspace = true }
|
||||
base64-simd = { workspace = true }
|
||||
pbkdf2 = { workspace = true, optional = true }
|
||||
rand = { workspace = true, optional = true }
|
||||
rsa = { workspace = true, features = ["sha2"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha2 = { workspace = true, optional = true }
|
||||
thiserror.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -13,19 +13,23 @@
|
||||
// limitations under the License.
|
||||
|
||||
pub fn native_aes() -> bool {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] {
|
||||
cfg_select! {
|
||||
any(target_arch = "x86", target_arch = "x86_64") => {
|
||||
std::is_x86_feature_detected!("aes") && std::is_x86_feature_detected!("pclmulqdq")
|
||||
} else if #[cfg(target_arch = "aarch64")] {
|
||||
}
|
||||
target_arch = "aarch64" => {
|
||||
std::arch::is_aarch64_feature_detected!("aes")
|
||||
} else if #[cfg(target_arch = "powerpc64")] {
|
||||
}
|
||||
target_arch = "powerpc64" => {
|
||||
false
|
||||
} else if #[cfg(target_arch = "s390x")] {
|
||||
}
|
||||
target_arch = "s390x" => {
|
||||
std::is_s390x_feature_detected!("aes")
|
||||
&& std::is_s390x_feature_detected!("aescbc")
|
||||
&& std::is_s390x_feature_detected!("aesctr")
|
||||
&& (std::is_s390x_feature_detected!("aesgcm") || std::is_s390x_feature_detected!("ghash"))
|
||||
} else {
|
||||
}
|
||||
_ => {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
mod encdec;
|
||||
mod error;
|
||||
mod jwt;
|
||||
pub mod license_token;
|
||||
|
||||
pub use encdec::decrypt::decrypt_data;
|
||||
pub use encdec::encrypt::encrypt_data;
|
||||
@@ -25,3 +26,4 @@ pub use encdec::stream_io::{decrypt_stream_io, encrypt_stream_io};
|
||||
pub use error::Error;
|
||||
pub use jwt::decode::decode as jwt_decode;
|
||||
pub use jwt::encode::encode as jwt_encode;
|
||||
pub use license_token::{Token, parse_license_with_public_key, parse_signed_license_token, sign_license_token};
|
||||
|
||||
@@ -115,12 +115,20 @@ mod tests {
|
||||
let private_key = RsaPrivateKey::new(&mut rng, bits).expect("Failed to generate private key");
|
||||
let public_key = RsaPublicKey::from(&private_key);
|
||||
|
||||
let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF).unwrap();
|
||||
let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).unwrap();
|
||||
let private_key_pem = private_key
|
||||
.to_pkcs8_pem(LineEnding::LF)
|
||||
.expect("failed to encode private key pem");
|
||||
let public_key_pem = public_key
|
||||
.to_public_key_pem(LineEnding::LF)
|
||||
.expect("failed to encode public key pem");
|
||||
|
||||
let token = Token {
|
||||
name: "test_app".to_string(),
|
||||
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now
|
||||
expired: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system clock before unix epoch")
|
||||
.as_secs()
|
||||
+ 3600, // 1 hour from now
|
||||
};
|
||||
|
||||
let encoded = sign_license_token(&token, &private_key_pem).expect("Failed to encode token");
|
||||
@@ -139,12 +147,20 @@ mod tests {
|
||||
let private_key = RsaPrivateKey::new(&mut rng, bits).expect("Failed to generate private key");
|
||||
let public_key = RsaPublicKey::from(&private_key);
|
||||
|
||||
let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF).unwrap();
|
||||
let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).unwrap();
|
||||
let private_key_pem = private_key
|
||||
.to_pkcs8_pem(LineEnding::LF)
|
||||
.expect("failed to encode private key pem");
|
||||
let public_key_pem = public_key
|
||||
.to_public_key_pem(LineEnding::LF)
|
||||
.expect("failed to encode public key pem");
|
||||
|
||||
let token = Token {
|
||||
name: "test_app".to_string(),
|
||||
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600,
|
||||
expired: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system clock before unix epoch")
|
||||
.as_secs()
|
||||
+ 3600,
|
||||
};
|
||||
|
||||
let encoded = gencode(&token, &public_key_pem).expect("Failed to encode token");
|
||||
@@ -159,11 +175,19 @@ mod tests {
|
||||
let mut rng = rand::rng();
|
||||
let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("Failed to generate private key");
|
||||
let public_key = RsaPublicKey::from(&private_key);
|
||||
let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF).unwrap();
|
||||
let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).unwrap();
|
||||
let private_key_pem = private_key
|
||||
.to_pkcs8_pem(LineEnding::LF)
|
||||
.expect("failed to encode private key pem");
|
||||
let public_key_pem = public_key
|
||||
.to_public_key_pem(LineEnding::LF)
|
||||
.expect("failed to encode public key pem");
|
||||
let token = Token {
|
||||
name: "test_app".to_string(),
|
||||
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600,
|
||||
expired: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system clock before unix epoch")
|
||||
.as_secs()
|
||||
+ 3600,
|
||||
};
|
||||
|
||||
let encoded = sign_license_token(&token, &private_key_pem).expect("Failed to encode token");
|
||||
@@ -181,7 +205,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_source_does_not_embed_private_key() {
|
||||
let source = include_str!("token.rs");
|
||||
let source = include_str!("license_token.rs");
|
||||
let forbidden = ["BEGIN", "PRIVATE KEY"].join(" ");
|
||||
|
||||
assert!(!source.contains(&forbidden));
|
||||
@@ -192,7 +216,9 @@ mod tests {
|
||||
let mut rng = rand::rng();
|
||||
let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("Failed to generate private key");
|
||||
let public_key = RsaPublicKey::from(&private_key);
|
||||
let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).unwrap();
|
||||
let public_key_pem = public_key
|
||||
.to_public_key_pem(LineEnding::LF)
|
||||
.expect("failed to encode public key pem");
|
||||
|
||||
let invalid_token = "invalid_base64_token";
|
||||
let result = parse_signed_license_token(invalid_token, &public_key_pem);
|
||||
@@ -204,7 +230,11 @@ mod tests {
|
||||
fn test_sign_license_token_with_invalid_signing_key() {
|
||||
let token = Token {
|
||||
name: "test_app".to_string(),
|
||||
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now
|
||||
expired: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system clock before unix epoch")
|
||||
.as_secs()
|
||||
+ 3600, // 1 hour from now
|
||||
};
|
||||
|
||||
let invalid_key = "invalid_private_key";
|
||||
@@ -13,24 +13,26 @@
|
||||
# limitations under the License.
|
||||
|
||||
[package]
|
||||
name = "rustfs-workers"
|
||||
name = "rustfs-data-usage"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
homepage.workspace = true
|
||||
description = "Workers for RustFS, providing background processing capabilities for tasks such as data synchronization, indexing, and more."
|
||||
keywords = ["workers", "tasks", "rustfs", "Minio"]
|
||||
categories = ["web-programming", "development-tools"]
|
||||
documentation = "https://docs.rs/rustfs-workers/latest/rustfs_workers/"
|
||||
description = "Shared data usage models and algorithms for RustFS"
|
||||
keywords = ["rustfs", "data-usage", "cache", "histogram"]
|
||||
categories = ["data-structures", "filesystem"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
tokio = { workspace = true, features = ["sync", "time", "macros"] }
|
||||
tracing.workspace = true
|
||||
serde = { workspace = true }
|
||||
path-clean = { workspace = true }
|
||||
rmp-serde = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
rustfs-filemeta = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
@@ -312,6 +312,12 @@ impl SizeHistogram {
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub fn merge_from(&mut self, other: &Self) {
|
||||
for (dst, src) in self.0.iter_mut().zip(other.0.iter()) {
|
||||
*dst += src;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Versions histogram for version count distribution
|
||||
@@ -361,6 +367,12 @@ impl VersionsHistogram {
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub fn merge_from(&mut self, other: &Self) {
|
||||
for (dst, src) in self.0.iter_mut().zip(other.0.iter()) {
|
||||
*dst += src;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replication statistics for a single target
|
||||
@@ -419,6 +431,9 @@ pub struct DataUsageEntry {
|
||||
pub obj_versions: VersionsHistogram,
|
||||
pub replication_stats: Option<ReplicationAllStats>,
|
||||
pub compacted: bool,
|
||||
/// Number of objects that failed to scan (e.g., IO errors)
|
||||
#[serde(default)]
|
||||
pub failed_objects: usize,
|
||||
}
|
||||
|
||||
impl DataUsageEntry {
|
||||
@@ -456,6 +471,7 @@ impl DataUsageEntry {
|
||||
self.versions += other.versions;
|
||||
self.delete_markers += other.delete_markers;
|
||||
self.size += other.size;
|
||||
self.failed_objects += other.failed_objects;
|
||||
|
||||
if let Some(o_rep) = &other.replication_stats {
|
||||
let s_rep = self.replication_stats.get_or_insert_with(ReplicationAllStats::default);
|
||||
@@ -490,9 +506,11 @@ impl DataUsageEntry {
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct DataUsageCacheInfo {
|
||||
pub name: String,
|
||||
pub next_cycle: u32,
|
||||
pub next_cycle: u64,
|
||||
pub last_update: Option<SystemTime>,
|
||||
pub skip_healing: bool,
|
||||
#[serde(default)]
|
||||
pub failed_objects: HashMap<String, u64>,
|
||||
}
|
||||
|
||||
/// Data usage cache
|
||||
@@ -12,4 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub mod workers;
|
||||
pub mod data_usage;
|
||||
|
||||
pub use data_usage::*;
|
||||
@@ -26,10 +26,12 @@ workspace = true
|
||||
[features]
|
||||
default = []
|
||||
ftps = []
|
||||
sftp = []
|
||||
|
||||
[dependencies]
|
||||
rustfs-config = { workspace = true, features = ["constants"] }
|
||||
rustfs-ecstore.workspace = true
|
||||
rustfs-common.workspace = true
|
||||
rustfs-data-usage.workspace = true
|
||||
rustfs-rio.workspace = true
|
||||
flatbuffers.workspace = true
|
||||
futures.workspace = true
|
||||
@@ -72,5 +74,7 @@ suppaftp = { workspace = true, features = ["tokio", "rustls-aws-lc-rs"] }
|
||||
rcgen.workspace = true
|
||||
anyhow.workspace = true
|
||||
rustls.workspace = true
|
||||
russh = { workspace = true }
|
||||
russh-sftp = { workspace = true }
|
||||
zip.workspace = true
|
||||
clap.workspace = true
|
||||
|
||||
@@ -41,7 +41,9 @@ use walkdir::WalkDir;
|
||||
// Common constants for all E2E tests
|
||||
pub const DEFAULT_ACCESS_KEY: &str = "rustfsadmin";
|
||||
pub const DEFAULT_SECRET_KEY: &str = "rustfsadmin";
|
||||
pub const ENV_RUSTFS_BUILD_FEATURES: &str = "RUSTFS_BUILD_FEATURES";
|
||||
pub const TEST_BUCKET: &str = "e2e-test-bucket";
|
||||
const RUSTFS_FULL_FEATURE: &str = "full";
|
||||
|
||||
fn build_test_s3_config(endpoint_url: &str, access_key: &str, secret_key: &str, provider_name: &'static str) -> Config {
|
||||
let credentials = Credentials::new(access_key, secret_key, None, None, provider_name);
|
||||
@@ -83,6 +85,7 @@ pub fn rustfs_binary_path_with_features(requested_features: Option<&str>) -> Pat
|
||||
if let Some(path) = std::env::var_os("CARGO_BIN_EXE_rustfs") {
|
||||
return PathBuf::from(path);
|
||||
}
|
||||
let requested_features = requested_features.and_then(normalize_rustfs_build_features);
|
||||
|
||||
let mut binary_path = workspace_root();
|
||||
binary_path.push("target");
|
||||
@@ -90,7 +93,7 @@ pub fn rustfs_binary_path_with_features(requested_features: Option<&str>) -> Pat
|
||||
binary_path.push(profile_dir);
|
||||
binary_path.push(format!("rustfs{}", std::env::consts::EXE_SUFFIX));
|
||||
|
||||
let features_match = binary_features_match(&binary_path, requested_features);
|
||||
let features_match = binary_features_match(&binary_path, requested_features.as_deref());
|
||||
let source_is_newer = workspace_sources_newer_than_binary(&binary_path);
|
||||
let can_reuse_inside_e2e = running_inside_e2e_test_binary() && requested_features.is_none() && features_match;
|
||||
if binary_path.is_file() && features_match && (!source_is_newer || can_reuse_inside_e2e) {
|
||||
@@ -105,7 +108,7 @@ pub fn rustfs_binary_path_with_features(requested_features: Option<&str>) -> Pat
|
||||
}
|
||||
|
||||
info!("Building RustFS binary to ensure it's up to date...");
|
||||
build_rustfs_binary(requested_features);
|
||||
build_rustfs_binary(requested_features.as_deref());
|
||||
|
||||
info!("Using RustFS binary at {:?}", binary_path);
|
||||
binary_path
|
||||
@@ -134,11 +137,31 @@ fn running_inside_e2e_test_binary() -> bool {
|
||||
std::env::var("CARGO_PKG_NAME").is_ok_and(|value| value == "e2e_test")
|
||||
}
|
||||
|
||||
fn requested_rustfs_build_features() -> Option<String> {
|
||||
std::env::var("RUSTFS_BUILD_FEATURES")
|
||||
pub fn requested_rustfs_build_features() -> Option<String> {
|
||||
std::env::var(ENV_RUSTFS_BUILD_FEATURES)
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.and_then(|value| normalize_rustfs_build_features(&value))
|
||||
}
|
||||
|
||||
pub fn normalize_rustfs_build_features(features: &str) -> Option<String> {
|
||||
let features = features
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|feature| !feature.is_empty())
|
||||
.map(str::to_ascii_lowercase)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if features.is_empty() { None } else { Some(features.join(",")) }
|
||||
}
|
||||
|
||||
pub fn rustfs_build_feature_enabled(requested_features: Option<&str>, required_feature: &str) -> bool {
|
||||
let Some(requested_features) = requested_features.and_then(normalize_rustfs_build_features) else {
|
||||
return true;
|
||||
};
|
||||
|
||||
requested_features
|
||||
.split(',')
|
||||
.any(|feature| feature.eq_ignore_ascii_case(RUSTFS_FULL_FEATURE) || feature.eq_ignore_ascii_case(required_feature))
|
||||
}
|
||||
|
||||
fn rustfs_binary_features_stamp_path(binary_path: &Path) -> PathBuf {
|
||||
@@ -147,11 +170,14 @@ fn rustfs_binary_features_stamp_path(binary_path: &Path) -> PathBuf {
|
||||
|
||||
fn binary_features_match(binary_path: &Path, requested_features: Option<&str>) -> bool {
|
||||
let stamp_path = rustfs_binary_features_stamp_path(binary_path);
|
||||
let recorded = stdfs::read_to_string(stamp_path).ok().map(|value| value.trim().to_string());
|
||||
let recorded = stdfs::read_to_string(stamp_path)
|
||||
.ok()
|
||||
.and_then(|value| normalize_rustfs_build_features(&value));
|
||||
let requested = requested_features.and_then(normalize_rustfs_build_features);
|
||||
|
||||
match requested_features {
|
||||
match requested.as_deref() {
|
||||
Some(features) => recorded.as_deref() == Some(features),
|
||||
None => recorded.as_deref().is_none_or(str::is_empty),
|
||||
None => recorded.is_none(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +377,7 @@ impl RustFSTestEnvironment {
|
||||
async fn start_rustfs_server_inner(
|
||||
&mut self,
|
||||
extra_args: Vec<&str>,
|
||||
extra_env: &[(&str, &str)],
|
||||
cleanup_existing: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if cleanup_existing {
|
||||
@@ -362,10 +389,12 @@ impl RustFSTestEnvironment {
|
||||
info!("Starting RustFS server with args: {:?}", args);
|
||||
|
||||
let binary_path = rustfs_binary_path();
|
||||
let process = Command::new(&binary_path)
|
||||
.env("RUST_LOG", "rustfs=info,rustfs_notify=debug")
|
||||
.args(&args)
|
||||
.spawn()?;
|
||||
let mut command = Command::new(&binary_path);
|
||||
command.env("RUST_LOG", "rustfs=info,rustfs_notify=debug");
|
||||
for (key, value) in extra_env {
|
||||
command.env(key, value);
|
||||
}
|
||||
let process = command.args(&args).spawn()?;
|
||||
|
||||
self.process = Some(process);
|
||||
|
||||
@@ -377,7 +406,16 @@ impl RustFSTestEnvironment {
|
||||
|
||||
/// Start RustFS server with basic configuration
|
||||
pub async fn start_rustfs_server(&mut self, extra_args: Vec<&str>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.start_rustfs_server_inner(extra_args, true).await
|
||||
self.start_rustfs_server_inner(extra_args, &[], true).await
|
||||
}
|
||||
|
||||
/// Start RustFS server with extra child-process environment variables.
|
||||
pub async fn start_rustfs_server_with_env(
|
||||
&mut self,
|
||||
extra_args: Vec<&str>,
|
||||
extra_env: &[(&str, &str)],
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.start_rustfs_server_inner(extra_args, extra_env, true).await
|
||||
}
|
||||
|
||||
/// Start RustFS server without cleaning up other running RustFS processes.
|
||||
@@ -388,7 +426,7 @@ impl RustFSTestEnvironment {
|
||||
&mut self,
|
||||
extra_args: Vec<&str>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.start_rustfs_server_inner(extra_args, false).await
|
||||
self.start_rustfs_server_inner(extra_args, &[], false).await
|
||||
}
|
||||
|
||||
/// Wait for RustFS server to be ready.
|
||||
@@ -888,3 +926,36 @@ impl Drop for RustFSTestClusterEnvironment {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn normalizes_rustfs_build_features() {
|
||||
assert_eq!(
|
||||
normalize_rustfs_build_features(" SFTP, ftps ,, WebDAV "),
|
||||
Some("sftp,ftps,webdav".to_string())
|
||||
);
|
||||
assert_eq!(normalize_rustfs_build_features(" , "), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_feature_enables_any_required_feature() {
|
||||
assert!(rustfs_build_feature_enabled(Some("full"), "sftp"));
|
||||
assert!(rustfs_build_feature_enabled(Some("ftps, full"), "webdav"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_feature_stamp_matching_uses_normalized_features() {
|
||||
let binary_path = std::env::temp_dir().join(format!("rustfs-feature-stamp-test-{}", Uuid::new_v4()));
|
||||
let stamp_path = rustfs_binary_features_stamp_path(&binary_path);
|
||||
|
||||
stdfs::write(&stamp_path, " SFTP, ftps ").expect("write feature stamp");
|
||||
assert!(binary_features_match(&binary_path, Some("sftp,ftps")));
|
||||
assert!(binary_features_match(&binary_path, Some(" SFTP, FTPS ")));
|
||||
assert!(!binary_features_match(&binary_path, Some("sftp")));
|
||||
|
||||
stdfs::remove_file(stamp_path).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
use rustfs_common::data_usage::DataUsageInfo;
|
||||
use rustfs_data_usage::DataUsageInfo;
|
||||
use serial_test::serial;
|
||||
|
||||
use crate::common::{RustFSTestEnvironment, TEST_BUCKET, awscurl_get, init_logging};
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
//! Regression coverage for signed `DELETE Object?versionId` requests sent with
|
||||
//! no body and no `Content-Length` header.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::common::{RustFSTestEnvironment, init_logging};
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
use aws_sdk_s3::types::{BucketVersioningStatus, VersioningConfiguration};
|
||||
use http::header::HOST;
|
||||
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
|
||||
use rustfs_signer::sign_v4;
|
||||
use s3s::Body;
|
||||
use serial_test::serial;
|
||||
use std::error::Error;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{Duration, timeout};
|
||||
use tracing::info;
|
||||
|
||||
const RAW_RESPONSE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
fn parse_status(raw_response: &str) -> Option<u16> {
|
||||
raw_response.lines().next()?.split_whitespace().nth(1)?.parse().ok()
|
||||
}
|
||||
|
||||
fn response_body(raw_response: &str) -> &str {
|
||||
raw_response
|
||||
.split_once("\r\n\r\n")
|
||||
.map(|(_, body)| body)
|
||||
.or_else(|| raw_response.split_once("\n\n").map(|(_, body)| body))
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
async fn signed_delete_without_content_length(
|
||||
url: &str,
|
||||
access_key: &str,
|
||||
secret_key: &str,
|
||||
) -> Result<String, Box<dyn Error + Send + Sync>> {
|
||||
let uri = url.parse::<http::Uri>()?;
|
||||
let authority = uri.authority().ok_or("request URL missing authority")?.to_string();
|
||||
let path_and_query = uri.path_and_query().ok_or("request URL missing path")?.as_str().to_string();
|
||||
|
||||
let request = http::Request::builder()
|
||||
.method(http::Method::DELETE)
|
||||
.uri(uri)
|
||||
.header(HOST, authority.clone())
|
||||
.header("x-amz-content-sha256", UNSIGNED_PAYLOAD)
|
||||
.body(Body::empty())?;
|
||||
|
||||
let signed = sign_v4(request, 0, access_key, secret_key, "", "us-east-1");
|
||||
|
||||
let mut raw_request = format!("DELETE {path_and_query} HTTP/1.1\r\nHost: {authority}\r\nConnection: close\r\n");
|
||||
for (name, value) in signed.headers() {
|
||||
if name.as_str().eq_ignore_ascii_case("host") || name.as_str().eq_ignore_ascii_case("content-length") {
|
||||
continue;
|
||||
}
|
||||
raw_request.push_str(name.as_str());
|
||||
raw_request.push_str(": ");
|
||||
raw_request.push_str(value.to_str()?);
|
||||
raw_request.push_str("\r\n");
|
||||
}
|
||||
raw_request.push_str("\r\n");
|
||||
|
||||
assert!(
|
||||
!raw_request.to_ascii_lowercase().contains("\r\ncontent-length:"),
|
||||
"raw regression request must omit Content-Length; request was:\n{raw_request}"
|
||||
);
|
||||
|
||||
let mut stream = TcpStream::connect(&authority).await?;
|
||||
stream.write_all(raw_request.as_bytes()).await?;
|
||||
stream.flush().await?;
|
||||
|
||||
let mut response = Vec::new();
|
||||
timeout(RAW_RESPONSE_TIMEOUT, stream.read_to_end(&mut response))
|
||||
.await
|
||||
.map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out reading raw DELETE response"))??;
|
||||
Ok(String::from_utf8_lossy(&response).into_owned())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_delete_object_version_without_content_length_succeeds() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
init_logging();
|
||||
info!("🧪 TEST: signed DELETE Object?versionId succeeds without Content-Length");
|
||||
|
||||
let mut env = RustFSTestEnvironment::new().await?;
|
||||
env.start_rustfs_server(vec![]).await?;
|
||||
|
||||
let client = env.create_s3_client();
|
||||
let bucket = "delete-version-no-content-length";
|
||||
let key = "versioned-delete-target.txt";
|
||||
|
||||
client.create_bucket().bucket(bucket).send().await?;
|
||||
|
||||
client
|
||||
.put_object()
|
||||
.bucket(bucket)
|
||||
.key(key)
|
||||
.body(ByteStream::from_static(b"delete me after versioning is enabled"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
client
|
||||
.put_bucket_versioning()
|
||||
.bucket(bucket)
|
||||
.versioning_configuration(
|
||||
VersioningConfiguration::builder()
|
||||
.status(BucketVersioningStatus::Enabled)
|
||||
.build(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let listed_versions = client.list_object_versions().bucket(bucket).prefix(key).send().await?;
|
||||
let version_id = listed_versions
|
||||
.versions()
|
||||
.iter()
|
||||
.find(|version| version.key() == Some(key))
|
||||
.and_then(|version| version.version_id())
|
||||
.ok_or("ListObjectVersions did not return the pre-versioning object version")?;
|
||||
|
||||
let url = format!("{}/{}/{}?versionId={}", env.url, bucket, key, urlencoding::encode(version_id));
|
||||
let raw_response = signed_delete_without_content_length(&url, &env.access_key, &env.secret_key).await?;
|
||||
info!("raw DELETE response:\n{}", raw_response);
|
||||
|
||||
assert_eq!(
|
||||
parse_status(&raw_response),
|
||||
Some(204),
|
||||
"DELETE Object?versionId without Content-Length should succeed, got:\n{raw_response}"
|
||||
);
|
||||
assert!(
|
||||
!raw_response.contains("MissingContentLength"),
|
||||
"DELETE Object?versionId without Content-Length regressed to MissingContentLength: {raw_response}"
|
||||
);
|
||||
assert!(
|
||||
response_body(&raw_response).trim().is_empty(),
|
||||
"successful DELETE Object?versionId should not return an error body: {raw_response}"
|
||||
);
|
||||
|
||||
let get_deleted_version = client
|
||||
.get_object()
|
||||
.bucket(bucket)
|
||||
.key(key)
|
||||
.version_id(version_id)
|
||||
.send()
|
||||
.await;
|
||||
assert!(get_deleted_version.is_err(), "explicitly deleted version should no longer be readable");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright 2026 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
use crate::common::{RustFSTestEnvironment, init_logging, local_http_client};
|
||||
use aws_sdk_s3::presigning::PresigningConfig;
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart};
|
||||
use serial_test::serial;
|
||||
use std::time::Duration;
|
||||
use tracing::info;
|
||||
|
||||
const CONSISTENCY_BUCKET: &str = "head-consistency-bucket";
|
||||
const PUT_KEY: &str = "consistency-put-object.txt";
|
||||
const MPU_KEY: &str = "consistency-multipart-object.txt";
|
||||
|
||||
fn list_contains_key(output: &aws_sdk_s3::operation::list_objects_v2::ListObjectsV2Output, key: &str) -> bool {
|
||||
output.contents().iter().any(|obj| obj.key().is_some_and(|k| k == key))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn head_object_consistency_after_write_and_multipart_and_presigned_head()
|
||||
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
init_logging();
|
||||
info!("Starting HeadObject consistency regression test");
|
||||
|
||||
let mut env = RustFSTestEnvironment::new().await?;
|
||||
env.start_rustfs_server(Vec::new()).await?;
|
||||
|
||||
let client = env.create_s3_client();
|
||||
env.create_test_bucket(CONSISTENCY_BUCKET).await?;
|
||||
|
||||
client
|
||||
.put_object()
|
||||
.bucket(CONSISTENCY_BUCKET)
|
||||
.key(PUT_KEY)
|
||||
.body(ByteStream::from_static(b"head-consistency-put-body"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
client.get_object().bucket(CONSISTENCY_BUCKET).key(PUT_KEY).send().await?;
|
||||
let put_list = client
|
||||
.list_objects_v2()
|
||||
.bucket(CONSISTENCY_BUCKET)
|
||||
.prefix(PUT_KEY)
|
||||
.send()
|
||||
.await?;
|
||||
assert!(list_contains_key(&put_list, PUT_KEY), "ListObjectsV2 should include the PutObject key");
|
||||
|
||||
client.head_object().bucket(CONSISTENCY_BUCKET).key(PUT_KEY).send().await?;
|
||||
|
||||
let create = client
|
||||
.create_multipart_upload()
|
||||
.bucket(CONSISTENCY_BUCKET)
|
||||
.key(MPU_KEY)
|
||||
.send()
|
||||
.await?;
|
||||
let upload_id = create.upload_id().ok_or("missing multipart upload id")?.to_string();
|
||||
|
||||
let part1 = client
|
||||
.upload_part()
|
||||
.bucket(CONSISTENCY_BUCKET)
|
||||
.key(MPU_KEY)
|
||||
.upload_id(&upload_id)
|
||||
.part_number(1)
|
||||
.body(ByteStream::from_static(b"head-consistency-multipart-part-1"))
|
||||
.send()
|
||||
.await?;
|
||||
let completed = CompletedMultipartUpload::builder()
|
||||
.parts(
|
||||
CompletedPart::builder()
|
||||
.part_number(1)
|
||||
.set_e_tag(part1.e_tag().map(str::to_string))
|
||||
.build(),
|
||||
)
|
||||
.build();
|
||||
|
||||
client
|
||||
.complete_multipart_upload()
|
||||
.bucket(CONSISTENCY_BUCKET)
|
||||
.key(MPU_KEY)
|
||||
.upload_id(&upload_id)
|
||||
.multipart_upload(completed)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
client.get_object().bucket(CONSISTENCY_BUCKET).key(MPU_KEY).send().await?;
|
||||
let mpu_list = client
|
||||
.list_objects_v2()
|
||||
.bucket(CONSISTENCY_BUCKET)
|
||||
.prefix(MPU_KEY)
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
list_contains_key(&mpu_list, MPU_KEY),
|
||||
"ListObjectsV2 should include the completed multipart key"
|
||||
);
|
||||
|
||||
client.head_object().bucket(CONSISTENCY_BUCKET).key(MPU_KEY).send().await?;
|
||||
|
||||
let presigned = client
|
||||
.head_object()
|
||||
.bucket(CONSISTENCY_BUCKET)
|
||||
.key(PUT_KEY)
|
||||
.presigned(PresigningConfig::expires_in(Duration::from_secs(300))?)
|
||||
.await?;
|
||||
let presigned_resp = local_http_client().head(presigned.uri().to_string()).send().await?;
|
||||
assert!(
|
||||
presigned_resp.status().is_success(),
|
||||
"Presigned HEAD should succeed, got status {}",
|
||||
presigned_resp.status()
|
||||
);
|
||||
|
||||
client.delete_object().bucket(CONSISTENCY_BUCKET).key(PUT_KEY).send().await?;
|
||||
client.delete_object().bucket(CONSISTENCY_BUCKET).key(MPU_KEY).send().await?;
|
||||
env.delete_test_bucket(CONSISTENCY_BUCKET).await?;
|
||||
env.stop_server();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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.
|
||||
|
||||
//! Erasure-set healing regression tests.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::common::{RustFSTestEnvironment, execute_awscurl, init_logging};
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
use serial_test::serial;
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::time::{Duration, sleep};
|
||||
use tracing::info;
|
||||
|
||||
fn has_file_under(path: &Path) -> bool {
|
||||
let Ok(entries) = std::fs::read_dir(path) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
for entry in entries.filter_map(Result::ok) {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if has_file_under(&path) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn object_metadata_exists_on_disk(disk: &Path, bucket: &str, key: &str) -> bool {
|
||||
disk.join(bucket).join(key).join("xl.meta").is_file()
|
||||
}
|
||||
|
||||
async fn assert_object_body(env: &RustFSTestEnvironment, bucket: &str, key: &str, expected: &[u8]) {
|
||||
let client = env.create_s3_client();
|
||||
let response = client
|
||||
.get_object()
|
||||
.bucket(bucket)
|
||||
.key(key)
|
||||
.send()
|
||||
.await
|
||||
.expect("GET should succeed during/after heal");
|
||||
let body = response.body.collect().await.expect("GET body should collect").into_bytes();
|
||||
assert_eq!(body.as_ref(), expected, "object body changed for {key}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_admin_deep_heal_rebuilds_cleared_disk_in_single_node_erasure_set() {
|
||||
init_logging();
|
||||
info!("Discussion #2964: admin deep heal should rebuild a wiped disk in a 4-disk single-node erasure set");
|
||||
|
||||
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
|
||||
let root = PathBuf::from(env.temp_dir.clone());
|
||||
let disk0 = root.join("disk0");
|
||||
let disk1 = root.join("disk1");
|
||||
let disk2 = root.join("disk2");
|
||||
let disk3 = root.join("disk3");
|
||||
for disk in [&disk0, &disk1, &disk2, &disk3] {
|
||||
std::fs::create_dir_all(disk).expect("disk directory should be created");
|
||||
}
|
||||
|
||||
// The test helper always appends env.temp_dir as the final storage path.
|
||||
// Point it at disk3 and pass the other three disks explicitly.
|
||||
env.temp_dir = disk3.to_string_lossy().to_string();
|
||||
let disk0_arg = disk0.to_string_lossy().to_string();
|
||||
let disk1_arg = disk1.to_string_lossy().to_string();
|
||||
let disk2_arg = disk2.to_string_lossy().to_string();
|
||||
env.start_rustfs_server_with_env(
|
||||
vec![disk0_arg.as_str(), disk1_arg.as_str(), disk2_arg.as_str()],
|
||||
&[("RUSTFS_UNSAFE_BYPASS_DISK_CHECK", "true")],
|
||||
)
|
||||
.await
|
||||
.expect("Failed to start 4-disk RustFS");
|
||||
|
||||
let client = env.create_s3_client();
|
||||
let bucket = "heal-cleared-disk-regression";
|
||||
let target_object_count = std::env::var("RUSTFS_HEAL_REBUILD_OBJECT_COUNT")
|
||||
.ok()
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.unwrap_or(4)
|
||||
.max(4);
|
||||
let heal_timeout_secs = std::env::var("RUSTFS_HEAL_REBUILD_TIMEOUT_SECS")
|
||||
.ok()
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
.unwrap_or(60);
|
||||
|
||||
let mut objects: Vec<(String, Vec<u8>, &'static str)> = vec![
|
||||
(
|
||||
"中文/报告-0001.json".to_string(),
|
||||
"{\"message\":\"hello 中文\"}".as_bytes().to_vec(),
|
||||
"application/json",
|
||||
),
|
||||
(
|
||||
"english/images/photo-0002.jpg".to_string(),
|
||||
vec![0xff, 0xd8, 0xff, 0x00, 0x42, 0x24],
|
||||
"image/jpeg",
|
||||
),
|
||||
(
|
||||
"mixed/空 格 + symbols @#%.txt".to_string(),
|
||||
b"text object with spaces and symbols".to_vec(),
|
||||
"text/plain; charset=utf-8",
|
||||
),
|
||||
(
|
||||
"bin/archive-0004.bin".to_string(),
|
||||
(0..=255).collect::<Vec<u8>>(),
|
||||
"application/octet-stream",
|
||||
),
|
||||
];
|
||||
for index in objects.len()..target_object_count {
|
||||
objects.push((
|
||||
format!("bulk/prefix-{}/object-{index:04}.txt", index % 17),
|
||||
format!("bulk object {index}: heal regression payload").into_bytes(),
|
||||
"text/plain; charset=utf-8",
|
||||
));
|
||||
}
|
||||
|
||||
let object_keys = objects.iter().map(|(key, _, _)| key.clone()).collect::<Vec<_>>();
|
||||
let mut remaining_rebuild_keys: HashSet<String> = object_keys.iter().cloned().collect();
|
||||
|
||||
client
|
||||
.create_bucket()
|
||||
.bucket(bucket)
|
||||
.send()
|
||||
.await
|
||||
.expect("bucket create should succeed");
|
||||
for (key, body, content_type) in &objects {
|
||||
client
|
||||
.put_object()
|
||||
.bucket(bucket)
|
||||
.key(key)
|
||||
.content_type(*content_type)
|
||||
.body(ByteStream::from(body.clone()))
|
||||
.send()
|
||||
.await
|
||||
.expect("PUT should succeed");
|
||||
}
|
||||
|
||||
assert!(has_file_under(&disk0), "disk0 should contain object shards before wipe");
|
||||
env.stop_server();
|
||||
|
||||
std::fs::remove_dir_all(&disk0).expect("disk0 wipe should succeed");
|
||||
std::fs::create_dir_all(&disk0).expect("disk0 should be recreated empty");
|
||||
assert!(!has_file_under(&disk0), "disk0 must be empty before restart");
|
||||
|
||||
env.start_rustfs_server_with_env(
|
||||
vec![disk0_arg.as_str(), disk1_arg.as_str(), disk2_arg.as_str()],
|
||||
&[("RUSTFS_UNSAFE_BYPASS_DISK_CHECK", "true")],
|
||||
)
|
||||
.await
|
||||
.expect("Failed to restart 4-disk RustFS after disk wipe");
|
||||
// The helper's Drop cleanup removes env.temp_dir. Reset it to the parent
|
||||
// directory after server startup so all four disk directories are cleaned
|
||||
// without manually deleting a path Drop will also try to remove.
|
||||
env.temp_dir = root.to_string_lossy().to_string();
|
||||
|
||||
let heal_body = r#"{"recursive":true,"dryRun":false,"remove":false,"recreate":true,"scanMode":2,"updateParity":false,"nolock":false}"#;
|
||||
let heal_url = format!("{}/rustfs/admin/v3/heal/{}?forceStart=true", env.url, bucket);
|
||||
execute_awscurl(&heal_url, "POST", Some(heal_body), &env.access_key, &env.secret_key)
|
||||
.await
|
||||
.expect("admin deep heal should be accepted");
|
||||
|
||||
for _ in 0..heal_timeout_secs {
|
||||
if !remaining_rebuild_keys.is_empty() {
|
||||
let mut rebuilt = Vec::new();
|
||||
for key in &remaining_rebuild_keys {
|
||||
if object_metadata_exists_on_disk(&disk0, bucket, key) {
|
||||
rebuilt.push(key.clone());
|
||||
}
|
||||
}
|
||||
for key in rebuilt {
|
||||
let _ = remaining_rebuild_keys.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
if remaining_rebuild_keys.is_empty() {
|
||||
for (key, body, _) in &objects {
|
||||
assert_object_body(&env, bucket, key, body).await;
|
||||
}
|
||||
|
||||
env.stop_server();
|
||||
for key in &object_keys {
|
||||
assert!(
|
||||
object_metadata_exists_on_disk(&disk0, bucket, key),
|
||||
"wiped disk should contain rebuilt xl.meta for {key}"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
panic!("admin deep heal did not rebuild all files on the wiped disk within timeout");
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user