mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-29 17:48:58 +00:00
Compare commits
444 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f07029373 | |||
| 4b36667ba1 | |||
| 41ba34a145 | |||
| 3898d524fe | |||
| 718bec7722 | |||
| 50ddec3ffc | |||
| 7692c0c3bd | |||
| 3dd0692917 | |||
| 090d60e00a | |||
| c228fabfdb | |||
| bde569d055 | |||
| e19b0dd997 | |||
| 26b3aad069 | |||
| bf893bcb55 | |||
| 60d4598562 | |||
| 565cbdffed | |||
| 49b2782d51 | |||
| 743b87014b | |||
| 3208f930c5 | |||
| 36b3d21c44 | |||
| 935bb8a8e2 | |||
| 9364ecba67 | |||
| 0153710791 | |||
| 995e26f5ee | |||
| e6fdcd1ad6 | |||
| f016673416 | |||
| 42ff6b4d80 | |||
| 3ced40f221 | |||
| a830ab2a5d | |||
| 52f121ab2a | |||
| eb8868397e | |||
| 66c38b629d | |||
| eb23710d2e | |||
| 4b66155f26 | |||
| 4978f60254 | |||
| fe59eb4952 | |||
| a455b4377c | |||
| 08fab34804 | |||
| f07fed0c49 | |||
| dcfbc2612a | |||
| 87e1c7aeb6 | |||
| 0d7e0a814f | |||
| ef5ccc232a | |||
| c29c8a5a1e | |||
| 403997f2f8 | |||
| e34d75dfdf | |||
| 132e72ef39 | |||
| 4f4c759b67 | |||
| e331a26262 | |||
| 61d2e9fbc3 | |||
| 946755aa89 | |||
| c9622611ed | |||
| 7041e628b7 | |||
| 8e1bd560d8 | |||
| d447da75c1 | |||
| 2c9524e2c9 | |||
| e16f1ae639 | |||
| d79720da1d | |||
| b747e9817e | |||
| 90ce72122b | |||
| c4d5c5c5ec | |||
| c698a0f2b6 | |||
| 2953558f41 | |||
| e0b8c4fd42 | |||
| a995ec0315 | |||
| 946b502527 | |||
| 09be06a4d2 | |||
| 159ddd5bac | |||
| a68fe1601f | |||
| 334184b005 | |||
| cfbd094bc4 | |||
| 468dc3aebd | |||
| fd258e877c | |||
| 4dafb64d58 | |||
| 50d03ef021 | |||
| 438913b84a | |||
| 37a3cbc497 | |||
| a05687b900 | |||
| 561d695237 | |||
| 59f41eb86a | |||
| 81854762d4 | |||
| 1e9c75a201 | |||
| 80413e0f8e | |||
| b96ccfd110 | |||
| c717195de2 | |||
| 94f64acc87 | |||
| d949d4e794 | |||
| 7215761784 | |||
| f833cd9cbe | |||
| 13b4500212 | |||
| 2705e3f53b | |||
| 92f812fc80 | |||
| fefb308b35 | |||
| 8d4caeacad | |||
| 2860c82e3c | |||
| 572dd1264e | |||
| 47247789ad | |||
| 39f7de4450 | |||
| de6fe816c2 | |||
| 368ef0f16c | |||
| bc37cc4001 | |||
| ecf0db9bb7 | |||
| fa1554be7f | |||
| f08b592c6f | |||
| 8add0126f5 | |||
| 09a83a8f56 | |||
| a0f1bb4ff0 | |||
| 3ac1d2ab0b | |||
| 4aafb07173 | |||
| 8c76e9838b | |||
| d4dcb2ac9d | |||
| 41d2812861 | |||
| 960c13a34b | |||
| 989827e3b5 | |||
| a77be8f89b | |||
| 1525143a04 | |||
| b7a945e453 | |||
| 3796b684f0 | |||
| 1511f9eacb | |||
| 12f355a3bc | |||
| 83bac39417 | |||
| 177fe2ab44 | |||
| 1d1f00470d | |||
| 457f4e0170 | |||
| 93d0606cbd | |||
| ae7444ebb4 | |||
| b8c788ffca | |||
| 9677320f23 | |||
| 116db4f5d9 | |||
| f9b5ad17a9 | |||
| fb0d096d5d | |||
| a5de275875 | |||
| dc5ce7d0af | |||
| ac443a90ce | |||
| 2d0b3227c5 | |||
| 1cbf156559 | |||
| 03f8270a60 | |||
| 96fb06f48e | |||
| ffcf18f5f3 | |||
| 38eb0781cf | |||
| ce291ab610 | |||
| f77ccd5b23 | |||
| 8dc5ef6ef5 | |||
| f255b8a9f1 | |||
| 478720d2ee | |||
| 6b4172998b | |||
| 6ce24f3b63 | |||
| af93d2daba | |||
| 28edfd6190 | |||
| 579b124726 | |||
| 1ffe23e10f | |||
| 4615791193 | |||
| b2b92de26c | |||
| e05d07494e | |||
| c0d3f53f7a | |||
| 49366ee200 | |||
| 73e6542eea | |||
| f57dd5a3c7 | |||
| 1bda5ed636 | |||
| 27971f5c03 | |||
| 6506ae8c7b | |||
| 8223cda5ff | |||
| 8152c8e084 | |||
| da9dfb51e7 | |||
| c11efce1e4 | |||
| fa793237fb | |||
| df109680ae | |||
| 1eef75b0ca | |||
| 642d83f0e4 | |||
| 16b9189e9b | |||
| 1979fc7fb1 | |||
| 68d3dba9fc | |||
| db8ef63674 | |||
| 6d476ae9a1 | |||
| 2a3d127a86 | |||
| 8abf683178 | |||
| 5048ff8c69 | |||
| 711aab73c3 | |||
| 315e8134eb | |||
| 979626c370 | |||
| 505a566c7c | |||
| 8efa359a1a | |||
| 522c1675d8 | |||
| e160633ac4 | |||
| 458086dc80 | |||
| 0e697fa7e1 | |||
| 30cc481731 | |||
| b9f2369095 | |||
| 4ce374a181 | |||
| ff7dc77b5e | |||
| b387f6c58a | |||
| 64508ae770 | |||
| 6a2f98b468 | |||
| eb0dc24921 | |||
| c8c71e34b6 | |||
| 11552eb722 | |||
| b0b7f56281 | |||
| 120f1021b5 | |||
| 20e07519a1 | |||
| 5c4dadc0b7 | |||
| 0f7e7f35e9 | |||
| 5e97377aa0 | |||
| b49570d87a | |||
| d70ca1990e | |||
| 70fdf79297 | |||
| 4472324125 | |||
| b8c45fc9e3 | |||
| a8a2aaa460 | |||
| 2bf4b14394 | |||
| 59b5e4e722 | |||
| 9ec6465b26 | |||
| 80cb6b3939 | |||
| 30c8bead63 | |||
| 2125cffd0b | |||
| aeef2e67f9 | |||
| 8acf2a51b9 | |||
| 33b50a5366 | |||
| acdd2de21f | |||
| b8bfaccbc5 | |||
| 28d9bbbdcb | |||
| a9507f8ec7 | |||
| 722cab500c | |||
| 71e9178182 | |||
| aee8a4df51 | |||
| 57138fa660 | |||
| 8db55de72c | |||
| 90e584af74 | |||
| ff77cb5540 | |||
| 61c0da936b | |||
| d62114f8d5 | |||
| 9899985d15 | |||
| 064e21062d | |||
| d4ea14c2ba | |||
| 1977d19c29 | |||
| d72e7f6f28 | |||
| 79ffecbf14 | |||
| e3000f16e0 | |||
| ec8f059506 | |||
| 9cd1d6b4d5 | |||
| bf64bcabf8 | |||
| 1f598105dd | |||
| c4b5b00d9c | |||
| 0b99e02891 | |||
| 15c8c7cecf | |||
| c9c2428373 | |||
| d6158c0481 | |||
| 9ed6487e2f | |||
| 72928a43fa | |||
| 55f175b671 | |||
| 3a16c25cd9 | |||
| 6963b898ee | |||
| 7b8d7bdf18 | |||
| 751bc3d737 | |||
| 898857d1c9 | |||
| 97b06afd6c | |||
| 740e4399af | |||
| 832909306e | |||
| 32bf8f5bf3 | |||
| 8d27170ce4 | |||
| dd68a419e3 | |||
| e69d1dc3e6 | |||
| 97d3fb15fb | |||
| 0e5fc4bec1 | |||
| 77229fe426 | |||
| eabbea46d3 | |||
| d2901fd78c | |||
| 67863630b2 | |||
| a9be9af094 | |||
| 372f004a8d | |||
| 0ac3b5b992 | |||
| 25512e2635 | |||
| 2d91e2f580 | |||
| c244943313 | |||
| b3f31ad694 | |||
| c4efb46827 | |||
| 1fe036cb70 | |||
| 5d302febb7 | |||
| 6a114cd2e0 | |||
| 6696703343 | |||
| aefb99f1da | |||
| c44309c16a | |||
| c3361e38d6 | |||
| 30a757a9da | |||
| 84f58af628 | |||
| 890837aee8 | |||
| c513275741 | |||
| 9d3191e55b | |||
| 6fba01fb65 | |||
| a8af7c9617 | |||
| bd36cf3588 | |||
| 8893de1cad | |||
| d3dee898ee | |||
| d960acb170 | |||
| e86c6b726f | |||
| 15995aae14 | |||
| 6bf0c542a1 | |||
| d56e839f20 | |||
| 0fb070e912 | |||
| 172086ff42 | |||
| 0b0c10b323 | |||
| b256154f25 | |||
| d5f05993a3 | |||
| d0ea41e190 | |||
| 74b2c70602 | |||
| 16db18216d | |||
| 387c385dfa | |||
| dd9e093dcc | |||
| 7172e151de | |||
| 860a37d3a8 | |||
| 3515615e79 | |||
| 924c4b17a6 | |||
| 7fb405526b | |||
| 4764b849cb | |||
| 0c6bb6add5 | |||
| 3578baf501 | |||
| 939a69f9c2 | |||
| 3366bd2464 | |||
| 263e504c0c | |||
| f98664ea4a | |||
| 9536ed8d38 | |||
| ef33e43032 | |||
| c20f2555eb | |||
| 5e21c398f5 | |||
| af46a61fde | |||
| 14e4d94666 | |||
| 0779036535 | |||
| d637c4d342 | |||
| a236b0d01d | |||
| 59c437d901 | |||
| 0c42916fa9 | |||
| 41dcebda44 | |||
| fb2ced4d27 | |||
| 2681731443 | |||
| 19b8389dc4 | |||
| 8c8d157418 | |||
| 28f57b228c | |||
| 8aa59b12cb | |||
| dad9a7d708 | |||
| 5ea6d8a7e6 | |||
| 75e6902f46 | |||
| 24d359a867 | |||
| ca62b0c163 | |||
| b7789c8e08 | |||
| e95eb92612 | |||
| 2583d1e49b | |||
| 990acbcd4b | |||
| 05dc131a49 | |||
| 236142a682 | |||
| 2e7abfbd63 | |||
| 6cb094e30a | |||
| ff40e2bc79 | |||
| 99dbe70a89 | |||
| a42320848c | |||
| 8b4e5b2540 | |||
| 19d3a23a13 | |||
| 95850c1bcd | |||
| 628481be7c | |||
| 0047bcd3ac | |||
| fafbc4fe1d | |||
| f11c307aec | |||
| 3c28f0a0ba | |||
| 28f86a505e | |||
| e1a278aaf8 | |||
| 35e1f28f23 | |||
| c6715259b1 | |||
| df8f0edaea | |||
| a4411a24d6 | |||
| 4a807d80e3 | |||
| f095f56e20 | |||
| b9b7d86ae4 | |||
| 84077adf17 | |||
| 237c933f38 | |||
| c20b3c7f19 | |||
| c9a2fd756c | |||
| bddb0d0a05 | |||
| 9ce3c7742c | |||
| b5d881f399 | |||
| ce1f7cfdcb | |||
| c66c6d97ec | |||
| be89b5fc6a | |||
| 94cdb89e29 | |||
| 06dff96c09 | |||
| c1d5106acc | |||
| 0a2411f59c | |||
| 1ede71b881 | |||
| 4fb7059e6f | |||
| 2ad275ecc3 | |||
| 9179fd5608 | |||
| 7f1cdaedad | |||
| 7f3459f5a8 | |||
| d3cff7d033 | |||
| f66a90c1b2 | |||
| afcaaf66fc | |||
| a1104b45f6 | |||
| 82d9452736 | |||
| 6e0f034ad1 | |||
| 593a58c161 | |||
| f83bf95b04 | |||
| aa88b1976a | |||
| e2f741d41f | |||
| ad54293d7e | |||
| 83fb530609 | |||
| aa84d34bf8 | |||
| df57f0c033 | |||
| c47dec8549 | |||
| fdbe12ec95 | |||
| b2e8078971 | |||
| ac43a44a00 | |||
| 5625f04697 | |||
| e1f24f764d | |||
| 7d7e0b2654 | |||
| 9908a44c38 | |||
| 4b480727d6 | |||
| f00d01ec2d | |||
| 7e8c7fa2b2 | |||
| 845ad1fa16 | |||
| bb4fbf5ae2 | |||
| 3df7105dae | |||
| b3da8ae269 | |||
| 67e5f5e3c3 | |||
| 296efea42f | |||
| 16946c5a54 | |||
| 73d29e95dd | |||
| e930c5c281 | |||
| 9d03029959 | |||
| a02c354ef5 | |||
| 60aa47bf61 | |||
| 8e4a1ef917 | |||
| b035d10abb | |||
| 2180e9e7a1 | |||
| 57e49e6737 | |||
| b07383760f | |||
| 7c94be4e8c | |||
| d52a10c5fb | |||
| 8c4735ff88 | |||
| a0503168d4 | |||
| b73059dcf2 | |||
| ed18b3da75 | |||
| 05032cf887 | |||
| f89cdfe5b3 | |||
| f4b523c236 | |||
| c6209ba59d | |||
| 5e7495a042 | |||
| ac4b13def1 |
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: code-change-verification
|
||||
description: Verify code changes by identifying correctness, regression, security, and performance risks from diffs or patches, then produce prioritized findings with file/line evidence and concrete fixes. Use when reviewing commits, PRs, and merged patches before/after release.
|
||||
---
|
||||
|
||||
# Code Change Verification
|
||||
|
||||
Use this skill to review code changes consistently before merge, before release, and during incident follow-up.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Read the scope: commit, PR, patch, or file list.
|
||||
2. Map each changed area by risk and user impact.
|
||||
3. Inspect each risky change in context.
|
||||
4. Report findings first, ordered by severity.
|
||||
5. Close with residual risks and verification recommendations.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
### 1) Scope and assumptions
|
||||
- Confirm change source (diff, commit, PR, files), target branch, language/runtime, and version.
|
||||
- If context is missing, state assumptions before deeper analysis.
|
||||
- Focus only on requested scope; avoid reviewing unrelated files.
|
||||
|
||||
### 2) Risk map
|
||||
- Prioritize in this order:
|
||||
- Data correctness and user-visible behavior
|
||||
- API/contract compatibility
|
||||
- Security and authz/authn boundaries
|
||||
- Concurrency and lifecycle correctness
|
||||
- Performance and resource usage
|
||||
- Give higher priority to stateful paths, migration logic, defaults, and error handling.
|
||||
|
||||
### 3) Evidence-based inspection
|
||||
- Read each modified hunk with neighboring context.
|
||||
- Trace call paths and call-site expectations.
|
||||
- Check for:
|
||||
- invariant breaks and missing guards
|
||||
- unchecked assumptions and null/empty/error-path handling
|
||||
- stale tests, fixtures, and configs
|
||||
- hidden coupling to shared helpers/constants/features
|
||||
- If a point is uncertain, mark it as an open question instead of guessing.
|
||||
|
||||
### 4) Findings-first output
|
||||
- Order findings by severity:
|
||||
- P0: critical failure, security breach, or data loss risk
|
||||
- P1: high-impact regression
|
||||
- P2: medium risk correctness gap
|
||||
- P3: low risk/quality debt
|
||||
- For each finding include:
|
||||
- Severity
|
||||
- `path:line` reference
|
||||
- concise issue statement
|
||||
- impact and likely failure mode
|
||||
- specific fix or mitigation
|
||||
- validation step to confirm
|
||||
- If no issues exist, explicitly state `No findings` and why.
|
||||
|
||||
### 5) Close
|
||||
- Report assumptions and unknowns.
|
||||
- Suggest targeted checks (tests, canary checks, logs/metrics, migration validation).
|
||||
|
||||
## Output Template
|
||||
|
||||
1. Findings
|
||||
2. No findings (if applicable)
|
||||
3. Assumptions / Unknowns
|
||||
4. Recommended verification steps
|
||||
|
||||
## Finding Template
|
||||
|
||||
- `[P1] Missing timeout for downstream call`
|
||||
- Location: `path/to/file.rs:123`
|
||||
- Issue: ...
|
||||
- Impact: ...
|
||||
- Fix suggestion: ...
|
||||
- Validation: ...
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Code Change Verification"
|
||||
short_description: "Prioritize risks and verify code changes before merge."
|
||||
default_prompt: "Inspect a patch or diff, identify correctness/security/regression risks, and return prioritized findings with file/line evidence and fixes."
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
name: pr-creation-checker
|
||||
description: Prepare PR-ready diffs by validating scope, checking required verification steps, drafting a compliant English PR title/body, and surfacing blockers before opening or updating a pull request in RustFS.
|
||||
---
|
||||
|
||||
# PR Creation Checker
|
||||
|
||||
Use this skill before `gh pr create`, before `gh pr edit`, or when reviewing whether a branch is ready for PR.
|
||||
|
||||
## Read sources of truth first
|
||||
|
||||
- Read `AGENTS.md`.
|
||||
- Read `.github/pull_request_template.md`.
|
||||
- Use `Makefile` and `.config/make/` for local quality commands.
|
||||
- Use `.github/workflows/ci.yml` for CI expectations.
|
||||
- Do not restate long command matrices or template sections from memory when the files exist.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Collect PR context
|
||||
- Confirm base branch, current branch, change goal, and scope.
|
||||
- Confirm whether the task is: draft a new PR, update an existing PR, or preflight-check readiness.
|
||||
- Confirm whether the branch includes only intended changes.
|
||||
|
||||
2. Inspect change scope
|
||||
- Review the diff and summarize what changed.
|
||||
- Call out unrelated edits, generated artifacts, logs, or secrets as blockers.
|
||||
- Mark risky areas explicitly: auth, storage, config, network, migrations, breaking changes.
|
||||
- Scan the diff for newly added string literals and confirm whether they duplicate values already defined as constants/enums/typed wrappers in the same module or shared modules.
|
||||
- Treat introducing a new hardcoded literal where a project constant already exists as a likely regression risk; require either a refactor to reuse the constant or an explicit exception explanation in the PR body.
|
||||
|
||||
3. Verify readiness requirements
|
||||
- Require `make pre-commit` before marking the PR ready.
|
||||
- If `make` is unavailable, use the equivalent commands from `.config/make/`.
|
||||
- Add scope-specific verification commands when the changed area needs more than the baseline.
|
||||
- If required checks fail, stop and return `BLOCKED`.
|
||||
|
||||
4. Draft PR metadata
|
||||
- Write the PR title in English using Conventional Commits and keep it within 72 characters.
|
||||
- If a generic PR workflow suggests a different title format, ignore it and follow the repository rule instead.
|
||||
- In RustFS, do not use tool-specific prefixes such as `[codex]` when the repository requires Conventional Commits.
|
||||
- Keep the PR body in English.
|
||||
- Use the exact section headings from `.github/pull_request_template.md`.
|
||||
- Fill non-applicable sections with `N/A`.
|
||||
- Include verification commands in the PR description.
|
||||
- Do not include local filesystem paths in the PR body unless the user explicitly asks for them.
|
||||
- Prefer repo-relative paths, command names, and concise summaries over machine-specific paths such as `/Users/...`.
|
||||
|
||||
5. Prepare reviewer context
|
||||
- Summarize why the change exists.
|
||||
- Summarize what was verified.
|
||||
- Call out risks, rollout notes, config impact, and rollback notes when applicable.
|
||||
- Mention assumptions or missing context instead of guessing.
|
||||
|
||||
6. Prepare CLI-safe output
|
||||
- When proposing `gh pr create` or `gh pr edit`, use `--body-file`, never inline `--body` for multiline markdown.
|
||||
- Return a ready-to-save PR body plus a short title.
|
||||
- If not ready, return blockers first and list the minimum steps needed to unblock.
|
||||
|
||||
## Output format
|
||||
|
||||
### Status
|
||||
- `READY` or `BLOCKED`
|
||||
|
||||
### Title
|
||||
- `<type>(<scope>): <summary>`
|
||||
|
||||
### PR Body
|
||||
- Reproduce the repository template headings exactly.
|
||||
- Fill every section.
|
||||
- Omit local absolute paths unless explicitly required.
|
||||
|
||||
### Verification
|
||||
- List each command run.
|
||||
- State pass/fail.
|
||||
|
||||
### Risks
|
||||
- List breaking changes, config changes, migration impact, or `N/A`.
|
||||
|
||||
## Blocker rules
|
||||
|
||||
- Return `BLOCKED` if `make pre-commit` has not passed.
|
||||
- Return `BLOCKED` if the diff contains unrelated changes that are not acknowledged.
|
||||
- Return `BLOCKED` if required template sections are missing.
|
||||
- Return `BLOCKED` if the title/body is not in English.
|
||||
- Return `BLOCKED` if the title does not follow the repository's Conventional Commit rule.
|
||||
- Return `BLOCKED` if the diff introduces string literals that should use existing constants but did not.
|
||||
|
||||
## Reference
|
||||
|
||||
- Use [pr-readiness-checklist.md](references/pr-readiness-checklist.md) for a short final pass before opening or editing the PR.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "PR Creation Checker"
|
||||
short_description: "Draft RustFS-ready PRs with checks, template, and blockers."
|
||||
default_prompt: "Inspect a branch or diff, verify required PR checks, and produce a compliant English PR title/body plus blockers or readiness status."
|
||||
@@ -0,0 +1,15 @@
|
||||
# PR Readiness Checklist
|
||||
|
||||
- Confirm the branch is based on current `main`.
|
||||
- Confirm the diff matches the stated scope.
|
||||
- Confirm no secrets, logs, temp files, or unrelated refactors are included.
|
||||
- Confirm `make pre-commit` passed, or document why it could not run.
|
||||
- Confirm extra verification commands are listed for risky changes.
|
||||
- Confirm the PR title uses Conventional Commits and stays within 72 characters.
|
||||
- Confirm the PR title does not use tool-specific prefixes such as `[codex]`.
|
||||
- Confirm the PR body is in English.
|
||||
- Confirm the PR body keeps the exact headings from `.github/pull_request_template.md`.
|
||||
- Confirm non-applicable sections are filled with `N/A`.
|
||||
- Confirm the PR body does not include local absolute paths unless explicitly required.
|
||||
- Confirm multiline GitHub CLI commands use `--body-file`.
|
||||
- Confirm new hardcoded string literals were not introduced for values already represented by existing constants/enums (including protocol labels, error identifiers, headers, and metric names), or record a justified exception.
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
name: security-advisory-lessons
|
||||
description: Apply RustFS security lessons distilled from repository GitHub Security Advisories. Use when making or reviewing RustFS code changes, doing security checks, handling PR review for auth/authz, IAM, storage, RPC, logging, CORS, console/browser, encryption, policy, or endpoint changes, and when deciding which security regression tests are required.
|
||||
---
|
||||
|
||||
# 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).
|
||||
|
||||
When currentness matters, refresh the advisory inventory first:
|
||||
|
||||
```bash
|
||||
gh api repos/rustfs/rustfs/security-advisories --paginate \
|
||||
--jq '.[] | {ghsa_id,state,severity,summary,updated_at,html_url}'
|
||||
```
|
||||
|
||||
For the full pattern map, read [advisory-patterns.md](references/advisory-patterns.md).
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Scope the change
|
||||
- Identify touched routes, handlers, storage paths, credentials, logs, browser surfaces, CI/release code, and policy checks.
|
||||
- Treat these paths as security-sensitive by default: `rustfs/src/admin/`, `rustfs/src/storage/`, `rustfs/src/auth.rs`, `rustfs/src/server/layer.rs`, `crates/iam/`, `crates/policy/`, `crates/credentials/`, `crates/ecstore/src/rpc/`, `crates/rio/`, and console preview/auth code.
|
||||
|
||||
### 2. Map to advisory classes
|
||||
- Read [advisory-patterns.md](references/advisory-patterns.md) for matching GHSA lessons.
|
||||
- Do not rely on advisory titles alone. Confirm whether the issue is authentication, authorization, input validation, storage invariant, browser isolation, logging, or operational hardening.
|
||||
|
||||
### 3. Verify fail-closed behavior
|
||||
- Check that unauthenticated, wrong-permission, cross-user, cross-bucket, malformed-input, and default-config cases fail explicitly.
|
||||
- Prefer exact action/permission checks over broad helper calls or inferred ownership.
|
||||
- Confirm lower storage/RPC layers do not bypass checks done in upper layers.
|
||||
|
||||
### 4. Require regression evidence
|
||||
- For behavior changes, add focused negative tests that reproduce the advisory class.
|
||||
- For sensitive fixes, include tests for the bypass form, not only the happy path.
|
||||
- If a test is impractical, explain the residual risk and provide a manual verification command.
|
||||
|
||||
### 5. Report clearly
|
||||
- Lead with concrete findings and file/line evidence.
|
||||
- Separate proven vulnerabilities from hardening risks.
|
||||
- Avoid exaggerating unauthenticated impact when the code actually rejects unauthenticated requests but allows a low-privileged authenticated bypass.
|
||||
|
||||
## Advisory-Derived Guardrails
|
||||
|
||||
### Auth and admin authorization
|
||||
- Every admin or diagnostic route needs an explicit authn and authz story. Route registration, router whitelist, and handler-level authorization must agree.
|
||||
- Match the admin action to the operation exactly. Copy-paste action constants are a known RustFS vulnerability class.
|
||||
- Avoid authentication-only helpers for state-changing admin APIs; use `validate_admin_request` or the established equivalent with the right `AdminAction`.
|
||||
- Do not assume admin-action `Resource` scoping constrains blast radius unless the policy engine actually enforces resources for that action.
|
||||
|
||||
### IAM and service accounts
|
||||
- Treat imported IAM payload fields as attacker-controlled: `parent`, `claims`, `accessKey`, `secretKey`, status, policy names, and groups.
|
||||
- For service account create/update/import, prove parent ownership or root/admin authority before writing credentials or claims.
|
||||
- Do not let `deny_only` or "no explicit deny" become an allow decision that skips required allow checks.
|
||||
- Test cross-user list/update/import flows with wrong, correct, self, parent, and root identities.
|
||||
|
||||
### S3 copy, multipart, and presigned POST
|
||||
- Multipart copy must enforce source `GetObject` and destination `PutObject` semantics equivalent to `CopyObject`, including copy-source and policy conditions.
|
||||
- Do not let `CreateMultipartUpload`, `UploadPartCopy`, `CompleteMultipartUpload`, or `AbortMultipartUpload` return success without authorization.
|
||||
- Presigned POST policies are server-side contracts. Enforce `content-length-range`, key prefix, exact metadata/content-type, and all signed policy conditions.
|
||||
|
||||
### Paths, object keys, and filesystem access
|
||||
- Never join untrusted bucket/object/RPC path strings onto filesystem roots without normalization and boundary checks.
|
||||
- Reject or safely handle `..`, absolute paths, URL-encoded traversal, platform separators, empty components, and paths that canonicalize outside the intended root.
|
||||
- Validate both S3 object-key paths and internode/RPC disk paths; storage helpers can bypass S3 authorization if they trust already-parsed paths.
|
||||
|
||||
### Secrets, default credentials, and crypto
|
||||
- Do not ship hard-coded shared tokens, HMAC secrets, private keys, or production test keys.
|
||||
- Defaults for internode/RPC auth must fail closed for network-reachable deployments or require explicit opt-in with loud warnings.
|
||||
- License or token validation must use signatures with embedded public/verifying keys only; do not use private-key decryption as authenticity.
|
||||
- Plan key rotation and key IDs when removing exposed keys.
|
||||
|
||||
### Logging and debug output
|
||||
- Logs must never include access keys beyond safe identifiers, secret keys, session tokens, JWT claims, HMAC secrets, expected signatures, license secrets, or raw response bodies containing credentials.
|
||||
- Treat `Debug` implementations, `?value` tracing, merged config dumps, and dependency-level HTTP body logging as leak surfaces.
|
||||
- Add log-capture tests or targeted unit tests for redaction wrappers when changing credential structs or response bodies.
|
||||
|
||||
### RPC, parsing, and panic safety
|
||||
- Treat all RPC payload bytes as attacker-controlled. Replace `unwrap`, `expect`, and panic-prone deserialization with typed errors.
|
||||
- Malformed request tests should cover empty bytes, truncated MessagePack/protobuf, invalid enum values, stale timestamps, and invalid signatures.
|
||||
- RPC authentication must be independently strong; do not depend on S3 admin credentials unless the fallback is explicit and safe.
|
||||
|
||||
### Browser, CORS, and console surfaces
|
||||
- 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.
|
||||
|
||||
### 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.
|
||||
- Do not return absolute filesystem paths or other deployment layout in unauthenticated or low-privilege responses.
|
||||
- Ensure health endpoint allowlists cannot accidentally include expensive diagnostics.
|
||||
|
||||
### Trusted proxy and network identity
|
||||
- Only honor `X-Forwarded-For` or `X-Real-IP` when the request came from a configured trusted proxy.
|
||||
- Direct clients must use the socket peer address for `aws:SourceIp` and policy condition evaluation.
|
||||
- Add tests for direct spoofed headers and trusted-proxy headers.
|
||||
|
||||
### SSE and storage invariants
|
||||
- Encryption metadata is not proof that bytes were encrypted on disk.
|
||||
- When touching reader/writer wrappers such as hashing, encryption, compression, or warp readers, verify wrapper order and inspect stored bytes in regression tests.
|
||||
- Avoid helper shortcuts that unwrap nested readers and accidentally bypass encryption or integrity layers.
|
||||
|
||||
## Review Prompts
|
||||
|
||||
Use these prompts while reviewing a diff:
|
||||
|
||||
- Could a low-privileged authenticated user reach this path with the wrong action, parent, bucket, or source object?
|
||||
- Does a public/default/empty config change security behavior from fail-closed to fail-open?
|
||||
- Is any attacker-controlled value later used as a path, policy condition, credential identity, log field, URL, Origin, or response body?
|
||||
- Is the same operation implemented in multiple paths, such as `CopyObject` vs `UploadPartCopy`, and do all paths enforce the same security contract?
|
||||
- Does the test prove the exploit form is denied, or only that the intended form still works?
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Security Advisory Lessons"
|
||||
short_description: "Apply advisory lessons in reviews."
|
||||
default_prompt: "Review code changes against past RustFS security advisory lessons and report concrete risks, missing tests, and recommended fixes."
|
||||
@@ -0,0 +1,85 @@
|
||||
# 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.
|
||||
|
||||
Refresh this file when new advisories appear or when an advisory changes state materially.
|
||||
|
||||
## 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
## Useful Search Seeds
|
||||
|
||||
Use these targeted searches when a diff touches security-sensitive code:
|
||||
|
||||
```bash
|
||||
rg -n "validate_admin_request|check_permissions|AdminAction::|deny_only|is_allowed" rustfs crates
|
||||
rg -n "UploadPartCopy|upload_part_copy|CompleteMultipart|PostObject|content-length-range|starts-with" rustfs crates
|
||||
rg -n "PathBuf::join|canonicalize|\\.\\.|x-forwarded-for|x-real-ip|SourceIp" rustfs crates
|
||||
rg -n "DEFAULT_SECRET|DEFAULT_ACCESS|TEST_PRIVATE_KEY|rustfs rpc|RUSTFS_RPC_SECRET" rustfs crates
|
||||
rg -n "debug!|trace!|info!|error!|\\?resp|\\?merged_config|session_token|secret_key" rustfs crates
|
||||
rg -n "HashReader|EncryptReader|SSE|server-side encryption|Access-Control-Allow-Credentials|Origin" rustfs crates
|
||||
```
|
||||
|
||||
## Minimum Regression Test Expectations
|
||||
|
||||
- Authz fixes: include unauthenticated, valid low-privilege, wrong-action, correct-action, owner, non-owner, and root/admin cases as applicable.
|
||||
- IAM fixes: include import/update/list service-account cases with attacker-controlled parent, claims, access key, secret key, and policy.
|
||||
- Copy/upload fixes: include cross-bucket, cross-user, source-denied, destination-denied, copy-source-condition, and multipart completion cases.
|
||||
- Path fixes: include encoded traversal, absolute path, nested traversal, valid object keys that resemble traversal text but should be rejected, and canonical boundary checks.
|
||||
- Logging fixes: assert redacted output for structs and response bodies that may contain credentials.
|
||||
- Browser/CORS fixes: assert no credentials on reflected/default origins, correct behavior for explicit allowlists, and no same-origin script execution for previewed object content.
|
||||
- SSE fixes: inspect stored bytes and verify API metadata, read-back behavior, and on-disk ciphertext together.
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: test-coverage-improver
|
||||
description: Run project coverage checks, rank high-risk gaps, and propose high-impact tests to improve regression confidence for changed and critical code paths before release.
|
||||
---
|
||||
|
||||
# Test Coverage Improver
|
||||
|
||||
Use this skill when you need a prioritized, risk-aware plan to improve tests from coverage results.
|
||||
|
||||
## Usage assumptions
|
||||
- Focus scope is either changed lines/files, a module, or the whole repository.
|
||||
- Coverage artifact must be generated or provided in a supported format.
|
||||
- If required context is missing, call out assumptions explicitly before proposing work.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Define scope and baseline
|
||||
- Confirm target language, framework, and branch.
|
||||
- Confirm whether the scope is changed files only or full-repo.
|
||||
|
||||
2. Produce coverage snapshot
|
||||
- Rust: `cargo llvm-cov` (or `cargo tarpaulin`) with existing repo config.
|
||||
- JavaScript/TypeScript: `npm test -- --coverage` and read `coverage/coverage-final.json`.
|
||||
- Python: `pytest --cov=<pkg> --cov-report=json` and read `coverage.json`.
|
||||
- Collect total, per-file, and changed-line coverage.
|
||||
|
||||
3. Rank highest-risk gaps
|
||||
- Prioritize changed code, branch coverage gaps, and low-confidence boundaries.
|
||||
- Apply the risk rubric in [coverage-prioritization.md](references/coverage-prioritization.md).
|
||||
- Keep shortlist to 5–8 gaps.
|
||||
- For each gap, capture: file, lines, uncovered branches, and estimated risk score.
|
||||
|
||||
4. Propose high-impact tests
|
||||
- For each shortlisted gap, output:
|
||||
- Intent and expected behavior.
|
||||
- Normal, edge, and failure scenarios.
|
||||
- Assertions and side effects to verify.
|
||||
- Setup needs (fixtures, mocks, integration dependencies).
|
||||
- Estimated effort (`S/M/L`).
|
||||
|
||||
5. Close with validation plan
|
||||
- State which gaps remain after proposals.
|
||||
- Provide concrete verification command and acceptance threshold.
|
||||
- List assumptions or blockers (environment, fixtures, flaky dependencies).
|
||||
|
||||
## Output template
|
||||
|
||||
### Coverage Snapshot
|
||||
- total / branch coverage
|
||||
- changed-file coverage
|
||||
- top missing regions by size
|
||||
|
||||
### Top Gaps (ranked)
|
||||
- `path:line-range` | risk score | why critical
|
||||
|
||||
### Test Proposals
|
||||
- `path:line-range`
|
||||
- Test name
|
||||
- scenarios
|
||||
- assertions
|
||||
- effort
|
||||
|
||||
### Validation Plan
|
||||
- command
|
||||
- pass criteria
|
||||
- remaining risk
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Test Coverage Improver"
|
||||
short_description: "Find top uncovered risk areas and propose high-impact tests."
|
||||
default_prompt: "Run coverage checks, identify largest gaps, and recommend highest-impact test cases to improve risk coverage."
|
||||
@@ -0,0 +1,25 @@
|
||||
# Coverage Gap Prioritization Guide
|
||||
|
||||
Use this rubric for each uncovered area.
|
||||
|
||||
Score = (Criticality × 2) + CoverageDebt + (Volatility × 0.5)
|
||||
|
||||
- Criticality:
|
||||
- 5: authz/authn, data-loss, payment/consistency path
|
||||
- 4: state mutation, cache invalidation, scheduling
|
||||
- 3: error handling + fallbacks in user-visible flows
|
||||
- 2: parsing/format conversion paths
|
||||
- 1: logging-only or low-impact utilities
|
||||
|
||||
- CoverageDebt:
|
||||
- 0: 0–5 uncovered lines
|
||||
- 1: 6–20 uncovered lines
|
||||
- 2: 21–40 uncovered lines
|
||||
- 3: 41+ uncovered lines
|
||||
|
||||
- Volatility:
|
||||
- 1: stable legacy code with few recent edits
|
||||
- 2: changed in last 2 releases
|
||||
- 3: touched in last 30 days or currently in active PR
|
||||
|
||||
Sort by score descending, then by business impact.
|
||||
@@ -0,0 +1,26 @@
|
||||
# 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.
|
||||
|
||||
# RustFS Cargo configuration
|
||||
|
||||
# Enable tokio_unstable cfg for dial9-tokio-telemetry support
|
||||
# This allows dial9 to hook into Tokio's internal runtime events
|
||||
[build]
|
||||
# Enable Tokio unstable features required by dial9-tokio-telemetry for runtime tracing.
|
||||
# See: https://docs.rs/tokio/latest/tokio/#unstable-features
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
|
||||
# Enable frame pointers for CPU profiling (Linux only, optional but recommended)
|
||||
# Uncomment the following line for better CPU profiling data
|
||||
# rustflags = ["--cfg", "tokio_unstable", "-C", "force-frame-pointers=yes"]
|
||||
@@ -36,6 +36,24 @@ To start a 4-node cluster for distributed testing:
|
||||
docker compose -f .docker/compose/docker-compose.cluster.yaml up -d
|
||||
```
|
||||
|
||||
### Script-Based 4-Node Validation (Recommended)
|
||||
|
||||
Use the local validation script when you need local-source image build, failover checks,
|
||||
and benchmark workflow in one command:
|
||||
|
||||
```bash
|
||||
# Default mode: WAIT_PROBE_MODE=service
|
||||
# This avoids false negatives where /health/ready remains 503 locally
|
||||
# while the service path is already available.
|
||||
./scripts/run_four_node_cluster_failover_bench.sh
|
||||
```
|
||||
|
||||
Strict mode is available when you explicitly want `/health/ready == 200` as the gate:
|
||||
|
||||
```bash
|
||||
WAIT_PROBE_MODE=ready ./scripts/run_four_node_cluster_failover_bench.sh
|
||||
```
|
||||
|
||||
### (Deprecated) Minimal Observability
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
# 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.
|
||||
|
||||
services:
|
||||
node1:
|
||||
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:-rustfsadmin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin}
|
||||
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||
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:
|
||||
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:-rustfsadmin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin}
|
||||
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||
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:
|
||||
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:-rustfsadmin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin}
|
||||
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||
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:
|
||||
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:-rustfsadmin}
|
||||
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin}
|
||||
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||
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
|
||||
@@ -26,7 +26,7 @@ services:
|
||||
restart: "no"
|
||||
|
||||
tempo:
|
||||
image: grafana/tempo:latest
|
||||
image: grafana/tempo:2.10.5
|
||||
user: "10001"
|
||||
command: [ "-config.file=/etc/tempo.yaml" ]
|
||||
volumes:
|
||||
@@ -36,9 +36,19 @@ services:
|
||||
- "3200:3200" # tempo
|
||||
- "4317" # otlp grpc
|
||||
- "4318" # otlp http
|
||||
- "7946" # memberlist
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- rustfs-network
|
||||
depends_on:
|
||||
tempo-init:
|
||||
condition: service_completed_successfully
|
||||
healthcheck:
|
||||
test: [ "CMD", "/tempo", "-version" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
|
||||
otel-collector:
|
||||
image: otel/opentelemetry-collector-contrib:latest
|
||||
@@ -61,6 +71,12 @@ services:
|
||||
- jaeger
|
||||
- prometheus
|
||||
- loki
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: [ "CMD", "/otelcol-contrib", "--version" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
jaeger:
|
||||
image: jaegertracing/jaeger:latest
|
||||
@@ -72,12 +88,23 @@ services:
|
||||
- BADGER_DIRECTORY_KEY=/badger/key
|
||||
- COLLECTOR_OTLP_ENABLED=true
|
||||
volumes:
|
||||
- ../../.docker/observability/jaeger.yaml:/etc/jaeger/config.yml:ro
|
||||
- jaeger-data:/badger
|
||||
ports:
|
||||
- "16686:16686" # Web UI
|
||||
- "14269:14269" # Admin/Metrics
|
||||
- "4317" # otlp grpc
|
||||
- "4318" # otlp http
|
||||
command: [ "--config", "/etc/jaeger/config.yml" ]
|
||||
networks:
|
||||
- rustfs-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:14269" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
@@ -85,6 +112,7 @@ services:
|
||||
- TZ=Asia/Shanghai
|
||||
volumes:
|
||||
- ../../.docker/observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ../../.docker/observability/prometheus-rules:/etc/prometheus/rules:ro
|
||||
- prometheus-data:/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
@@ -94,23 +122,45 @@ services:
|
||||
- '--web.enable-remote-write-receiver'
|
||||
- '--enable-feature=promql-experimental-functions'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
||||
- '--web.console.templates=/usr/share/prometheus/consoles'
|
||||
- '--storage.tsdb.retention.time=30d'
|
||||
networks:
|
||||
- rustfs-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:9090/-/healthy" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
loki:
|
||||
image: grafana/loki:latest
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
volumes:
|
||||
- ../../.docker/observability/loki-config.yaml:/etc/loki/local-config.yaml:ro
|
||||
- ../../.docker/observability/loki.yaml:/etc/loki/loki.yaml:ro
|
||||
- loki-data:/loki
|
||||
ports:
|
||||
- "3100:3100"
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
command: -config.file=/etc/loki/loki.yaml
|
||||
networks:
|
||||
- rustfs-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3100/ready" ]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
|
||||
pyroscope:
|
||||
image: grafana/pyroscope:latest
|
||||
ports:
|
||||
- "4040:4040"
|
||||
command:
|
||||
- -self-profiling.disable-push=true
|
||||
networks:
|
||||
- rustfs-network
|
||||
restart: unless-stopped
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
@@ -127,10 +177,17 @@ services:
|
||||
volumes:
|
||||
- ../../.docker/observability/grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- ../../.docker/observability/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
- grafana-data:/var/lib/grafana
|
||||
depends_on:
|
||||
- prometheus
|
||||
- tempo
|
||||
- loki
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
# --- RustFS Cluster ---
|
||||
|
||||
@@ -215,6 +272,7 @@ volumes:
|
||||
tempo-data:
|
||||
loki-data:
|
||||
jaeger-data:
|
||||
grafana-data:
|
||||
|
||||
networks:
|
||||
rustfs-network:
|
||||
|
||||
@@ -74,6 +74,17 @@ http {
|
||||
# ssl_certificate /etc/nginx/ssl/server.crt;
|
||||
# ssl_certificate_key /etc/nginx/ssl/server.key;
|
||||
#
|
||||
# # Restrict to modern TLS versions and ciphers. Operators copying this
|
||||
# # example must keep at least these directives — without them, nginx
|
||||
# # may negotiate older protocol versions that have known weaknesses.
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# ssl_prefer_server_ciphers on;
|
||||
# ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
|
||||
# ssl_session_timeout 1d;
|
||||
# ssl_session_cache shared:SSL:10m;
|
||||
# ssl_session_tickets off;
|
||||
# # add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
#
|
||||
# location / {
|
||||
# proxy_pass http://rustfs:9000;
|
||||
# ...
|
||||
|
||||
@@ -13,6 +13,9 @@ The stack is composed of the following best-in-class open-source components:
|
||||
- **Jaeger** (v1.59.0): Distributed tracing system (configured as a secondary UI/storage).
|
||||
- **OpenTelemetry Collector** (v0.104.0): A vendor-agnostic implementation for receiving, processing, and exporting telemetry data.
|
||||
|
||||
By default, this stack uses Tempo in single-binary mode and does not require Kafka/Redpanda.
|
||||
If you want the Kafka-backed HA Tempo path, use `docker-compose-example-for-rustfs.yml` together with `docker-compose-tempo-ha-override.yml`.
|
||||
|
||||
## Architecture
|
||||
|
||||
1. **Telemetry Collection**: Applications send OTLP (OpenTelemetry Protocol) data (Metrics, Logs, Traces) to the **OpenTelemetry Collector**.
|
||||
@@ -46,6 +49,15 @@ Run the following command to start the entire stack:
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### High Availability Tempo
|
||||
|
||||
The default `docker-compose.yml` is the single-node stack.
|
||||
If you need the Kafka-backed HA Tempo configuration, start it with:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-example-for-rustfs.yml -f docker-compose-tempo-ha-override.yml up -d
|
||||
```
|
||||
|
||||
### Access Dashboards
|
||||
|
||||
| Service | URL | Credentials | Description |
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
- **Jaeger** (v1.59.0): 分布式追踪系统(配置为辅助 UI/存储)。
|
||||
- **OpenTelemetry Collector** (v0.104.0): 接收、处理和导出遥测数据的供应商无关实现。
|
||||
|
||||
默认情况下,这套技术栈使用 Tempo 单二进制模式,不依赖 Kafka/Redpanda。
|
||||
如果需要基于 Kafka 的 HA Tempo 路径,请使用 `docker-compose-example-for-rustfs.yml` 配合 `docker-compose-tempo-ha-override.yml`。
|
||||
|
||||
## 架构
|
||||
|
||||
1. **遥测收集**: 应用程序将 OTLP (OpenTelemetry Protocol) 数据(指标、日志、追踪)发送到 **OpenTelemetry Collector**。
|
||||
@@ -46,6 +49,15 @@
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Tempo 高可用模式
|
||||
|
||||
默认的 `docker-compose.yml` 对应单机栈。
|
||||
如果需要基于 Kafka 的 HA Tempo 配置,请使用:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-example-for-rustfs.yml -f docker-compose-tempo-ha-override.yml up -d
|
||||
```
|
||||
|
||||
### 访问仪表盘
|
||||
|
||||
| 服务 | URL | 凭据 | 描述 |
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
# 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.
|
||||
|
||||
services:
|
||||
rustfs:
|
||||
security_opt:
|
||||
- "no-new-privileges:true"
|
||||
image: rustfs/rustfs:latest
|
||||
container_name: rustfs-server
|
||||
ports:
|
||||
- "9000:9000" # S3 API port
|
||||
- "9001:9001" # Console port
|
||||
environment:
|
||||
- RUSTFS_VOLUMES=/data/rustfs
|
||||
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_CORS_ALLOWED_ORIGINS=*
|
||||
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
|
||||
- RUSTFS_ACCESS_KEY=rustfsadmin
|
||||
- RUSTFS_SECRET_KEY=rustfsadmin
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=info
|
||||
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4318
|
||||
- RUSTFS_OBS_PROFILING_ENDPOINT=http://pyroscope:4040
|
||||
volumes:
|
||||
- rustfs-data:/data/rustfs
|
||||
networks:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"sh",
|
||||
"-c",
|
||||
"curl -f http://127.0.0.1:9000/health && curl -f http://127.0.0.1:9001/rustfs/console/health",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
depends_on:
|
||||
otel-collector:
|
||||
condition: service_started
|
||||
|
||||
rustfs-init:
|
||||
image: alpine
|
||||
container_name: rustfs-init
|
||||
volumes:
|
||||
- rustfs-data:/data
|
||||
networks:
|
||||
- otel-network
|
||||
command: >
|
||||
sh -c "
|
||||
chown -R 10001:10001 /data &&
|
||||
echo 'Volume Permissions fixed' &&
|
||||
exit 0
|
||||
"
|
||||
restart: no
|
||||
|
||||
# --- Tracing ---
|
||||
|
||||
tempo:
|
||||
image: grafana/tempo:latest
|
||||
container_name: tempo
|
||||
command: [ "-config.file=/etc/tempo.yaml" ]
|
||||
volumes:
|
||||
- ./tempo.yaml:/etc/tempo.yaml:ro
|
||||
- tempo-data:/var/tempo
|
||||
ports:
|
||||
- "3200:3200" # tempo
|
||||
- "4317" # otlp grpc
|
||||
- "4318" # otlp http
|
||||
networks:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: [ "CMD", "/tempo", "-version" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
|
||||
redpanda:
|
||||
image: redpandadata/redpanda:latest # for tempo ingest
|
||||
container_name: redpanda
|
||||
ports:
|
||||
- "9092:9092"
|
||||
networks:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
command: >
|
||||
redpanda start --overprovisioned
|
||||
--mode=dev-container
|
||||
--kafka-addr=PLAINTEXT://0.0.0.0:9092
|
||||
--advertise-kafka-addr=PLAINTEXT://redpanda:9092
|
||||
|
||||
jaeger:
|
||||
image: jaegertracing/jaeger:latest
|
||||
container_name: jaeger
|
||||
environment:
|
||||
- SPAN_STORAGE_TYPE=badger
|
||||
- BADGER_EPHEMERAL=false
|
||||
- BADGER_DIRECTORY_VALUE=/badger/data
|
||||
- BADGER_DIRECTORY_KEY=/badger/key
|
||||
- COLLECTOR_OTLP_ENABLED=true
|
||||
volumes:
|
||||
- ./jaeger.yaml:/etc/jaeger/config.yml
|
||||
- jaeger-data:/badger
|
||||
ports:
|
||||
- "16686:16686" # Web UI
|
||||
- "14269:14269" # Admin/Metrics
|
||||
- "4317" # otlp grpc
|
||||
- "4318" # otlp http
|
||||
command: [ "--config", "/etc/jaeger/config.yml" ]
|
||||
networks:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:14269" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
|
||||
# --- Metrics ---
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- prometheus-data:/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--web.enable-otlp-receiver" # Enable OTLP
|
||||
- "--web.enable-remote-write-receiver" # Enable remote write
|
||||
- "--enable-feature=promql-experimental-functions" # Enable info()
|
||||
- "--storage.tsdb.retention.time=30d"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- otel-network
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:9090/-/healthy" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
# --- Logging ---
|
||||
|
||||
loki:
|
||||
image: grafana/loki:latest
|
||||
container_name: loki
|
||||
volumes:
|
||||
- ./loki.yaml:/etc/loki/loki.yaml:ro
|
||||
- loki-data:/loki
|
||||
ports:
|
||||
- "3100:3100"
|
||||
command: -config.file=/etc/loki/loki.yaml
|
||||
networks:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3100/ready" ]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
|
||||
# --- Collection ---
|
||||
|
||||
otel-collector:
|
||||
image: otel/opentelemetry-collector-contrib:latest
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml:ro
|
||||
ports:
|
||||
- "1888:1888" # pprof
|
||||
- "8888:8888" # Prometheus metrics for Collector
|
||||
- "8889:8889" # Prometheus metrics for application indicators
|
||||
- "13133:13133" # health check
|
||||
- "4317:4317" # OTLP gRPC
|
||||
- "4318:4318" # OTLP HTTP
|
||||
- "55679:55679" # zpages
|
||||
networks:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- tempo
|
||||
- jaeger
|
||||
- prometheus
|
||||
- loki
|
||||
healthcheck:
|
||||
test: [ "CMD", "/otelcol-contrib", "--version" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
# --- Profiles ---
|
||||
|
||||
pyroscope:
|
||||
image: grafana/pyroscope:latest
|
||||
container_name: pyroscope
|
||||
ports:
|
||||
- "4040:4040"
|
||||
command:
|
||||
- -self-profiling.disable-push=true
|
||||
networks:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
|
||||
# --- Visualization ---
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
- GF_SECURITY_ADMIN_USER=admin
|
||||
volumes:
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- ./grafana/dashboards:/etc/grafana/dashboards:ro
|
||||
- grafana-data:/var/lib/grafana
|
||||
networks:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- prometheus
|
||||
- tempo
|
||||
- loki
|
||||
healthcheck:
|
||||
test:
|
||||
[ "CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
rustfs-data:
|
||||
tempo-data:
|
||||
jaeger-data:
|
||||
prometheus-data:
|
||||
loki-data:
|
||||
grafana-data:
|
||||
|
||||
networks:
|
||||
otel-network:
|
||||
driver: bridge
|
||||
name: "network_otel"
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.28.0.0/16
|
||||
driver_opts:
|
||||
com.docker.network.enable_ipv6: "true"
|
||||
@@ -0,0 +1,62 @@
|
||||
# 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.
|
||||
|
||||
# Docker Compose override file for High Availability Tempo setup
|
||||
#
|
||||
# Usage:
|
||||
# docker-compose -f docker-compose-example-for-rustfs.yml \
|
||||
# -f docker-compose-tempo-ha-override.yml up
|
||||
|
||||
services:
|
||||
# Override Tempo to use high-availability configuration
|
||||
tempo:
|
||||
volumes:
|
||||
- ./tempo-ha.yaml:/etc/tempo.yaml:ro
|
||||
- tempo-data:/var/tempo
|
||||
ports:
|
||||
- "3200:3200" # Tempo HTTP
|
||||
- "4317:4317" # OTLP gRPC
|
||||
- "4318:4318" # OTLP HTTP
|
||||
- "7946:7946" # Memberlist
|
||||
- "14250:14250" # Jaeger gRPC
|
||||
- "14268:14268" # Jaeger Thrift HTTP
|
||||
- "9411:9411" # Zipkin
|
||||
environment:
|
||||
- TEMPO_MEMBERLIST_BIND_PORT=7946
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3200/ready" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
depends_on:
|
||||
- redpanda
|
||||
|
||||
volumes:
|
||||
tempo-data:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: tmpfs
|
||||
device: tmpfs
|
||||
o: "size=4g" # Allocate 4GB tmpfs for Tempo data (adjust based on your needs)
|
||||
|
||||
# Network configuration remains the same
|
||||
# networks:
|
||||
# otel-network:
|
||||
# driver: bridge
|
||||
# name: "network_otel"
|
||||
# ipam:
|
||||
# config:
|
||||
# - subnet: 172.28.0.0/16
|
||||
|
||||
@@ -13,99 +13,41 @@
|
||||
# limitations under the License.
|
||||
|
||||
services:
|
||||
rustfs:
|
||||
security_opt:
|
||||
- "no-new-privileges:true"
|
||||
image: rustfs/rustfs:latest
|
||||
container_name: rustfs-server
|
||||
ports:
|
||||
- "9000:9000" # S3 API port
|
||||
- "9001:9001" # Console port
|
||||
environment:
|
||||
- RUSTFS_VOLUMES=/data/rustfs
|
||||
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_CORS_ALLOWED_ORIGINS=*
|
||||
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
|
||||
- RUSTFS_ACCESS_KEY=rustfsadmin
|
||||
- RUSTFS_SECRET_KEY=rustfsadmin
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=info
|
||||
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4318
|
||||
volumes:
|
||||
- rustfs-data:/data/rustfs
|
||||
networks:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"sh",
|
||||
"-c",
|
||||
"curl -f http://127.0.0.1:9000/health && curl -f http://127.0.0.1:9001/rustfs/console/health",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
depends_on:
|
||||
otel-collector:
|
||||
condition: service_started
|
||||
|
||||
rustfs-init:
|
||||
image: alpine
|
||||
container_name: rustfs-init
|
||||
volumes:
|
||||
- rustfs-data:/data
|
||||
networks:
|
||||
- otel-network
|
||||
command: >
|
||||
sh -c "
|
||||
chown -R 10001:10001 /data &&
|
||||
echo 'Volume Permissions fixed' &&
|
||||
exit 0
|
||||
"
|
||||
restart: no
|
||||
|
||||
# --- Tracing ---
|
||||
|
||||
tempo:
|
||||
image: grafana/tempo:latest
|
||||
image: grafana/tempo:2.10.5
|
||||
container_name: tempo
|
||||
command: ["-config.file=/etc/tempo.yaml"]
|
||||
command: [ "-config.file=/etc/tempo.yaml" ]
|
||||
volumes:
|
||||
- ./tempo.yaml:/etc/tempo.yaml:ro
|
||||
- tempo-data:/var/tempo
|
||||
ports:
|
||||
- "3200:3200" # tempo
|
||||
- "4317" # otlp grpc
|
||||
- "4318" # otlp http
|
||||
- "4317" # otlp grpc
|
||||
- "4318" # otlp http
|
||||
- "7946" # memberlist
|
||||
networks:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- redpanda
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3200/ready"]
|
||||
test: [ "CMD", "/tempo", "-version" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
|
||||
redpanda:
|
||||
image: redpandadata/redpanda:latest # for tempo ingest
|
||||
container_name: redpanda
|
||||
ports:
|
||||
- "9092:9092"
|
||||
networks:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
command: >
|
||||
redpanda start --overprovisioned
|
||||
--mode=dev-container
|
||||
--kafka-addr=PLAINTEXT://0.0.0.0:9092
|
||||
--advertise-kafka-addr=PLAINTEXT://redpanda:9092
|
||||
vulture:
|
||||
image: grafana/tempo-vulture:latest
|
||||
restart: always
|
||||
command:
|
||||
[
|
||||
"-prometheus-listen-address=:8080",
|
||||
"-tempo-query-url=http://tempo:3200",
|
||||
"-tempo-push-url=http://tempo:4317",
|
||||
]
|
||||
depends_on:
|
||||
- tempo
|
||||
|
||||
jaeger:
|
||||
image: jaegertracing/jaeger:latest
|
||||
@@ -124,12 +66,12 @@ services:
|
||||
- "14269:14269" # Admin/Metrics
|
||||
- "4317" # otlp grpc
|
||||
- "4318" # otlp http
|
||||
command: ["--config", "/etc/jaeger/config.yml"]
|
||||
command: [ "--config", "/etc/jaeger/config.yml" ]
|
||||
networks:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:14269"]
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:14269" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
@@ -142,6 +84,7 @@ services:
|
||||
container_name: prometheus
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ./prometheus-rules:/etc/prometheus/rules:ro
|
||||
- prometheus-data:/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
@@ -155,7 +98,7 @@ services:
|
||||
networks:
|
||||
- otel-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:9090/-/healthy"]
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:9090/-/healthy" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
@@ -175,7 +118,7 @@ services:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3100/ready"]
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3100/ready" ]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
@@ -188,12 +131,12 @@ services:
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml:ro
|
||||
ports:
|
||||
- "1888:1888" # pprof
|
||||
- "8888:8888" # Prometheus metrics for Collector
|
||||
- "8889:8889" # Prometheus metrics for application indicators
|
||||
- "1888:1888" # pprof
|
||||
- "8888:8888" # Prometheus metrics for Collector
|
||||
- "8889:8889" # Prometheus metrics for application indicators
|
||||
- "13133:13133" # health check
|
||||
- "4317:4317" # OTLP gRPC
|
||||
- "4318:4318" # OTLP HTTP
|
||||
- "4317:4317" # OTLP gRPC
|
||||
- "4318:4318" # OTLP HTTP
|
||||
- "55679:55679" # zpages
|
||||
networks:
|
||||
- otel-network
|
||||
@@ -204,11 +147,24 @@ services:
|
||||
- prometheus
|
||||
- loki
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:13133"]
|
||||
test: [ "CMD", "/otelcol-contrib", "--version" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
# --- Profiles ---
|
||||
|
||||
pyroscope:
|
||||
image: grafana/pyroscope:latest
|
||||
container_name: pyroscope
|
||||
ports:
|
||||
- "4040:4040"
|
||||
command:
|
||||
- -self-profiling.disable-push=true
|
||||
networks:
|
||||
- otel-network
|
||||
restart: unless-stopped
|
||||
|
||||
# --- Visualization ---
|
||||
|
||||
grafana:
|
||||
@@ -231,13 +187,13 @@ services:
|
||||
- tempo
|
||||
- loki
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
|
||||
test:
|
||||
[ "CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
rustfs-data:
|
||||
tempo-data:
|
||||
jaeger-data:
|
||||
prometheus-data:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
|
||||
@@ -1,97 +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.
|
||||
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
uid: prometheus
|
||||
access: proxy
|
||||
orgId: 1
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
version: 1
|
||||
editable: false
|
||||
jsonData:
|
||||
httpMethod: GET
|
||||
exemplarTraceIdDestinations:
|
||||
- name: trace_id
|
||||
datasourceUid: tempo
|
||||
|
||||
- name: Tempo
|
||||
type: tempo
|
||||
uid: tempo
|
||||
access: proxy
|
||||
orgId: 1
|
||||
url: http://tempo:3200
|
||||
isDefault: false
|
||||
version: 1
|
||||
editable: false
|
||||
jsonData:
|
||||
httpMethod: GET
|
||||
serviceMap:
|
||||
datasourceUid: prometheus
|
||||
tracesToLogs:
|
||||
datasourceUid: loki
|
||||
tags: [ 'job', 'instance', 'pod', 'namespace', 'service.name' ]
|
||||
mappedTags: [ { key: 'service.name', value: 'app' } ]
|
||||
spanStartTimeShift: '1s'
|
||||
spanEndTimeShift: '-1s'
|
||||
filterByTraceID: true
|
||||
filterBySpanID: false
|
||||
tracesToMetrics:
|
||||
datasourceUid: prometheus
|
||||
tags: [ { key: 'service.name' }, { key: 'job' } ]
|
||||
queries:
|
||||
- name: 'Service-Level Latency'
|
||||
query: 'sum(rate(traces_spanmetrics_latency_bucket{$$__tags}[5m])) by (le)'
|
||||
- name: 'Service-Level Calls'
|
||||
query: 'sum(rate(traces_spanmetrics_calls_total{$$__tags}[5m]))'
|
||||
- name: 'Service-Level Errors'
|
||||
query: 'sum(rate(traces_spanmetrics_calls_total{status_code="ERROR", $$__tags}[5m]))'
|
||||
nodeGraph:
|
||||
enabled: true
|
||||
|
||||
- name: Loki
|
||||
type: loki
|
||||
uid: loki
|
||||
orgId: 1
|
||||
url: http://loki:3100
|
||||
isDefault: false
|
||||
version: 1
|
||||
editable: false
|
||||
jsonData:
|
||||
derivedFields:
|
||||
- datasourceUid: tempo
|
||||
matcherRegex: 'trace_id=(\w+)'
|
||||
name: 'TraceID'
|
||||
url: '$${__value.raw}'
|
||||
|
||||
- name: Jaeger
|
||||
type: jaeger
|
||||
uid: jaeger
|
||||
url: http://jaeger:16686
|
||||
access: proxy
|
||||
isDefault: false
|
||||
editable: false
|
||||
jsonData:
|
||||
tracesToLogs:
|
||||
datasourceUid: loki
|
||||
tags: [ 'job', 'instance', 'pod', 'namespace', 'service.name' ]
|
||||
mappedTags: [ { key: 'service.name', value: 'app' } ]
|
||||
spanStartTimeShift: '1s'
|
||||
spanEndTimeShift: '-1s'
|
||||
filterByTraceID: true
|
||||
filterBySpanID: false
|
||||
@@ -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.
|
||||
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
@@ -76,3 +90,9 @@ datasources:
|
||||
spanEndTimeShift: '-1s'
|
||||
filterByTraceID: true
|
||||
filterBySpanID: false
|
||||
|
||||
- name: Pyroscope
|
||||
type: grafana-pyroscope-datasource
|
||||
url: http://pyroscope:4040
|
||||
jsonData:
|
||||
minStep: '15s'
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
groups:
|
||||
- name: rustfs-dashboard
|
||||
interval: 30s
|
||||
rules:
|
||||
- record: rustfs:http_server_requests:rate5m
|
||||
expr: sum by (job) (rate(rustfs_http_server_requests_total[5m]))
|
||||
|
||||
- record: rustfs:http_server_request_duration_seconds:p50_5m
|
||||
expr: histogram_quantile(0.50, sum by (le, job) (rate(rustfs_http_server_request_duration_seconds_bucket[5m])))
|
||||
- record: rustfs:http_server_request_duration_seconds:p95_5m
|
||||
expr: histogram_quantile(0.95, sum by (le, job) (rate(rustfs_http_server_request_duration_seconds_bucket[5m])))
|
||||
- record: rustfs:http_server_request_duration_seconds:p99_5m
|
||||
expr: histogram_quantile(0.99, sum by (le, job) (rate(rustfs_http_server_request_duration_seconds_bucket[5m])))
|
||||
|
||||
- record: rustfs:http_server_response_body_size_bytes:p50_5m
|
||||
expr: histogram_quantile(0.50, sum by (le, job) (rate(rustfs_http_server_response_body_size_bytes_bucket[5m])))
|
||||
- record: rustfs:http_server_response_body_size_bytes:p95_5m
|
||||
expr: histogram_quantile(0.95, sum by (le, job) (rate(rustfs_http_server_response_body_size_bytes_bucket[5m])))
|
||||
- record: rustfs:http_server_response_body_size_bytes:p99_5m
|
||||
expr: histogram_quantile(0.99, sum by (le, job) (rate(rustfs_http_server_response_body_size_bytes_bucket[5m])))
|
||||
|
||||
- record: rustfs:log_cleaner_runs:rate15m
|
||||
expr: sum by (job) (rate(rustfs_log_cleaner_runs_total[15m]))
|
||||
- record: rustfs:log_cleaner_failure_ratio:rate5m
|
||||
expr: sum by (job) (rate(rustfs_log_cleaner_run_failures_total[5m])) / clamp_min(sum by (job) (rate(rustfs_log_cleaner_runs_total[5m])), 1e-9)
|
||||
- record: rustfs:log_cleaner_rotation_failure_ratio:rate5m
|
||||
expr: sum by (job) (rate(rustfs_log_cleaner_rotation_failures_total[5m])) / clamp_min(sum by (job) (rate(rustfs_log_cleaner_rotation_total[5m])), 1e-9)
|
||||
- record: rustfs:log_cleaner_rotation_duration_seconds:p95_5m
|
||||
expr: histogram_quantile(0.95, sum by (le, job) (rate(rustfs_log_cleaner_rotation_duration_seconds_bucket[5m])))
|
||||
- record: rustfs:log_cleaner_compress_duration_seconds:p95_5m
|
||||
expr: histogram_quantile(0.95, sum by (le, job) (rate(rustfs_log_cleaner_compress_duration_seconds_bucket[5m])))
|
||||
|
||||
- record: rustfs:scanner_objects_scanned:rate5m
|
||||
expr: sum by (job) (rate(rustfs_scanner_objects_scanned_total[5m]))
|
||||
- record: rustfs:scanner_directories_scanned:rate5m
|
||||
expr: sum by (job) (rate(rustfs_scanner_directories_scanned_total[5m]))
|
||||
- record: rustfs:scanner_buckets_scanned:rate5m
|
||||
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]))
|
||||
@@ -19,6 +19,9 @@ global:
|
||||
cluster: 'rustfs-dev' # Label to identify the cluster
|
||||
replica: '1' # Replica identifier
|
||||
|
||||
rule_files:
|
||||
- /etc/prometheus/rules/*.yml
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'otel-collector'
|
||||
static_configs:
|
||||
@@ -50,6 +53,11 @@ scrape_configs:
|
||||
static_configs:
|
||||
- targets: [ 'localhost:9090' ]
|
||||
|
||||
- job_name: 'vulture'
|
||||
static_configs:
|
||||
- targets:
|
||||
- 'vulture:8080'
|
||||
|
||||
otlp:
|
||||
promote_resource_attributes:
|
||||
- service.instance.id
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
kafka:
|
||||
brokers:
|
||||
- redpanda:9092
|
||||
@@ -0,0 +1,286 @@
|
||||
# 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.
|
||||
|
||||
# High Availability Tempo Configuration for docker-compose-example-for-rustfs.yml
|
||||
# Features:
|
||||
# - Distributed architecture with multiple components
|
||||
# - Kafka-based ingestion for fault tolerance
|
||||
# - Replication factor of 3 for data resilience
|
||||
# - Query frontend for load balancing
|
||||
# - Metrics generation from traces
|
||||
# - WAL for durability
|
||||
|
||||
partition_ring_live_store: true
|
||||
stream_over_http_enabled: true
|
||||
|
||||
server:
|
||||
http_listen_port: 3200
|
||||
http_server_read_timeout: 30s
|
||||
http_server_write_timeout: 30s
|
||||
grpc_server_max_recv_msg_size: 4194304 # 4MB
|
||||
grpc_server_max_send_msg_size: 4194304
|
||||
log_level: info
|
||||
log_format: json
|
||||
|
||||
# Memberlist configuration for distributed mode
|
||||
memberlist:
|
||||
node_name: tempo
|
||||
bind_port: 7946
|
||||
join_members:
|
||||
- tempo:7946
|
||||
retransmit_factor: 4
|
||||
node_timeout: 15s
|
||||
retransmit_interval: 300ms
|
||||
dead_node_reclaim_time: 30s
|
||||
|
||||
# Distributor configuration - receives traces and routes to ingesters
|
||||
distributor:
|
||||
ingester_write_path_enabled: true
|
||||
kafka_write_path_enabled: true
|
||||
rate_limit_bytes: 10MB
|
||||
rate_limit_enabled: true
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: "0.0.0.0:4317"
|
||||
max_concurrent_streams: 0
|
||||
max_receive_message_size: 4194304
|
||||
http:
|
||||
endpoint: "0.0.0.0:4318"
|
||||
cors:
|
||||
allowed_origins:
|
||||
- "*"
|
||||
max_age: 86400
|
||||
jaeger:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: "0.0.0.0:14250"
|
||||
thrift_http:
|
||||
endpoint: "0.0.0.0:14268"
|
||||
zipkin:
|
||||
endpoint: "0.0.0.0:9411"
|
||||
ring:
|
||||
kvstore:
|
||||
store: memberlist
|
||||
heartbeat_timeout: 5s
|
||||
replication_factor: 3
|
||||
heartbeat_interval: 5s
|
||||
|
||||
# Ingester configuration - stores traces and querying
|
||||
ingester:
|
||||
lifecycler:
|
||||
address: tempo
|
||||
ring:
|
||||
kvstore:
|
||||
store: memberlist
|
||||
replication_factor: 3
|
||||
max_cache_freshness_per_sec: 10s
|
||||
heartbeat_interval: 5s
|
||||
heartbeat_timeout: 5s
|
||||
num_tokens: 128
|
||||
tokens_file_path: /var/tempo/tokens.json
|
||||
claim_on_rollout: true
|
||||
trace_idle_period: 20s
|
||||
max_block_bytes: 10_000_000
|
||||
max_block_duration: 10m
|
||||
chunk_size_bytes: 1_000_000
|
||||
chunk_encoding: snappy
|
||||
wal:
|
||||
checkpoint_duration: 5s
|
||||
max_wal_blocks: 4
|
||||
metrics:
|
||||
enabled: true
|
||||
level: block
|
||||
target_info_duration: 15m
|
||||
|
||||
# WAL configuration for data durability
|
||||
wal:
|
||||
checkpoint_duration: 5s
|
||||
flush_on_shutdown: true
|
||||
path: /var/tempo/wal
|
||||
|
||||
# Kafka ingestion configuration - for high throughput scenarios
|
||||
ingest:
|
||||
enabled: true
|
||||
kafka:
|
||||
brokers: [ redpanda:9092 ]
|
||||
topic: tempo-ingest
|
||||
encoding: protobuf
|
||||
consumer_group: tempo-ingest-consumer
|
||||
session_timeout: 10s
|
||||
rebalance_timeout: 1m
|
||||
partition: auto
|
||||
verbosity: 2
|
||||
|
||||
# Query frontend configuration - distributed querying
|
||||
query_frontend:
|
||||
compression: gzip
|
||||
downstream_url: http://localhost:3200
|
||||
log_queries_longer_than: 5s
|
||||
cache_uncompressed_bytes: 100MB
|
||||
max_outstanding_requests_per_tenant: 100
|
||||
max_query_length: 48h
|
||||
max_query_lookback: 30d
|
||||
default_result_cache_ttl: 1m
|
||||
result_cache:
|
||||
cache:
|
||||
enable_fifocache: true
|
||||
default_validity: 1m
|
||||
rf1_after: "1999-01-01T00:00:00Z"
|
||||
mcp_server:
|
||||
enabled: true
|
||||
|
||||
# Querier configuration - queries traces
|
||||
querier:
|
||||
frontend_worker:
|
||||
frontend_address: localhost:3200
|
||||
grpc_client_config:
|
||||
max_recv_msg_size: 104857600
|
||||
max_concurrent_queries: 20
|
||||
max_metric_bytes_per_trace: 1MB
|
||||
|
||||
# Query scheduler configuration - for distributed querying
|
||||
query_scheduler:
|
||||
use_scheduler_ring: false
|
||||
|
||||
# Metrics generator configuration - generates metrics from traces
|
||||
metrics_generator:
|
||||
enabled: true
|
||||
registry:
|
||||
enabled: true
|
||||
external_labels:
|
||||
source: tempo
|
||||
cluster: rustfs-docker-ha
|
||||
environment: production
|
||||
storage:
|
||||
path: /var/tempo/generator/wal
|
||||
remote_write:
|
||||
- url: http://prometheus:9090/api/v1/write
|
||||
send_exemplars: true
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
processor:
|
||||
batch:
|
||||
timeout: 10s
|
||||
send_batch_size: 1024
|
||||
memory_limiter:
|
||||
check_interval: 5s
|
||||
limit_mib: 512
|
||||
spike_limit_mib: 128
|
||||
processors:
|
||||
- span-metrics
|
||||
- local-blocks
|
||||
- service-graphs
|
||||
generate_native_histograms: both
|
||||
|
||||
# Backend worker configuration
|
||||
backend_worker:
|
||||
backend_scheduler_addr: localhost:3200
|
||||
compaction:
|
||||
block_retention: 24h
|
||||
compacted_block_retention: 1h
|
||||
ring:
|
||||
kvstore:
|
||||
store: memberlist
|
||||
|
||||
# Backend scheduler configuration
|
||||
backend_scheduler:
|
||||
enabled: true
|
||||
provider:
|
||||
compaction:
|
||||
compaction:
|
||||
block_retention: 24h
|
||||
compacted_block_retention: 1h
|
||||
concurrency: 25
|
||||
v2_out_path: /var/tempo/blocks/compaction
|
||||
|
||||
# Storage configuration - local backend with proper retention
|
||||
storage:
|
||||
trace:
|
||||
backend: local
|
||||
wal:
|
||||
path: /var/tempo/wal
|
||||
checkpoint_duration: 5s
|
||||
flush_on_shutdown: true
|
||||
local:
|
||||
path: /var/tempo/blocks
|
||||
bloom_filter_false_positive: 0.05
|
||||
bloom_shift: 4
|
||||
index:
|
||||
downsample_bytes: 1000000
|
||||
page_size_bytes: 0
|
||||
cache_size_bytes: 0
|
||||
pool:
|
||||
max_workers: 400
|
||||
queue_depth: 10000
|
||||
|
||||
# Compactor configuration - manages block compaction
|
||||
compactor:
|
||||
compaction:
|
||||
block_retention: 168h # 7 days
|
||||
compacted_block_retention: 1h
|
||||
concurrency: 25
|
||||
v2_out_path: /var/tempo/blocks/compaction
|
||||
shard_count: 32
|
||||
max_block_bytes: 107374182400 # 100GB
|
||||
max_compaction_objects: 6000000
|
||||
max_time_per_tenant: 5m
|
||||
block_size_bytes: 107374182400
|
||||
ring:
|
||||
kvstore:
|
||||
store: memberlist
|
||||
heartbeat_interval: 5s
|
||||
heartbeat_timeout: 5s
|
||||
|
||||
# Limits configuration - rate limiting and quotas
|
||||
limits:
|
||||
max_traces_per_user: 10000
|
||||
max_bytes_per_trace: 10485760 # 10MB
|
||||
max_search_bytes_per_trace: 0
|
||||
forgiving_oversize_traces: true
|
||||
rate_limit_bytes: 10MB
|
||||
rate_limit_enabled: true
|
||||
ingestion_burst_size_bytes: 20MB
|
||||
ingestion_rate_limit_bytes: 10MB
|
||||
max_bytes_per_second: 10485760
|
||||
metrics_generator_max_active_series: 10000
|
||||
metrics_generator_max_churned_series: 10000
|
||||
metrics_generator_forta_out_of_order_ttl: 5m
|
||||
|
||||
# Override configuration
|
||||
overrides:
|
||||
defaults:
|
||||
metrics_generator:
|
||||
processors:
|
||||
- span-metrics
|
||||
- local-blocks
|
||||
- service-graphs
|
||||
generate_native_histograms: both
|
||||
max_active_series: 10000
|
||||
max_churned_series: 10000
|
||||
|
||||
# Usage reporting configuration
|
||||
usage_report:
|
||||
reporting_enabled: false
|
||||
|
||||
# Tracing configuration for debugging
|
||||
tracing:
|
||||
enabled: true
|
||||
jaeger:
|
||||
sampler:
|
||||
name: probabilistic
|
||||
param: 0.1
|
||||
reporter_log_spans: false
|
||||
|
||||
@@ -12,50 +12,37 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
partition_ring_live_store: true
|
||||
stream_over_http_enabled: true
|
||||
|
||||
server:
|
||||
http_listen_port: 3200
|
||||
log_level: info
|
||||
|
||||
memberlist:
|
||||
node_name: tempo
|
||||
bind_port: 7946
|
||||
join_members:
|
||||
- tempo:7946
|
||||
|
||||
distributor:
|
||||
ingester_write_path_enabled: false
|
||||
kafka_write_path_enabled: true
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: "tempo:4317"
|
||||
endpoint: "0.0.0.0:4317"
|
||||
http:
|
||||
endpoint: "tempo:4318"
|
||||
#log_received_spans:
|
||||
# enabled: true
|
||||
# log_discarded_spans:
|
||||
# enabled: true
|
||||
endpoint: "0.0.0.0:4318"
|
||||
|
||||
backend_scheduler:
|
||||
provider:
|
||||
compaction:
|
||||
compaction:
|
||||
block_retention: 1h
|
||||
|
||||
backend_worker:
|
||||
backend_scheduler_addr: localhost:3200
|
||||
compaction:
|
||||
block_retention: 1h
|
||||
ring:
|
||||
kvstore:
|
||||
store: memberlist
|
||||
|
||||
querier:
|
||||
query_live_store: true
|
||||
ingester:
|
||||
max_block_duration: 5m
|
||||
|
||||
metrics_generator:
|
||||
registry:
|
||||
external_labels:
|
||||
source: tempo
|
||||
cluster: docker-compose
|
||||
traces_storage:
|
||||
path: /var/tempo/generator/traces
|
||||
storage:
|
||||
path: /var/tempo/generator/wal
|
||||
remote_write:
|
||||
@@ -71,24 +58,15 @@ storage:
|
||||
trace:
|
||||
backend: local
|
||||
wal:
|
||||
path: /var/tempo/wal
|
||||
path: /var/tempo/wal # where to store the wal locally
|
||||
local:
|
||||
path: /var/tempo/blocks
|
||||
path: /var/tempo/blocks # where to store the traces locally
|
||||
|
||||
overrides:
|
||||
defaults:
|
||||
metrics_generator:
|
||||
processors: ["span-metrics", "service-graphs", "local-blocks"]
|
||||
processors: [ "span-metrics", "service-graphs", "local-blocks" ]
|
||||
generate_native_histograms: both
|
||||
|
||||
ingest:
|
||||
enabled: true
|
||||
kafka:
|
||||
address: redpanda:9092
|
||||
topic: tempo-ingest
|
||||
|
||||
block_builder:
|
||||
consume_cycle_duration: 30s
|
||||
|
||||
usage_report:
|
||||
reporting_enabled: false
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
services:
|
||||
rustfs:
|
||||
image: rustfs/rustfs:1.0.0-alpha.99-glibc
|
||||
container_name: rustfs-issue-2715-test
|
||||
security_opt:
|
||||
- "no-new-privileges:true"
|
||||
ports:
|
||||
- "19000:9000"
|
||||
- "19001:9001"
|
||||
environment:
|
||||
- RUSTFS_VOLUMES=/data/rustfs{0...8}
|
||||
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
|
||||
- RUSTFS_CONSOLE_ENABLE=true
|
||||
- RUSTFS_CORS_ALLOWED_ORIGINS=*
|
||||
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
|
||||
- RUSTFS_ACCESS_KEY=admin
|
||||
- RUSTFS_SECRET_KEY=admin
|
||||
- RUSTFS_OBS_LOGGER_LEVEL=info
|
||||
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4318
|
||||
- RUSTFS_OBS_PROFILING_ENDPOINT=http://pyroscope:4040
|
||||
- RUSTFS_STORAGE_CLASS_STANDARD=EC:2
|
||||
- RUSTFS_STORAGE_CLASS_RRS=EC:1
|
||||
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true
|
||||
- RUSTFS_OBS_LOG_DIRECTORY=/opt/rustfs/logs
|
||||
extra_hosts:
|
||||
- "otel-collector:host-gateway"
|
||||
- "pyroscope:host-gateway"
|
||||
volumes:
|
||||
- ./deploy/data/issue-2715/rustfs0:/data/rustfs0
|
||||
- ./deploy/data/issue-2715/rustfs1:/data/rustfs1
|
||||
- ./deploy/data/issue-2715/rustfs2:/data/rustfs2
|
||||
- ./deploy/data/issue-2715/rustfs3:/data/rustfs3
|
||||
- ./deploy/data/issue-2715/rustfs4:/data/rustfs4
|
||||
- ./deploy/data/issue-2715/rustfs5:/data/rustfs5
|
||||
- ./deploy/data/issue-2715/rustfs6:/data/rustfs6
|
||||
- ./deploy/data/issue-2715/rustfs7:/data/rustfs7
|
||||
- ./deploy/data/issue-2715/rustfs8:/data/rustfs8
|
||||
- ./deploy/logs/issue-2715:/opt/rustfs/logs
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"sh",
|
||||
"-c",
|
||||
"curl -f http://127.0.0.1:9000/health && curl -f http://127.0.0.1:9001/rustfs/console/health"
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
@@ -0,0 +1 @@
|
||||
data/
|
||||
@@ -0,0 +1,106 @@
|
||||
# Issue 2815 Local Docker Verification
|
||||
|
||||
## Purpose
|
||||
|
||||
This directory contains the local distributed Docker verification assets used to validate issue `#2815` against the current source build.
|
||||
|
||||
The target behavior is:
|
||||
|
||||
- 4-node distributed cluster starts successfully
|
||||
- `/health/ready` becomes reachable on each node
|
||||
- logs no longer contain `storage_info failed: Io error: wrong msgpack marker FixArray(1)`
|
||||
- internode RPC authentication succeeds with an explicit non-default RPC secret
|
||||
|
||||
## Files
|
||||
|
||||
- `docker-compose.yml`: 4-node distributed cluster using a locally built image
|
||||
|
||||
## Data Directories
|
||||
|
||||
Create the bind-mount directories before `docker compose up`:
|
||||
|
||||
```bash
|
||||
mkdir -p .docker/test/issues-2815/data/rustfs{1..4}-disk{0..3}
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Apple Silicon / arm64 host:
|
||||
|
||||
```bash
|
||||
docker build --platform linux/arm64 -f Dockerfile.source -t rustfs-issue-2815-local .
|
||||
```
|
||||
|
||||
If you intentionally want amd64 emulation:
|
||||
|
||||
```bash
|
||||
docker build --platform linux/amd64 -f Dockerfile.source -t rustfs-issue-2815-local .
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
docker compose -f .docker/test/issues-2815/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
If the image platform is not `linux/arm64`, align compose explicitly:
|
||||
|
||||
```bash
|
||||
RUSTFS_DOCKER_PLATFORM=linux/amd64 docker compose -f .docker/test/issues-2815/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
|
||||
Container-level healthcheck is now included and probes:
|
||||
|
||||
```bash
|
||||
curl -fsS http://127.0.0.1:9000/health
|
||||
```
|
||||
|
||||
Manual checks:
|
||||
|
||||
```bash
|
||||
curl -i http://127.0.0.1:9101/health/ready
|
||||
curl -i http://127.0.0.1:9102/health/ready
|
||||
curl -i http://127.0.0.1:9103/health/ready
|
||||
curl -i http://127.0.0.1:9104/health/ready
|
||||
```
|
||||
|
||||
## RPC Secret Requirement
|
||||
|
||||
The current source build no longer reproduces the original `FixArray(1)` decode error from issue `#2815`.
|
||||
|
||||
Earlier local Docker attempts failed during erasure bootstrap with:
|
||||
|
||||
```text
|
||||
No valid auth token
|
||||
store init failed to load formats after 10 retries: erasure read quorum
|
||||
```
|
||||
|
||||
Root cause:
|
||||
|
||||
- RPC authentication rejects the default secret `rustfsadmin`
|
||||
- distributed local Docker validation therefore needs an explicit non-default secret
|
||||
|
||||
This compose now sets both:
|
||||
|
||||
- `RUSTFS_SECRET_KEY=issue-2815-secret`
|
||||
- `RUSTFS_RPC_SECRET=issue-2815-rpc-secret`
|
||||
|
||||
With those values in place, the current 4-node local Docker cluster reaches healthy state and `/health/ready` returns `200`.
|
||||
|
||||
In other words:
|
||||
|
||||
- `RUSTFS_ACCESS_KEY` may still be `rustfsadmin` for local service credentials if desired
|
||||
- `RUSTFS_SECRET_KEY` can still be used for service credentials
|
||||
- but RPC authentication must not resolve to the default secret value `rustfsadmin`
|
||||
- if `RUSTFS_RPC_SECRET` is unset, the code falls back to `RUSTFS_SECRET_KEY`
|
||||
- so at least one of them must provide a non-default shared secret for internode RPC signing
|
||||
|
||||
## Suggested Debug Commands
|
||||
|
||||
```bash
|
||||
docker compose -f .docker/test/issues-2815/docker-compose.yml ps
|
||||
docker compose -f .docker/test/issues-2815/docker-compose.yml logs --no-color --tail=200
|
||||
docker compose -f .docker/test/issues-2815/docker-compose.yml down -v
|
||||
```
|
||||
@@ -0,0 +1,120 @@
|
||||
services:
|
||||
rustfs1:
|
||||
image: rustfs-issue-2815-local
|
||||
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/arm64}
|
||||
hostname: rustfs1
|
||||
container_name: rustfs-issue-2815-rustfs1
|
||||
environment:
|
||||
RUSTFS_ADDRESS: "0.0.0.0:9000"
|
||||
RUSTFS_ACCESS_KEY: "rustfsadmin"
|
||||
RUSTFS_SECRET_KEY: "issue-2815-secret"
|
||||
RUSTFS_RPC_SECRET: "issue-2815-rpc-secret"
|
||||
RUSTFS_CONSOLE_ENABLE: "false"
|
||||
RUST_LOG: "info"
|
||||
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "true"
|
||||
RUSTFS_VOLUMES: "http://rustfs{1...4}:9000/data/rustfs{0...3}"
|
||||
volumes:
|
||||
- ./data/rustfs1-disk0:/data/rustfs0
|
||||
- ./data/rustfs1-disk1:/data/rustfs1
|
||||
- ./data/rustfs1-disk2:/data/rustfs2
|
||||
- ./data/rustfs1-disk3:/data/rustfs3
|
||||
networks: [rustfs-issue-2815-net]
|
||||
ports:
|
||||
- "9101:9000"
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "curl -fsS http://127.0.0.1:9000/health || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 8
|
||||
start_period: 30s
|
||||
|
||||
rustfs2:
|
||||
image: rustfs-issue-2815-local
|
||||
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/arm64}
|
||||
hostname: rustfs2
|
||||
container_name: rustfs-issue-2815-rustfs2
|
||||
environment:
|
||||
RUSTFS_ADDRESS: "0.0.0.0:9000"
|
||||
RUSTFS_ACCESS_KEY: "rustfsadmin"
|
||||
RUSTFS_SECRET_KEY: "issue-2815-secret"
|
||||
RUSTFS_RPC_SECRET: "issue-2815-rpc-secret"
|
||||
RUSTFS_CONSOLE_ENABLE: "false"
|
||||
RUST_LOG: "info"
|
||||
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "true"
|
||||
RUSTFS_VOLUMES: "http://rustfs{1...4}:9000/data/rustfs{0...3}"
|
||||
volumes:
|
||||
- ./data/rustfs2-disk0:/data/rustfs0
|
||||
- ./data/rustfs2-disk1:/data/rustfs1
|
||||
- ./data/rustfs2-disk2:/data/rustfs2
|
||||
- ./data/rustfs2-disk3:/data/rustfs3
|
||||
networks: [rustfs-issue-2815-net]
|
||||
ports:
|
||||
- "9102:9000"
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "curl -fsS http://127.0.0.1:9000/health || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 8
|
||||
start_period: 30s
|
||||
|
||||
rustfs3:
|
||||
image: rustfs-issue-2815-local
|
||||
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/arm64}
|
||||
hostname: rustfs3
|
||||
container_name: rustfs-issue-2815-rustfs3
|
||||
environment:
|
||||
RUSTFS_ADDRESS: "0.0.0.0:9000"
|
||||
RUSTFS_ACCESS_KEY: "rustfsadmin"
|
||||
RUSTFS_SECRET_KEY: "issue-2815-secret"
|
||||
RUSTFS_RPC_SECRET: "issue-2815-rpc-secret"
|
||||
RUSTFS_CONSOLE_ENABLE: "false"
|
||||
RUST_LOG: "info"
|
||||
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "true"
|
||||
RUSTFS_VOLUMES: "http://rustfs{1...4}:9000/data/rustfs{0...3}"
|
||||
volumes:
|
||||
- ./data/rustfs3-disk0:/data/rustfs0
|
||||
- ./data/rustfs3-disk1:/data/rustfs1
|
||||
- ./data/rustfs3-disk2:/data/rustfs2
|
||||
- ./data/rustfs3-disk3:/data/rustfs3
|
||||
networks: [rustfs-issue-2815-net]
|
||||
ports:
|
||||
- "9103:9000"
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "curl -fsS http://127.0.0.1:9000/health || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 8
|
||||
start_period: 30s
|
||||
|
||||
rustfs4:
|
||||
image: rustfs-issue-2815-local
|
||||
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/arm64}
|
||||
hostname: rustfs4
|
||||
container_name: rustfs-issue-2815-rustfs4
|
||||
environment:
|
||||
RUSTFS_ADDRESS: "0.0.0.0:9000"
|
||||
RUSTFS_ACCESS_KEY: "rustfsadmin"
|
||||
RUSTFS_SECRET_KEY: "issue-2815-secret"
|
||||
RUSTFS_RPC_SECRET: "issue-2815-rpc-secret"
|
||||
RUSTFS_CONSOLE_ENABLE: "false"
|
||||
RUST_LOG: "info"
|
||||
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "true"
|
||||
RUSTFS_VOLUMES: "http://rustfs{1...4}:9000/data/rustfs{0...3}"
|
||||
volumes:
|
||||
- ./data/rustfs4-disk0:/data/rustfs0
|
||||
- ./data/rustfs4-disk1:/data/rustfs1
|
||||
- ./data/rustfs4-disk2:/data/rustfs2
|
||||
- ./data/rustfs4-disk3:/data/rustfs3
|
||||
networks: [rustfs-issue-2815-net]
|
||||
ports:
|
||||
- "9104:9000"
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "curl -fsS http://127.0.0.1:9000/health || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 8
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
rustfs-issue-2815-net:
|
||||
name: rustfs-issue-2815-net
|
||||
@@ -0,0 +1,36 @@
|
||||
# GitHub Workflow Instructions
|
||||
|
||||
Applies to `.github/` and repository pull-request operations.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- PR titles and descriptions must be in English.
|
||||
- Use `.github/pull_request_template.md` for every PR body.
|
||||
- Keep all template section headings.
|
||||
- Use `N/A` for non-applicable sections.
|
||||
- Include verification commands in the PR details.
|
||||
- For `gh pr create` and `gh pr edit`, always write markdown body to a file and pass `--body-file`.
|
||||
- Do not use multiline inline `--body`; backticks and shell expansion can corrupt content or trigger unintended commands.
|
||||
- Recommended pattern:
|
||||
- `cat > /tmp/pr_body.md <<'EOF'`
|
||||
- `...markdown...`
|
||||
- `EOF`
|
||||
- `gh pr create ... --body-file /tmp/pr_body.md`
|
||||
|
||||
## CI Alignment
|
||||
|
||||
When changing CI-sensitive behavior, keep local validation aligned with `.github/workflows/ci.yml`.
|
||||
|
||||
Current `test-and-lint` gate includes:
|
||||
|
||||
- `cargo nextest run --all --exclude e2e_test`
|
||||
- `cargo test --all --doc`
|
||||
- `cargo test -p rustfs get_object_chunk_fast_path`
|
||||
- `cargo test -p rustfs materialize_chunk_stream_before_commit`
|
||||
- `touch rustfs/build.rs`
|
||||
- `cargo build -p rustfs --bins --jobs 2`
|
||||
- `cargo test -p e2e_test archive_multipart_roundtrip_preserves_bytes`
|
||||
- `cargo test -p e2e_test presigned_get_and_reverse_proxy_preserve_multipart_bytes_with_fast_path`
|
||||
- `cargo fmt --all --check`
|
||||
- `cargo clippy --all-targets --all-features -- -D warnings`
|
||||
- `./scripts/check_layer_dependencies.sh`
|
||||
@@ -62,7 +62,7 @@ runs:
|
||||
repo-token: ${{ inputs.github-token }}
|
||||
|
||||
- name: Install flatc
|
||||
uses: Nugine/setup-flatc@v1
|
||||
uses: Nugine/setup-flatc@v1.2.4
|
||||
with:
|
||||
version: "25.9.23"
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
enabled: true
|
||||
|
||||
document:
|
||||
version: v1
|
||||
url: https://github.com/rustfs/cla/blob/main/cla/v1.md
|
||||
|
||||
signing:
|
||||
mode: comment
|
||||
comment_pattern: I have read and agree to the CLA.
|
||||
|
||||
registry:
|
||||
type: json-repo
|
||||
repository: rustfs/cla
|
||||
path_prefix: signatures
|
||||
|
||||
status:
|
||||
check_name: CLA Check
|
||||
@@ -26,11 +26,20 @@ updates:
|
||||
day: "monday"
|
||||
timezone: "Asia/Shanghai"
|
||||
time: "08:00"
|
||||
assignees:
|
||||
- "houseme"
|
||||
reviewers:
|
||||
- "overtrue"
|
||||
- "majinghe"
|
||||
ignore:
|
||||
- dependency-name: "object_store"
|
||||
versions: [ "0.13.x" ]
|
||||
- dependency-name: "libunftp"
|
||||
versions: [ "0.23.x" ]
|
||||
- dependency-name: "ratelimit"
|
||||
versions: [ "1.x" ]
|
||||
- dependency-name: "ratelimit"
|
||||
versions: [ "2.x" ]
|
||||
groups:
|
||||
s3s:
|
||||
update-types:
|
||||
|
||||
@@ -2,36 +2,35 @@
|
||||
Pull Request Template for RustFS
|
||||
-->
|
||||
|
||||
## Type of Change
|
||||
- [ ] New Feature
|
||||
- [ ] Bug Fix
|
||||
- [ ] Documentation
|
||||
- [ ] Performance Improvement
|
||||
- [ ] Test/CI
|
||||
- [ ] Refactor
|
||||
- [ ] Other:
|
||||
|
||||
## Related Issues
|
||||
<!-- List related Issue numbers, e.g. #123 -->
|
||||
<!--
|
||||
List related issues, e.g. Fixes #123.
|
||||
Use N/A when there is no related issue.
|
||||
-->
|
||||
|
||||
## Summary of Changes
|
||||
<!-- Briefly describe the main changes and motivation for this PR -->
|
||||
<!--
|
||||
Briefly explain what changed and why reviewers should accept it.
|
||||
Focus on behavior, compatibility, and review-relevant context.
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
- [ ] I have read and followed the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines
|
||||
- [ ] Passed `make pre-commit`
|
||||
- [ ] Added/updated necessary tests
|
||||
- [ ] Documentation updated (if needed)
|
||||
- [ ] CI/CD passed (if applicable)
|
||||
## Verification
|
||||
<!--
|
||||
List the commands or checks you ran, for example:
|
||||
- `make pre-commit`
|
||||
|
||||
Use N/A only when verification is not applicable.
|
||||
-->
|
||||
|
||||
## Impact
|
||||
- [ ] Breaking change (compatibility)
|
||||
- [ ] Requires doc/config/deployment update
|
||||
- [ ] Other impact:
|
||||
<!--
|
||||
Describe user-facing, compatibility, API, deployment, configuration, or
|
||||
documentation impact. Use N/A when there is no expected impact.
|
||||
-->
|
||||
|
||||
## Additional Notes
|
||||
<!-- Any extra information for reviewers -->
|
||||
<!-- Any extra information for reviewers, or N/A. -->
|
||||
|
||||
---
|
||||
|
||||
Thank you for your contribution! Please ensure your PR follows the community standards ([CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)) and sign the CLA if this is your first contribution.
|
||||
Thank you for your contribution! Please ensure your PR follows the community standards ([CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)). If this is your first contribution, review the [CLA document](https://github.com/rustfs/cla/blob/main/cla/v1.md) and sign it by commenting `I have read and agree to the CLA.` on the PR.
|
||||
|
||||
+78
-69
@@ -23,6 +23,7 @@
|
||||
#
|
||||
# Manual Parameters:
|
||||
# - build_docker: Build and push Docker images (default: true)
|
||||
# - platforms: Comma-separated platform IDs or 'all' (default: all)
|
||||
|
||||
name: Build and Release
|
||||
|
||||
@@ -44,22 +45,6 @@ on:
|
||||
- "**/*.svg"
|
||||
- ".gitignore"
|
||||
- ".dockerignore"
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "**.txt"
|
||||
- ".github/**"
|
||||
- "docs/**"
|
||||
- "deploy/**"
|
||||
- "scripts/dev_*.sh"
|
||||
- "LICENSE*"
|
||||
- "README*"
|
||||
- "**/*.png"
|
||||
- "**/*.jpg"
|
||||
- "**/*.svg"
|
||||
- ".gitignore"
|
||||
- ".dockerignore"
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # Weekly on Sunday at midnight UTC
|
||||
workflow_dispatch:
|
||||
@@ -69,6 +54,11 @@ on:
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
platforms:
|
||||
description: "Comma-separated targets or 'all' (e.g. linux-x86_64-musl,macos-aarch64)"
|
||||
required: false
|
||||
default: "all"
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -154,63 +144,75 @@ jobs:
|
||||
echo " - Is prerelease: $is_prerelease"
|
||||
|
||||
# Build RustFS binaries
|
||||
prepare-platform-matrix:
|
||||
name: Prepare Platform Matrix
|
||||
runs-on: ubicloud-standard-2
|
||||
outputs:
|
||||
matrix: ${{ steps.select.outputs.matrix }}
|
||||
selected: ${{ steps.select.outputs.selected }}
|
||||
steps:
|
||||
- name: Select target platforms
|
||||
id: select
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
selected="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.platforms || 'all' }}"
|
||||
selected="$(echo "${selected}" | tr -d '[:space:]')"
|
||||
if [[ -z "${selected}" ]]; then
|
||||
selected="all"
|
||||
fi
|
||||
|
||||
all='{"include":[
|
||||
{"target_id":"linux-x86_64-musl","os":"ubicloud-standard-2","target":"x86_64-unknown-linux-musl","cross":false,"platform":"linux","rustflags":""},
|
||||
{"target_id":"linux-aarch64-musl","os":"ubicloud-standard-2","target":"aarch64-unknown-linux-musl","cross":true,"platform":"linux","rustflags":""},
|
||||
{"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":"windows-x86_64","os":"windows-latest","target":"x86_64-pc-windows-msvc","cross":false,"platform":"windows","rustflags":""}
|
||||
]}'
|
||||
|
||||
if [[ "${selected}" == "all" ]]; then
|
||||
matrix="$(jq -c . <<<"${all}")"
|
||||
else
|
||||
unknown="$(jq -rn --arg selected "${selected}" --argjson all "${all}" '
|
||||
($selected | split(",") | map(select(length > 0))) as $req
|
||||
| ($all.include | map(.target_id)) as $known
|
||||
| [$req[] | select(( $known | index(.) ) == null)]
|
||||
')"
|
||||
if [[ "$(jq 'length' <<<"${unknown}")" -gt 0 ]]; then
|
||||
echo "Unknown platforms: $(jq -r 'join(\",\")' <<<"${unknown}")" >&2
|
||||
echo "Allowed: $(jq -r '.include[].target_id' <<<"${all}" | paste -sd ',' -)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
matrix="$(jq -c --arg selected "${selected}" '
|
||||
($selected | split(",") | map(select(length > 0))) as $req
|
||||
| .include |= map(select(.target_id as $id | ($req | index($id))))
|
||||
' <<<"${all}")"
|
||||
fi
|
||||
|
||||
echo "selected=${selected}" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix=${matrix}" >> "$GITHUB_OUTPUT"
|
||||
echo "Selected platforms: ${selected}"
|
||||
|
||||
build-rustfs:
|
||||
name: Build RustFS
|
||||
needs: [ build-check ]
|
||||
if: needs.build-check.outputs.should_build == 'true'
|
||||
needs: [ build-check, prepare-platform-matrix ]
|
||||
if: needs.build-check.outputs.should_build == 'true' && needs.prepare-platform-matrix.result == 'success'
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
RUSTFLAGS: ${{ matrix.rustflags }}
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
# Always enable Tokio unstable features (required by dial9-tokio-telemetry).
|
||||
# The RUSTFLAGS env var takes precedence over .cargo/config.toml [build] rustflags,
|
||||
# so we must include --cfg tokio_unstable here explicitly; otherwise an empty
|
||||
# RUSTFLAGS value would shadow the config-file flag and silently break tracing.
|
||||
RUSTFLAGS: "--cfg tokio_unstable ${{ matrix.rustflags }}"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Linux builds
|
||||
# Use x86-64-v2 (SSE4.2 baseline) instead of native to ensure distributed
|
||||
# binaries run on older x86_64 CPUs (e.g. Intel Celeron/Atom, Synology NAS).
|
||||
# See: https://github.com/rustfs/rustfs/issues/1838
|
||||
- os: ubicloud-standard-2
|
||||
target: x86_64-unknown-linux-musl
|
||||
cross: false
|
||||
platform: linux
|
||||
rustflags: ''
|
||||
- os: ubicloud-standard-2
|
||||
target: aarch64-unknown-linux-musl
|
||||
cross: true
|
||||
platform: linux
|
||||
rustflags: ''
|
||||
- os: ubicloud-standard-2
|
||||
target: x86_64-unknown-linux-gnu
|
||||
cross: false
|
||||
platform: linux
|
||||
rustflags: ''
|
||||
- os: ubicloud-standard-2
|
||||
target: aarch64-unknown-linux-gnu
|
||||
cross: true
|
||||
platform: linux
|
||||
rustflags: ''
|
||||
# macOS builds
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
cross: false
|
||||
platform: macos
|
||||
rustflags: ''
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
cross: false
|
||||
platform: macos
|
||||
rustflags: ''
|
||||
# Windows builds (temporarily disabled)
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
cross: false
|
||||
platform: windows
|
||||
rustflags: ''
|
||||
#- os: windows-latest
|
||||
# target: aarch64-pc-windows-msvc
|
||||
# cross: true
|
||||
# platform: windows
|
||||
matrix: ${{ fromJson(needs.prepare-platform-matrix.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -319,19 +321,26 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
# Normalize version used for package filenames
|
||||
PACKAGE_VERSION="${VERSION}"
|
||||
if [[ "$PACKAGE_VERSION" == v* ]]; then
|
||||
PACKAGE_VERSION="${PACKAGE_VERSION#v}"
|
||||
fi
|
||||
|
||||
# Generate package name based on build type
|
||||
if [[ -n "$VARIANT" ]]; then
|
||||
ARCH_WITH_VARIANT="${ARCH}-${VARIANT}"
|
||||
else
|
||||
ARCH_WITH_VARIANT="${ARCH}"
|
||||
fi
|
||||
PACKAGE_BASENAME="rustfs-${PLATFORM}-${ARCH_WITH_VARIANT}"
|
||||
|
||||
if [[ "$BUILD_TYPE" == "development" ]]; then
|
||||
# Development build: rustfs-${platform}-${arch}-${variant}-dev-${short_sha}.zip
|
||||
PACKAGE_NAME="rustfs-${PLATFORM}-${ARCH_WITH_VARIANT}-dev-${SHORT_SHA}"
|
||||
else
|
||||
# Release/Prerelease build: rustfs-${platform}-${arch}-${variant}-v${version}.zip
|
||||
PACKAGE_NAME="rustfs-${PLATFORM}-${ARCH_WITH_VARIANT}-v${VERSION}"
|
||||
PACKAGE_NAME="${PACKAGE_BASENAME}-v${PACKAGE_VERSION}"
|
||||
fi
|
||||
|
||||
# Create zip packages for all platforms
|
||||
@@ -401,7 +410,7 @@ jobs:
|
||||
if [[ "$BUILD_TYPE" == "release" ]] || [[ "$BUILD_TYPE" == "prerelease" ]]; then
|
||||
# Create latest version filename
|
||||
# Convert from rustfs-linux-x86_64-musl-v1.0.0 to rustfs-linux-x86_64-musl-latest
|
||||
LATEST_FILE="${PACKAGE_NAME%-v*}-latest.zip"
|
||||
LATEST_FILE="${PACKAGE_BASENAME}-latest.zip"
|
||||
|
||||
echo "🔄 Creating latest version: ${PACKAGE_NAME}.zip -> $LATEST_FILE"
|
||||
cp "${PACKAGE_NAME}.zip" "$LATEST_FILE"
|
||||
@@ -414,7 +423,7 @@ jobs:
|
||||
# Development builds (only main branch triggers development builds)
|
||||
# Create main-latest version filename
|
||||
# Convert from rustfs-linux-x86_64-dev-abc123 to rustfs-linux-x86_64-main-latest
|
||||
MAIN_LATEST_FILE="${PACKAGE_NAME%-dev-*}-main-latest.zip"
|
||||
MAIN_LATEST_FILE="${PACKAGE_BASENAME}-main-latest.zip"
|
||||
|
||||
echo "🔄 Creating main-latest version: ${PACKAGE_NAME}.zip -> $MAIN_LATEST_FILE"
|
||||
cp "${PACKAGE_NAME}.zip" "$MAIN_LATEST_FILE"
|
||||
|
||||
+57
-27
@@ -53,6 +53,8 @@ on:
|
||||
- ".github/workflows/docker.yml"
|
||||
- ".github/workflows/audit.yml"
|
||||
- ".github/workflows/performance.yml"
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # Weekly on Sunday at midnight UTC
|
||||
workflow_dispatch:
|
||||
@@ -91,6 +93,8 @@ jobs:
|
||||
|
||||
typos:
|
||||
name: Typos
|
||||
needs: skip-check
|
||||
if: needs.skip-check.outputs.should_skip != 'true'
|
||||
runs-on: ubicloud-standard-2
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -104,6 +108,8 @@ jobs:
|
||||
if: needs.skip-check.outputs.should_skip != 'true'
|
||||
runs-on: ubicloud-standard-4
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -116,9 +122,6 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install cargo-nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cargo nextest run --all --exclude e2e_test
|
||||
@@ -133,9 +136,42 @@ jobs:
|
||||
- name: Check layered dependencies
|
||||
run: ./scripts/check_layer_dependencies.sh
|
||||
|
||||
build-rustfs-debug-binary:
|
||||
name: Build RustFS Debug Binary
|
||||
needs: skip-check
|
||||
if: needs.skip-check.outputs.should_skip != 'true'
|
||||
runs-on: ubicloud-standard-4
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Rust environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
rust-version: stable
|
||||
cache-shared-key: ci-rustfs-debug-binary-${{ hashFiles('**/Cargo.lock') }}
|
||||
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build debug binary
|
||||
run: |
|
||||
touch rustfs/build.rs
|
||||
cargo build -p rustfs --bins --jobs 2
|
||||
|
||||
- name: Upload debug binary
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: rustfs-debug-binary
|
||||
path: target/debug/rustfs
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
e2e-tests:
|
||||
name: End-to-End Tests
|
||||
needs: skip-check
|
||||
needs: [ skip-check, build-rustfs-debug-binary ]
|
||||
if: needs.skip-check.outputs.should_skip != 'true'
|
||||
runs-on: ubicloud-standard-2
|
||||
timeout-minutes: 30
|
||||
@@ -148,13 +184,17 @@ jobs:
|
||||
rm -rf /tmp/rustfs
|
||||
rm -f /tmp/rustfs.log
|
||||
|
||||
- name: Setup Rust environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Download debug binary
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
rust-version: stable
|
||||
cache-shared-key: ci-e2e-${{ hashFiles('**/Cargo.lock') }}
|
||||
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: rustfs-debug-binary
|
||||
path: target/debug
|
||||
|
||||
- name: Make binary executable
|
||||
run: chmod +x ./target/debug/rustfs
|
||||
|
||||
- name: Setup Rust toolchain for s3s-e2e installation
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install s3s-e2e test tool
|
||||
uses: taiki-e/cache-cargo-install-action@v2
|
||||
@@ -163,12 +203,6 @@ jobs:
|
||||
git: https://github.com/s3s-project/s3s.git
|
||||
rev: 4a04a670cf41274d9be9ab65dc36f4aa3f92fbad
|
||||
|
||||
- name: Build debug binary
|
||||
run: |
|
||||
touch rustfs/build.rs
|
||||
# Limit concurrency to prevent OOM
|
||||
cargo build -p rustfs --bins --jobs 2
|
||||
|
||||
- name: Run end-to-end tests
|
||||
run: |
|
||||
s3s-e2e --version
|
||||
@@ -184,7 +218,7 @@ jobs:
|
||||
|
||||
s3-implemented-tests:
|
||||
name: S3 Implemented Tests
|
||||
needs: skip-check
|
||||
needs: [ skip-check, build-rustfs-debug-binary ]
|
||||
if: needs.skip-check.outputs.should_skip != 'true'
|
||||
runs-on: ubicloud-standard-4
|
||||
timeout-minutes: 60
|
||||
@@ -192,18 +226,14 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Rust environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Download debug binary
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
rust-version: stable
|
||||
cache-shared-key: ci-s3tests-${{ hashFiles('**/Cargo.lock') }}
|
||||
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: rustfs-debug-binary
|
||||
path: target/debug
|
||||
|
||||
- name: Build debug binary
|
||||
run: |
|
||||
touch rustfs/build.rs
|
||||
cargo build -p rustfs --bins --jobs 2
|
||||
- name: Make binary executable
|
||||
run: chmod +x ./target/debug/rustfs
|
||||
|
||||
- name: Run implemented s3-tests
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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.
|
||||
|
||||
name: CLA Check
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
cla:
|
||||
if: ${{ github.event_name != 'issue_comment' || github.event.issue.pull_request }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Report CLA result for merge queue
|
||||
if: github.event_name == 'merge_group'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
await github.rest.checks.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: 'CLA Check',
|
||||
head_sha: context.sha,
|
||||
status: 'completed',
|
||||
conclusion: 'success',
|
||||
output: {
|
||||
title: 'CLA requirements satisfied for merge queue',
|
||||
summary: 'Queued pull requests must satisfy the required CLA check before they enter the merge queue. This reports the existing CLA result on the merge-group SHA.'
|
||||
}
|
||||
});
|
||||
|
||||
- name: Create token for rustfs/cla
|
||||
if: github.event_name != 'merge_group'
|
||||
id: registry-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ vars.CLA_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.CLA_BOT_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: cla
|
||||
permission-contents: write
|
||||
|
||||
- name: Run CLA Bot
|
||||
if: github.event_name != 'merge_group'
|
||||
uses: overtrue/cla-bot@v0.0.9
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
registry-token: ${{ steps.registry-token.outputs.token }}
|
||||
@@ -66,6 +66,7 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
REGISTRY_DOCKERHUB: rustfs/rustfs
|
||||
REGISTRY_GHCR: ghcr.io/${{ github.repository }}
|
||||
REGISTRY_QUAY: quay.io/${{ secrets.QUAY_USERNAME }}/rustfs
|
||||
DOCKER_PLATFORMS: linux/amd64,linux/arm64
|
||||
|
||||
jobs:
|
||||
@@ -162,11 +163,10 @@ jobs:
|
||||
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]] || [[ "$version" == *"rc"* ]]; then
|
||||
build_type="prerelease"
|
||||
is_prerelease=true
|
||||
# TODO: Temporary change - currently allows alpha versions to also create latest tags
|
||||
# After the version is stable, you need to remove the following line and restore the original logic (latest is created only for stable versions)
|
||||
if [[ "$version" == *"alpha"* ]]; then
|
||||
# Current policy: create latest tags for stable releases and selected prereleases (alpha/beta).
|
||||
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]]; then
|
||||
create_latest=true
|
||||
echo "🧪 Building Docker image for prerelease: $version (temporarily allowing creation of latest tag)"
|
||||
echo "🧪 Building Docker image for prerelease: $version (creating latest tag)"
|
||||
else
|
||||
echo "🧪 Building Docker image for prerelease: $version"
|
||||
fi
|
||||
@@ -215,11 +215,10 @@ jobs:
|
||||
v*alpha*|v*beta*|v*rc*|*alpha*|*beta*|*rc*)
|
||||
build_type="prerelease"
|
||||
is_prerelease=true
|
||||
# TODO: Temporary change - currently allows alpha versions to also create latest tags
|
||||
# After the version is stable, you need to remove the if block below and restore the original logic.
|
||||
if [[ "$input_version" == *"alpha"* ]]; then
|
||||
# Current policy: create latest tags for stable releases and selected prereleases (alpha/beta).
|
||||
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]]; then
|
||||
create_latest=true
|
||||
echo "🧪 Building with prerelease version: $input_version (temporarily allowing creation of latest tag)"
|
||||
echo "🧪 Building with prerelease version: $input_version (creating latest tag)"
|
||||
else
|
||||
echo "🧪 Building with prerelease version: $input_version"
|
||||
fi
|
||||
@@ -287,12 +286,19 @@ jobs:
|
||||
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# - name: Login to GitHub Container Registry
|
||||
# uses: docker/login-action@v3
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.actor }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ secrets.GHCR_USERNAME }}
|
||||
password: ${{ secrets.GHCR_PASSWORD }}
|
||||
|
||||
- name: Login to Quay.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_PASSWORD }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -338,14 +344,13 @@ jobs:
|
||||
|
||||
# Generate tags based on build type
|
||||
# Only support release and prerelease builds (no development builds)
|
||||
TAGS="${{ env.REGISTRY_DOCKERHUB }}:${VERSION}${VARIANT_SUFFIX}"
|
||||
TAG_BASE="${VERSION}${VARIANT_SUFFIX}"
|
||||
TAGS="${{ env.REGISTRY_DOCKERHUB }}:$TAG_BASE,${{ env.REGISTRY_GHCR }}:$TAG_BASE,${{ env.REGISTRY_QUAY }}:$TAG_BASE"
|
||||
|
||||
# Add channel tags for prereleases and latest for stable
|
||||
if [[ "$CREATE_LATEST" == "true" ]]; then
|
||||
# TODO: Temporary change - the current alpha version will also create the latest tag
|
||||
# After the version is stabilized, the logic here remains unchanged, but the upstream CREATE_LATEST setting needs to be restored.
|
||||
# Stable release (and temporary alpha versions)
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:latest${VARIANT_SUFFIX}"
|
||||
# Create latest tags for stable releases and selected prereleases when CREATE_LATEST=true.
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:latest${VARIANT_SUFFIX},${{ env.REGISTRY_GHCR }}:latest${VARIANT_SUFFIX},${{ env.REGISTRY_QUAY }}:latest${VARIANT_SUFFIX}"
|
||||
elif [[ "$BUILD_TYPE" == "prerelease" ]]; then
|
||||
# Prerelease channel tags (alpha, beta, rc)
|
||||
if [[ "$VERSION" == *"alpha"* ]]; then
|
||||
@@ -357,7 +362,7 @@ jobs:
|
||||
fi
|
||||
|
||||
if [[ -n "$CHANNEL" ]]; then
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:${CHANNEL}${VARIANT_SUFFIX}"
|
||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:${CHANNEL}${VARIANT_SUFFIX},${{ env.REGISTRY_GHCR }}:${CHANNEL}${VARIANT_SUFFIX},${{ env.REGISTRY_QUAY }}:${CHANNEL}${VARIANT_SUFFIX}"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -441,10 +446,9 @@ jobs:
|
||||
"prerelease")
|
||||
echo "🧪 Prerelease Docker image has been built with ${VERSION} tags"
|
||||
echo "⚠️ This is a prerelease image - use with caution"
|
||||
# TODO: Temporary change - alpha versions currently create the latest tag
|
||||
# After the version is stable, you need to restore the following prompt information
|
||||
if [[ "$VERSION" == *"alpha"* ]] && [[ "$CREATE_LATEST" == "true" ]]; then
|
||||
echo "🏷️ Latest tag has been created for alpha version (temporary measures)"
|
||||
# Create latest tags for stable releases and selected prereleases when CREATE_LATEST=true.
|
||||
if [[ "$CREATE_LATEST" == "true" ]]; then
|
||||
echo "🏷️ Latest tag has been created for prerelease: $VERSION"
|
||||
else
|
||||
echo "🚫 Latest tag NOT created for prerelease"
|
||||
fi
|
||||
|
||||
@@ -50,6 +50,11 @@ env:
|
||||
|
||||
RUST_LOG: info
|
||||
PLATFORM: linux/amd64
|
||||
BUILDX_CACHE_SCOPE: rustfs-e2e-s3tests-source
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs['test-mode'] || 'single' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -57,12 +62,25 @@ defaults:
|
||||
|
||||
jobs:
|
||||
s3tests-single:
|
||||
if: github.event.inputs.test-mode == 'single'
|
||||
if: github.event.inputs['test-mode'] == 'single'
|
||||
runs-on: ubicloud-standard-2
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Cache pip downloads
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-e2e-s3tests-${{ hashFiles('.github/workflows/e2e-s3tests.yml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-e2e-s3tests-
|
||||
|
||||
- name: Install Python tools
|
||||
run: |
|
||||
python3 -m pip install --user --upgrade pip awscurl tox
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Enable buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -70,8 +88,8 @@ jobs:
|
||||
run: |
|
||||
DOCKER_BUILDKIT=1 docker buildx build --load \
|
||||
--platform ${PLATFORM} \
|
||||
--cache-from type=gha \
|
||||
--cache-to type=gha,mode=max \
|
||||
--cache-from type=gha,scope=${BUILDX_CACHE_SCOPE} \
|
||||
--cache-to type=gha,mode=max,scope=${BUILDX_CACHE_SCOPE} \
|
||||
-t rustfs-ci \
|
||||
-f Dockerfile.source .
|
||||
|
||||
@@ -121,9 +139,6 @@ jobs:
|
||||
|
||||
- name: Provision s3-tests alt user (required by suite)
|
||||
run: |
|
||||
python3 -m pip install --user --upgrade pip awscurl
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# Admin API requires AWS SigV4 signing. awscurl is used by RustFS codebase as well.
|
||||
awscurl \
|
||||
--service s3 \
|
||||
@@ -156,8 +171,6 @@ jobs:
|
||||
|
||||
- name: Prepare s3-tests
|
||||
run: |
|
||||
python3 -m pip install --user --upgrade pip tox
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
git clone --depth 1 https://github.com/ceph/s3-tests.git s3-tests
|
||||
|
||||
- name: Run ceph s3-tests (debug friendly)
|
||||
@@ -211,12 +224,25 @@ jobs:
|
||||
path: artifacts/**
|
||||
|
||||
s3tests-multi:
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.test-mode == 'multi'
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs['test-mode'] == 'multi'
|
||||
runs-on: ubicloud-standard-2
|
||||
timeout-minutes: 150
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Cache pip downloads
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-e2e-s3tests-${{ hashFiles('.github/workflows/e2e-s3tests.yml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-e2e-s3tests-
|
||||
|
||||
- name: Install Python tools
|
||||
run: |
|
||||
python3 -m pip install --user --upgrade pip awscurl tox
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Enable buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -224,8 +250,8 @@ jobs:
|
||||
run: |
|
||||
DOCKER_BUILDKIT=1 docker buildx build --load \
|
||||
--platform ${PLATFORM} \
|
||||
--cache-from type=gha \
|
||||
--cache-to type=gha,mode=max \
|
||||
--cache-from type=gha,scope=${BUILDX_CACHE_SCOPE} \
|
||||
--cache-to type=gha,mode=max,scope=${BUILDX_CACHE_SCOPE} \
|
||||
-t rustfs-ci \
|
||||
-f Dockerfile.source .
|
||||
|
||||
@@ -337,9 +363,6 @@ jobs:
|
||||
|
||||
- name: Provision s3-tests alt user (required by suite)
|
||||
run: |
|
||||
python3 -m pip install --user --upgrade pip awscurl
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
awscurl \
|
||||
--service s3 \
|
||||
--region "${S3_REGION}" \
|
||||
@@ -368,8 +391,6 @@ jobs:
|
||||
|
||||
- name: Prepare s3-tests
|
||||
run: |
|
||||
python3 -m pip install --user --upgrade pip tox
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
git clone --depth 1 https://github.com/ceph/s3-tests.git s3-tests
|
||||
|
||||
- name: Run ceph s3-tests (multi, debug friendly)
|
||||
|
||||
@@ -18,41 +18,78 @@ on:
|
||||
workflow_run:
|
||||
workflows: [ "Build and Release" ]
|
||||
types: [ completed ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Release version to publish, e.g. 1.0.0-beta.1 or v1.0.0-beta.1"
|
||||
required: true
|
||||
default: "1.0.0-beta.1"
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
new_version: ${{ github.event.workflow_run.head_branch }}
|
||||
|
||||
jobs:
|
||||
build-helm-package:
|
||||
runs-on: ubicloud-standard-2
|
||||
# Only run on successful builds triggered by tag pushes (version format: x.y.z or x.y.z-suffix)
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
contains(github.event.workflow_run.head_branch, '.')
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
contains(github.event.workflow_run.head_branch, '.')
|
||||
)
|
||||
|
||||
outputs:
|
||||
raw_tag: ${{ steps.version.outputs.raw_tag }}
|
||||
app_version: ${{ steps.version.outputs.app_version }}
|
||||
chart_version: ${{ steps.version.outputs.chart_version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout helm chart repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Replace chart app version
|
||||
- name: Normalize release version
|
||||
id: version
|
||||
run: |
|
||||
set -e
|
||||
set -x
|
||||
old_version=$(grep "^appVersion:" helm/rustfs/Chart.yaml | awk '{print $2}')
|
||||
sed -i "s/$old_version/$new_version/g" helm/rustfs/Chart.yaml
|
||||
set -eux
|
||||
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
RAW="${{ github.event.inputs.version }}"
|
||||
else
|
||||
RAW="${{ github.event.workflow_run.head_branch }}"
|
||||
fi
|
||||
|
||||
case "$RAW" in
|
||||
refs/tags/*)
|
||||
RAW_TAG="${RAW#refs/tags/}"
|
||||
;;
|
||||
*)
|
||||
RAW_TAG="$RAW"
|
||||
;;
|
||||
esac
|
||||
|
||||
./scripts/helm_chart_version.sh "$RAW_TAG"
|
||||
|
||||
- name: Replace chart version and app version
|
||||
run: |
|
||||
set -eux
|
||||
sed -i -E 's/^version:.*/version: "${{ steps.version.outputs.chart_version }}"/' helm/rustfs/Chart.yaml
|
||||
sed -i -E 's/^appVersion:.*/appVersion: "${{ steps.version.outputs.app_version }}"/' helm/rustfs/Chart.yaml
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4.3.0
|
||||
|
||||
- name: Test Helm Chart Templates
|
||||
run: ./scripts/test_helm_templates.sh
|
||||
|
||||
- name: Package Helm Chart
|
||||
run: |
|
||||
set -eux
|
||||
cp helm/README.md helm/rustfs/
|
||||
package_version=$(echo $new_version | awk -F '-' '{print $2}' | awk -F '.' '{print $NF}')
|
||||
helm package ./helm/rustfs --destination helm/rustfs/ --version "0.0.$package_version"
|
||||
helm package ./helm/rustfs \
|
||||
--destination helm/rustfs/ \
|
||||
--version "${{ steps.version.outputs.chart_version }}"
|
||||
|
||||
- name: Upload helm package as artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
@@ -64,6 +101,7 @@ jobs:
|
||||
publish-helm-package:
|
||||
runs-on: ubicloud-standard-2
|
||||
needs: [ build-helm-package ]
|
||||
if: needs.build-helm-package.result == 'success'
|
||||
|
||||
steps:
|
||||
- name: Checkout helm package repo
|
||||
@@ -86,9 +124,9 @@ jobs:
|
||||
|
||||
- name: Push helm package and index file
|
||||
run: |
|
||||
set -eux
|
||||
git config --global user.name "${{ secrets.USERNAME }}"
|
||||
git config --global user.email "${{ secrets.EMAIL_ADDRESS }}"
|
||||
git status .
|
||||
git add .
|
||||
git commit -m "Update rustfs helm package with $new_version."
|
||||
git commit -m "Update rustfs helm package with ${{ needs.build-helm-package.outputs.app_version }}." || echo "No changes to commit"
|
||||
git push origin main
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
update-flake:
|
||||
name: Update flake.lock
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -46,14 +46,18 @@ jobs:
|
||||
id: update
|
||||
uses: DeterminateSystems/update-flake-lock@main
|
||||
with:
|
||||
git-author-name: houseme
|
||||
git-author-email: housemecn@gmail.com
|
||||
git-committer-name: houseme
|
||||
git-committer-email: housemecn@gmail.com
|
||||
pr-title: "chore(deps): update flake.lock"
|
||||
pr-labels: |
|
||||
dependencies
|
||||
nix
|
||||
automated
|
||||
commit-msg: "chore(deps): update flake.lock"
|
||||
pr-reviewers: houseme, heihutu
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-reviewers: houseme, overtrue, majinghe
|
||||
token: ${{ secrets.FLAKE_UPDATE_TOKEN }}
|
||||
|
||||
- name: Log PR details
|
||||
if: steps.update.outputs.pull-request-number
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
nix-validation:
|
||||
name: Nix Build & Check
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
- name: Setup Magic Nix Cache
|
||||
uses: DeterminateSystems/magic-nix-cache-action@v13
|
||||
|
||||
|
||||
- name: Setup Flake Checker
|
||||
uses: DeterminateSystems/flake-checker-action@v12
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ jobs:
|
||||
name: Performance Profiling
|
||||
runs-on: ubicloud-standard-2
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -117,6 +119,8 @@ jobs:
|
||||
name: Benchmark Tests
|
||||
runs-on: ubicloud-standard-2
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
+10
-2
@@ -16,10 +16,12 @@ vendor
|
||||
cli/rustfs-gui/embedded-rustfs/rustfs
|
||||
*.log
|
||||
deploy/certs/*
|
||||
deploy/data/*
|
||||
*jsonl
|
||||
.env
|
||||
.rustfs.sys
|
||||
.cargo
|
||||
.cargo/
|
||||
!.cargo/config.toml
|
||||
profile.json
|
||||
.docker/openobserve-otel/data
|
||||
*.zst
|
||||
@@ -38,10 +40,16 @@ artifacts/
|
||||
*.audit
|
||||
*.snappy
|
||||
PR_DESCRIPTION.md
|
||||
IMPLEMENTATION_PLAN.md
|
||||
scripts/s3-tests/selected_tests.txt
|
||||
docs
|
||||
|
||||
# nix stuff
|
||||
result*
|
||||
*.gz
|
||||
rustfs-webdav.code-workspace
|
||||
|
||||
.aiexclude
|
||||
*.bak
|
||||
# Local test/benchmark artifacts
|
||||
benchmarks.logs
|
||||
tmp/
|
||||
|
||||
Vendored
+14
-6
@@ -197,19 +197,27 @@
|
||||
// "__RUSTFS_SSE_SIMPLE_CMK": "2dfNXGHlsEflGVCxb+5DIdGEl1sIvtwX+QfmYasi5QM=",
|
||||
|
||||
// 2. kms local backend test key
|
||||
"RUSTFS_KMS_ENABLE": "true",
|
||||
"RUSTFS_KMS_BACKEND": "local",
|
||||
"RUSTFS_KMS_KEY_DIR": "./target/kms-key-dir",
|
||||
"RUSTFS_KMS_LOCAL_MASTER_KEY": "my-secret-key", // Some Password
|
||||
"RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key",
|
||||
// "RUSTFS_KMS_ENABLE": "true",
|
||||
// "RUSTFS_KMS_BACKEND": "local",
|
||||
// "RUSTFS_KMS_KEY_DIR": "./target/kms-key-dir",
|
||||
// "RUSTFS_KMS_LOCAL_MASTER_KEY": "my-secret-key", // Some Password
|
||||
// "RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key",
|
||||
|
||||
// 3. kms vault backend test key
|
||||
// "RUSTFS_KMS_ENABLE": "true",
|
||||
// "RUSTFS_KMS_BACKEND": "vault",
|
||||
// "RUSTFS_KMS_BACKEND": "vault-kv2",
|
||||
// "RUSTFS_KMS_VAULT_ADDRESS": "http://127.0.0.1:8200",
|
||||
// "RUSTFS_KMS_VAULT_TOKEN": "Dev Token",
|
||||
// "RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key",
|
||||
|
||||
// 4. kms vault transit backend test key
|
||||
"RUSTFS_KMS_ENABLE": "true",
|
||||
"RUSTFS_KMS_BACKEND": "vault-transit",
|
||||
"RUSTFS_KMS_VAULT_ADDRESS": "http://127.0.0.1:8200",
|
||||
"RUSTFS_KMS_VAULT_TOKEN": "Dev Token",
|
||||
"RUSTFS_KMS_VAULT_MOUNT_PATH": "transit",
|
||||
"RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key",
|
||||
|
||||
},
|
||||
"sourceLanguages": [
|
||||
"rust"
|
||||
|
||||
@@ -1,103 +1,113 @@
|
||||
# Repository Guidelines
|
||||
# RustFS Agent Instructions (Global)
|
||||
|
||||
This file provides guidance for AI agents and developers working with code in this repository.
|
||||
This root file keeps repository-wide rules only.
|
||||
Use the nearest subdirectory `AGENTS.md` for path-specific guidance.
|
||||
|
||||
## Project Overview
|
||||
## Rule Precedence
|
||||
|
||||
RustFS is a high-performance distributed object storage software built with Rust, providing S3-compatible APIs and advanced features like data lakes, AI, and big data support. It's designed as an alternative to MinIO with better performance and a more business-friendly Apache 2.0 license.
|
||||
1. System/developer instructions.
|
||||
2. Current user/task instructions.
|
||||
3. The nearest `AGENTS.md` in the current path.
|
||||
4. This file (global defaults).
|
||||
|
||||
## ⚠️ Pre-Commit Checklist (MANDATORY)
|
||||
If repo-level instructions conflict, follow the nearest file and keep behavior aligned with CI.
|
||||
|
||||
## Execution Discipline
|
||||
|
||||
- Read the relevant existing code, tests, and local guidance before changing behavior.
|
||||
- State assumptions when they affect the implementation or verification path.
|
||||
- If a task has multiple plausible interpretations, list the options briefly and choose the narrowest reasonable path; ask when the ambiguity would make the change risky.
|
||||
- For multi-step work, keep the plan minimal and tied to verifiable outcomes.
|
||||
- Avoid redundant file reads, repeated commands, and unnecessary exploratory work once enough context is available.
|
||||
- A good result is a minimal diff with clear assumptions, no over-engineering, and independent verification.
|
||||
|
||||
## Communication and Language
|
||||
|
||||
- Respond in the same language used by the requester.
|
||||
- Keep source code, comments, commit messages, and PR title/body in English.
|
||||
- Be concise. Avoid sycophantic openers, closing fluff, and verbose status reporting.
|
||||
|
||||
## Change Style for Existing Logic
|
||||
|
||||
- Prefer direct, local code over extracting one-off helpers.
|
||||
- Extract a helper only when logic is reused or the extraction materially clarifies a non-trivial flow.
|
||||
- Solve only the requested problem; do not add speculative features, configurability, or adjacent improvements.
|
||||
- Prefer editing existing code over rewriting files or reshaping unrelated logic.
|
||||
- Modify only what is required and remove only artifacts introduced by your own changes.
|
||||
- Preserve the existing control-flow and logic shape when fixing bugs or addressing review comments, especially in init, distributed coordination, locking, metadata, and concurrency paths.
|
||||
- Do not refactor existing code only to make it easier to unit test.
|
||||
- Keep fixes narrowly aligned with the requested behavior; avoid semantic-adjacent rewrites while touching sensitive paths.
|
||||
- Keep code elegant, concise, and direct. Prefer minimal, readable implementations over over-engineering and excessive abstraction. Use comments to clarify non-obvious intent and invariants, not to compensate for unclear code.
|
||||
- Mention unrelated issues when useful, but do not fix them as part of a narrow task.
|
||||
|
||||
## Constant and String Usage
|
||||
|
||||
- Before introducing new string literals, search for existing constants/enums that already represent the same semantic value.
|
||||
- Reuse existing constants for protocol labels, error identifiers, header keys, event names, metric names, command tags, and similar fixed tokens.
|
||||
- If a new string is truly unique, define a local constant near related logic and avoid scattering the literal across multiple sites.
|
||||
- When changing existing behavior, keep naming and format consistency by aligning with established project constants.
|
||||
|
||||
## Sources of Truth
|
||||
|
||||
- Workspace layout and crate membership: `Cargo.toml` (`[workspace].members`)
|
||||
- Local quality commands: `Makefile` and `.config/make/`
|
||||
- CI quality gates: `.github/workflows/ci.yml`
|
||||
- PR template: `.github/pull_request_template.md`
|
||||
|
||||
Avoid duplicating long crate lists or command matrices in instruction files.
|
||||
Reference the source files above instead.
|
||||
|
||||
## Verification Before PR
|
||||
|
||||
Convert changes into independently verifiable outcomes. Prefer focused tests for behavior changes and run the relevant checks before declaring completion.
|
||||
|
||||
For code changes, run and pass the following before opening a PR:
|
||||
|
||||
**Before EVERY commit, you MUST run and pass ALL of the following:**
|
||||
```bash
|
||||
cargo fmt --all --check # Code formatting
|
||||
cargo clippy --all-targets --all-features -- -D warnings # Lints
|
||||
cargo test --workspace --exclude e2e_test # Unit tests
|
||||
make pre-commit
|
||||
```
|
||||
Or simply run `make pre-commit` which covers all checks. **DO NOT commit if any check fails.**
|
||||
|
||||
## Communication Rules
|
||||
Before pushing code changes, make sure formatting is clean:
|
||||
|
||||
- Respond to the user in the same language used by the user.
|
||||
- Code and documentation must be written in English only.
|
||||
- **Pull Request titles and descriptions must be written in English** to ensure consistency and accessibility for all contributors.
|
||||
- Run `cargo fmt --all`.
|
||||
- Run `cargo fmt --all --check` and ensure no files are modified unexpectedly.
|
||||
|
||||
## Project Structure
|
||||
If `make` is unavailable, run the equivalent checks defined under `.config/make/`.
|
||||
Documentation-only or instruction-only changes are exempt from the verification commands above (including the `.config/make/` equivalents), though any installed git pre-commit hooks (for example, from `make setup-hooks`) may still run on commit unless explicitly skipped.
|
||||
After build-based verification completes, clean generated build artifacts before wrapping up to avoid unnecessary disk usage.
|
||||
Do not open a PR with code changes when the required checks fail.
|
||||
|
||||
The workspace root hosts shared dependencies in `Cargo.toml`. The service binary lives under `rustfs/src/main.rs`, while reusable crates sit in `crates/` (including `ecstore`, `iam`, `kms`, `madmin`, `s3select-api`, `s3select-query`, `config`, `crypto`, `lock`, `filemeta`, `rio`, `common`, `protos`, `audit-logger`, `notify`, `obs`, `workers`, `appauth`, `ahm`, `mcp`, `signer`, `checksums`, `utils`, `zip`, `targets`, and `e2e_test`). Deployment manifests are under `deploy/`, Docker assets sit at the root, and automation lives in `scripts/`.
|
||||
## Git and PR Baseline
|
||||
|
||||
### Core Architecture
|
||||
- Use feature branches based on the latest `main`.
|
||||
- Follow Conventional Commits, with subject length <= 72 characters.
|
||||
- Keep PR title and description in English.
|
||||
- Use `.github/pull_request_template.md` and keep all section headings.
|
||||
- Use `N/A` for non-applicable template sections.
|
||||
- Include verification commands in the PR description.
|
||||
- When using `gh pr create`/`gh pr edit`, use `--body-file` instead of inline `--body` for multiline markdown.
|
||||
- Do not include the literal sequence `\n` in any GitHub issue, pull request, or discussion comment.
|
||||
- After fixing code review comments or CI findings, always mark corresponding review
|
||||
comments/threads as resolved before returning to the user.
|
||||
- In handling review comments, confirm the underlying issue before changing code.
|
||||
If a suggested change is not appropriate for behavior or risk, reply with a
|
||||
concise rationale instead of blindly applying it.
|
||||
|
||||
- **Main Binary (`rustfs/`):** Entry point at `rustfs/src/main.rs`, includes admin, auth, config, server, storage, license management, and profiling modules.
|
||||
- **Key Crates:** Cargo workspace with 25+ crates supporting S3-compatible APIs, erasure coding storage, IAM, KMS, S3 Select, and observability.
|
||||
- **Build System:** Custom `build-rustfs.sh` script, multi-architecture Docker builds, Make/Just task runners, cross-compilation support.
|
||||
## Security Baseline
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- Never commit secrets, credentials, or key material.
|
||||
- Use environment variables or vault tooling for sensitive configuration.
|
||||
- For localhost-sensitive tests, verify proxy settings to avoid traffic leakage.
|
||||
|
||||
### Quick Commands
|
||||
- `cargo check --all-targets` - Fast validation
|
||||
- `cargo build --release` or `make build` - Release build
|
||||
- `./build-rustfs.sh --dev` - Development build with debug symbols
|
||||
- `make pre-commit` - Run all quality checks (fmt, clippy, check, test)
|
||||
## Scoped Guidance in This Repository
|
||||
|
||||
### Testing
|
||||
- `cargo test --workspace --exclude e2e_test` - Unit tests
|
||||
- `cargo test --package e2e_test` - E2E tests
|
||||
- For KMS tests: `NO_PROXY=127.0.0.1,localhost HTTP_PROXY= HTTPS_PROXY= http_proxy= https_proxy= cargo test --package e2e_test test_local_kms_end_to_end -- --nocapture --test-threads=1`
|
||||
|
||||
### Cross-Platform Builds
|
||||
- `./build-rustfs.sh --platform x86_64-unknown-linux-musl` - Build for musl
|
||||
- `./build-rustfs.sh --platform aarch64-unknown-linux-gnu` - Build for ARM64
|
||||
- `make build-cross-all` - Build all supported architectures
|
||||
|
||||
## Coding Style & Safety Requirements
|
||||
|
||||
- **Formatting:** Follow `rustfmt.toml` (130-column width). Use `snake_case` for items, `PascalCase` for types, `SCREAMING_SNAKE_CASE` for constants.
|
||||
- **Safety:** `unsafe_code = "deny"` enforced at workspace level. Never use `unwrap()`, `expect()`, or panic-inducing code except in tests.
|
||||
- **Error Handling:** Prefer `anyhow` for applications, `thiserror` for libraries. Use proper error handling with `Result<T, E>` and `Option<T>`.
|
||||
- **Async:** Keep async code non-blocking. Offload CPU-heavy work with `tokio::task::spawn_blocking` when necessary.
|
||||
- **Language:** Code comments, function names, variable names, and all text in source files must be in English only.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Co-locate unit tests with their modules and give behavior-led names. Integration suites belong in each crate's `tests/` directory, while exhaustive end-to-end scenarios live in `crates/e2e_test/`. When fixing bugs or adding features, include regression tests that capture the new behavior.
|
||||
|
||||
## KMS (Key Management Service)
|
||||
|
||||
- **Implementation:** Complete with Local and Vault backends, auto-configures on startup with `--kms-enable` flag.
|
||||
- **Encryption:** Full S3-compatible server-side encryption (SSE-S3, SSE-KMS, SSE-C).
|
||||
- **Testing:** Comprehensive E2E tests in `crates/e2e_test/src/kms/`. Requires proxy bypass for local testing.
|
||||
- **Key Files:** `crates/kms/`, `rustfs/src/storage/ecfs.rs`, `rustfs/src/admin/handlers/kms*.rs`
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- Global configuration environment variables must use flat `RUSTFS_*` names (no module segments), such as `RUSTFS_REGION`, `RUSTFS_ADDRESS`, `RUSTFS_VOLUMES`, and `RUSTFS_LICENSE`.
|
||||
- `RUSTFS_SCANNER_ENABLED` - Enable/disable background data scanner (default: true)
|
||||
- `RUSTFS_HEAL_ENABLED` - Enable/disable auto-heal functionality (default: true)
|
||||
- Deprecated aliases (for pre-beta compatibility) are documented in [the config module README](crates/config/README.md#environment-variable-naming-conventions) and must log warnings when used.
|
||||
- For KMS tests: `NO_PROXY=127.0.0.1,localhost` and clear proxy environment variables
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Work on feature branches (e.g., `feat/...`) after syncing `main`. Follow Conventional Commits under 72 characters. Each commit must compile, format cleanly, and pass `make pre-commit`.
|
||||
|
||||
**Pull Request Requirements:**
|
||||
- PR titles and descriptions **MUST be written in English**
|
||||
- Open PRs with a concise summary, note verification commands, link relevant issues
|
||||
- Follow the PR template format and fill in all required sections
|
||||
- Wait for reviewer approval before merging
|
||||
|
||||
## Security & Configuration Tips
|
||||
|
||||
Do not commit secrets or cloud credentials; prefer environment variables or vault tooling. Review IAM- and KMS-related changes with a second maintainer. Confirm proxy settings before running sensitive tests to avoid leaking traffic outside localhost.
|
||||
|
||||
## Important Reminders
|
||||
|
||||
- Always compile after code changes: Use `cargo build` to catch errors early
|
||||
- Don't bypass tests: All functionality must be properly tested, not worked around
|
||||
- Use proper error handling: Never use `unwrap()` or `expect()` in production code (except tests)
|
||||
- Do what has been asked; nothing more, nothing less
|
||||
- NEVER create files unless they're absolutely necessary for achieving your goal
|
||||
- ALWAYS prefer editing an existing file to creating a new one
|
||||
- NEVER proactively create documentation files (*.md) or README files unless explicitly requested
|
||||
- NEVER commit PR description files (e.g., PR_DESCRIPTION.md): These are temporary reference files for creating pull requests and should remain local only
|
||||
- `.github/AGENTS.md`
|
||||
- `crates/AGENTS.md`
|
||||
- `crates/config/AGENTS.md`
|
||||
- `crates/ecstore/AGENTS.md`
|
||||
- `crates/e2e_test/AGENTS.md`
|
||||
- `crates/iam/AGENTS.md`
|
||||
- `crates/kms/AGENTS.md`
|
||||
- `crates/policy/AGENTS.md`
|
||||
- `rustfs/src/admin/AGENTS.md`
|
||||
- `rustfs/src/storage/AGENTS.md`
|
||||
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
# ARCHITECTURE.md
|
||||
|
||||
> Last updated: 2026-04-13 · Revision: 1 (draft)
|
||||
>
|
||||
> This document describes the high-level architecture of RustFS.
|
||||
> If you want to familiarize yourself with the code base, you are in the right place!
|
||||
>
|
||||
> See also [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow.
|
||||
|
||||
## Bird's Eye View
|
||||
|
||||
RustFS is a high-performance, S3-compatible distributed object storage system written
|
||||
in Rust. It uses erasure coding for data durability, supports multi-tenancy through
|
||||
IAM/STS, and provides a web-based admin console.
|
||||
|
||||
A running RustFS node exposes:
|
||||
|
||||
- **S3 API** (port 9000) — the primary data path for object CRUD
|
||||
- **Admin API** (port 9000, `/minio/` prefix) — cluster management, IAM, metrics
|
||||
- **Console** (port 9001) — web UI backed by the Admin API
|
||||
- **Inter-node RPC** (gRPC/tonic) — cluster communication for distributed mode
|
||||
|
||||
The core data flow for a PUT request looks like:
|
||||
|
||||
```
|
||||
HTTP request
|
||||
→ server (TLS, auth, routing, compression)
|
||||
→ app/object_usecase (validation, policy, lifecycle)
|
||||
→ storage/ecfs (erasure coding, encryption, checksums)
|
||||
→ ecstore (disk pool selection, data distribution)
|
||||
→ rio (reader pipeline: encrypt → compress → hash → write)
|
||||
→ io-core (zero-copy I/O, buffer pool, direct I/O)
|
||||
→ local disk / remote disk via RPC
|
||||
```
|
||||
|
||||
## Code Map
|
||||
|
||||
The repository is a Cargo workspace with a flat `crates/` layout:
|
||||
|
||||
```
|
||||
rustfs/ # Workspace root (virtual manifest)
|
||||
├── rustfs/ # Main binary + library crate (75K lines)
|
||||
│ └── src/
|
||||
│ ├── main.rs # Entry point, startup sequence
|
||||
│ ├── lib.rs # Module tree root
|
||||
│ ├── server/ # HTTP server, TLS, routing, middleware
|
||||
│ ├── admin/ # Admin API handlers and console
|
||||
│ ├── app/ # Use-case layer (object, bucket, multipart)
|
||||
│ ├── storage/ # Storage engine interface and implementation
|
||||
│ ├── auth.rs # S3 request authentication
|
||||
│ ├── config/ # CLI args, config parsing, workload profiles
|
||||
│ └── ...
|
||||
├── crates/ # 39 library crates
|
||||
│ ├── ecstore/ # Erasure-coded storage engine (⚠️ 87K lines)
|
||||
│ ├── rio/ # Reader I/O pipeline (encrypt, compress, hash)
|
||||
│ ├── io-core/ # Zero-copy I/O, scheduling, buffer pool
|
||||
│ ├── io-metrics/ # I/O metrics collection
|
||||
│ ├── common/ # Shared runtime state, globals, data usage types
|
||||
│ ├── config/ # Configuration types and parsing
|
||||
│ ├── utils/ # Pure utility functions
|
||||
│ ├── ... # (see "Crate Reference" below)
|
||||
│ └── e2e_test/ # End-to-end integration tests
|
||||
└── docs/ # Design documents and analysis
|
||||
```
|
||||
|
||||
### Main Crate Layers (`rustfs/src/`)
|
||||
|
||||
The main crate is organized in layers, top to bottom:
|
||||
|
||||
| Layer | Directory | Responsibility |
|
||||
|-------|-----------|----------------|
|
||||
| **Server** | `server/` | HTTP listener, TLS, CORS, compression, middleware, graceful shutdown |
|
||||
| **Admin** | `admin/` | Admin API routing, 30+ handler modules, web console |
|
||||
| **App** | `app/` | Use-case orchestration: object_usecase, bucket_usecase, multipart_usecase |
|
||||
| **Storage** | `storage/` | S3 API translation, erasure-coded FS, SSE encryption, RPC, concurrency |
|
||||
| **Auth** | `auth.rs` | S3 signature verification, credential validation |
|
||||
| **Config** | `config/` | CLI parsing, config struct, workload profiles |
|
||||
|
||||
A request flows **downward** through the layers. No layer should reach upward
|
||||
(e.g., storage must not import from admin).
|
||||
|
||||
### Crate Reference
|
||||
|
||||
Crates are organized in a dependency DAG with 9 depth levels (0 = leaf, 8 = top):
|
||||
|
||||
```
|
||||
Depth 0 — LEAF (no internal deps):
|
||||
appauth, checksums, config, credentials, crypto, io-metrics,
|
||||
madmin, s3-common, workers, zip
|
||||
|
||||
Depth 1:
|
||||
io-core (→ io-metrics)
|
||||
policy (→ config, credentials, crypto)
|
||||
utils (→ config) ⚠️ inverted: utils should be leaf
|
||||
|
||||
Depth 2:
|
||||
concurrency, filemeta, keystone, kms, lock, obs,
|
||||
signer, targets, trusted-proxies
|
||||
|
||||
Depth 3:
|
||||
common (→ filemeta, madmin) ⚠️ inverted: common should be leaf
|
||||
|
||||
Depth 4:
|
||||
object-capacity, protos, rio
|
||||
|
||||
Depth 5 — CORE:
|
||||
ecstore (16 internal deps, 11 dependents — the architectural heart)
|
||||
|
||||
Depth 6:
|
||||
audit, heal, iam, metrics, notify, s3select-api, scanner
|
||||
|
||||
Depth 7:
|
||||
object-io, protocols, s3select-query
|
||||
|
||||
Depth 8 — TOP:
|
||||
rustfs (35 internal deps — the binary, depends on almost everything)
|
||||
```
|
||||
|
||||
#### By Domain
|
||||
|
||||
**Core Infrastructure:**
|
||||
|
||||
| Crate | Lines | Purpose |
|
||||
|-------|-------|---------|
|
||||
| `config` | 3.3K | Configuration types and environment parsing |
|
||||
| `utils` | 8.7K | Pure utilities (paths, compression, network, retry) |
|
||||
| `common` | 4.4K | Shared runtime state, globals, data usage types, metrics |
|
||||
| `madmin` | 5.5K | Admin API request/response types |
|
||||
|
||||
**I/O Pipeline:**
|
||||
|
||||
| Crate | Lines | Purpose |
|
||||
|-------|-------|---------|
|
||||
| `io-core` | 6.5K | Zero-copy I/O, buffer pool, direct I/O, scheduling, backpressure |
|
||||
| `io-metrics` | 4.5K | I/O operation metrics and counters |
|
||||
| `rio` | 6.9K | Composable reader chain (encrypt → compress → hash → limit) |
|
||||
| `object-io` | 2.4K | High-level object read/write using rio + ecstore |
|
||||
| `concurrency` | 1.8K | Concurrency control wrappers over io-core |
|
||||
|
||||
**Storage Engine:**
|
||||
|
||||
| Crate | Lines | Purpose |
|
||||
|-------|-------|---------|
|
||||
| `ecstore` | 87K | ⚠️ Erasure-coded storage: disks, pools, buckets, replication, lifecycle |
|
||||
| `filemeta` | 10K | File/object metadata types and versioning |
|
||||
| `checksums` | 732 | Checksum computation |
|
||||
| `lock` | 7.1K | Distributed lock manager |
|
||||
| `heal` | 5.9K | Data healing / bitrot repair |
|
||||
| `scanner` | 5.4K | Background data usage scanner |
|
||||
| `object-capacity` | 2.5K | Capacity tracking and management |
|
||||
|
||||
**Security & Auth:**
|
||||
|
||||
| Crate | Lines | Purpose |
|
||||
|-------|-------|---------|
|
||||
| `crypto` | 1.6K | Encryption primitives |
|
||||
| `credentials` | 713 | Credential types (access key / secret key) |
|
||||
| `signer` | 1.4K | S3 v4 request signing |
|
||||
| `iam` | 9.0K | Identity and access management |
|
||||
| `policy` | 8.8K | Policy engine (S3 bucket/IAM policies) |
|
||||
| `kms` | 8.1K | Key management service integration |
|
||||
| `keystone` | 1.9K | OpenStack Keystone auth |
|
||||
| `appauth` | 143 | Application-level auth tokens |
|
||||
|
||||
**Protocol & API:**
|
||||
|
||||
| Crate | Lines | Purpose |
|
||||
|-------|-------|---------|
|
||||
| `protos` | 5.7K | Protobuf/gRPC definitions for inter-node RPC |
|
||||
| `protocols` | 18K | FTP/FTPS, WebDAV, Swift API support |
|
||||
| `s3-common` | 738 | Shared S3 types |
|
||||
| `s3select-api` | 1.9K | S3 Select interface |
|
||||
| `s3select-query` | 3.6K | S3 Select query engine |
|
||||
|
||||
**Observability:**
|
||||
|
||||
| Crate | Lines | Purpose |
|
||||
|-------|-------|---------|
|
||||
| `metrics` | 8.4K | Prometheus metric collectors |
|
||||
| `io-metrics` | 4.5K | I/O-specific metrics |
|
||||
| `obs` | 5.6K | OpenTelemetry tracing and telemetry |
|
||||
| `audit` | 2.4K | Audit logging |
|
||||
|
||||
**Events:**
|
||||
|
||||
| Crate | Lines | Purpose |
|
||||
|-------|-------|---------|
|
||||
| `notify` | 5.5K | Event notification system |
|
||||
| `targets` | 3.2K | Notification targets (Kafka, AMQP, webhook, etc.) |
|
||||
|
||||
**Other:**
|
||||
|
||||
| Crate | Lines | Purpose |
|
||||
|-------|-------|---------|
|
||||
| `trusted-proxies` | 4.0K | Trusted proxy / IP forwarding |
|
||||
| `zip` | 986 | ZIP archive support for bulk downloads |
|
||||
| `workers` | 136 | Simple worker abstraction |
|
||||
|
||||
## Architecture Invariants
|
||||
|
||||
> These are rules that the codebase should follow. Some are currently violated
|
||||
> (marked with ⚠️). Documenting them here makes the violations explicit and
|
||||
> trackable.
|
||||
|
||||
1. **Layers flow downward.** Server → Admin/App → Storage → ecstore → rio/io-core.
|
||||
No upward imports.
|
||||
|
||||
2. **Leaf crates have zero internal dependencies.** `config`, `credentials`, `crypto`,
|
||||
`io-metrics`, `madmin`, `s3-common` should depend only on external crates.
|
||||
- ⚠️ VIOLATED: `utils` depends on `config`, `common` depends on `filemeta` and `madmin`.
|
||||
|
||||
3. **Each type has exactly one definition.** Types shared across crates must be defined
|
||||
in one crate and re-exported or imported by others.
|
||||
- ⚠️ VIOLATED: `ReplicationStats` (4 copies), `LastMinuteLatency` (3 copies),
|
||||
`BackpressureConfig` (3 copies), `DataUsageInfo` (2 copies).
|
||||
|
||||
4. **ecstore does not know about HTTP or S3 protocol details.** It operates on
|
||||
storage-level abstractions (objects, buckets, disks, pools).
|
||||
|
||||
5. **The `rustfs` binary crate is the only place that wires everything together.**
|
||||
Individual crates should be testable in isolation.
|
||||
|
||||
6. **Error types use `thiserror` with descriptive names** (e.g., `StorageError`,
|
||||
not bare `Error`).
|
||||
- ⚠️ VIOLATED: 6 crates use `pub enum Error`; 2 crates use `snafu`;
|
||||
`heal` use `anyhow` in library code.
|
||||
|
||||
## Known Structural Issues
|
||||
|
||||
> This section documents known problems in the current architecture.
|
||||
> It exists so the team can track and address them deliberately.
|
||||
|
||||
### Critical
|
||||
|
||||
- **common/scanner code duplication (~3K lines).** `scanner` depends on `common`
|
||||
but maintains its own copies of `DataUsageInfo`, `LastMinuteLatency`, and related
|
||||
types instead of importing them.
|
||||
|
||||
- **ecstore is a monolith (87K lines, 163 files).** It contains disk management,
|
||||
bucket management, erasure coding, replication, lifecycle, RPC, and configuration
|
||||
— all in one crate. It should be decomposed along its existing subdirectories.
|
||||
|
||||
### High
|
||||
|
||||
- **Dependency inversions.** `utils → config` and `common → filemeta/madmin` break
|
||||
the layering model. These need to be untangled.
|
||||
|
||||
- **Three-layer BackpressureConfig/DeadlockConfig duplication** across io-core,
|
||||
concurrency, and rustfs/storage. Should be defined once with builder/composition.
|
||||
|
||||
### Medium
|
||||
|
||||
- **Inconsistent error handling.** Three strategies (thiserror/snafu/anyhow) and
|
||||
mixed naming (bare `Error` vs descriptive names).
|
||||
|
||||
- **Ambiguous common vs utils boundary.** Both described as "utilities and data
|
||||
structures." Need clear ownership rules.
|
||||
|
||||
## Cross-Cutting Concerns
|
||||
|
||||
### Error Handling
|
||||
|
||||
The project convention is `thiserror` for typed errors with descriptive names.
|
||||
See `AGENTS.md`: "Prefer thiserror for library-facing error types."
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum StorageError {
|
||||
#[error("disk not found: {0}")]
|
||||
DiskNotFound(String),
|
||||
}
|
||||
|
||||
// AVOID
|
||||
pub enum Error { ... } // too generic
|
||||
anyhow::Result<T> // in library code (OK in tests/CLI)
|
||||
```
|
||||
|
||||
### Logging & Tracing
|
||||
|
||||
- Use `tracing` crate (`info!`, `warn!`, `error!`, `debug!`, `trace!`)
|
||||
- Structured fields: `tracing::info!(bucket = %name, "created bucket")`
|
||||
- Spans for request-scoped context
|
||||
|
||||
### Metrics
|
||||
|
||||
- Prometheus-style metrics via `rustfs-obs` runtime and schema
|
||||
- I/O-specific counters via `rustfs-io-metrics`
|
||||
- Registration happens at crate level, collection/reporting in `rustfs-obs`
|
||||
|
||||
### Testing
|
||||
|
||||
- Unit tests: `#[cfg(test)] mod tests` in the same file
|
||||
- Integration tests: inside respective crates (not top-level `tests/`)
|
||||
- E2E tests: `crates/e2e_test/` — tests against a running server
|
||||
- Run all: `make test` or `cargo nextest run`
|
||||
|
||||
## Startup Sequence
|
||||
|
||||
The binary (`main.rs`) boots in this order:
|
||||
|
||||
1. Environment variable compatibility (`MINIO_*` → `RUSTFS_*`)
|
||||
2. Tokio runtime construction
|
||||
3. CLI argument parsing
|
||||
4. License, observability, TLS, trusted proxies initialization
|
||||
5. Config parsing, server address resolution
|
||||
6. Credentials, endpoints, local disks, lock client initialization
|
||||
7. Capacity management initialization
|
||||
8. HTTP server start (S3 API + optional console)
|
||||
9. ECStore initialization (erasure coding storage engine)
|
||||
10. Global config, background replication, KMS
|
||||
11. Optional: FTP/FTPS/WebDAV servers
|
||||
12. Event notifier, audit system, deadlock detector
|
||||
13. Bucket metadata, IAM, Keystone, OIDC
|
||||
14. Scanner and heal manager
|
||||
15. Metrics system, mark `FullReady`
|
||||
16. Wait for shutdown signal → graceful shutdown
|
||||
|
||||
## Dependency Diagram (Simplified)
|
||||
|
||||
```
|
||||
┌─────────┐
|
||||
│ rustfs │ (binary + lib, 75K lines)
|
||||
│ main │
|
||||
└────┬────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
┌────▼────┐ ┌────▼────┐ ┌──────▼─────┐
|
||||
│ server │ │ admin │ │ app │
|
||||
│ (HTTP) │ │(console)│ │(use-cases) │
|
||||
└────┬────┘ └────┬────┘ └──────┬─────┘
|
||||
│ │ │
|
||||
└───────────────┼───────────────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ storage │
|
||||
│ (ecfs, SSE, │
|
||||
│ RPC, ACL) │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────────────────┼──────────────────┐
|
||||
│ │ │
|
||||
┌─────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
|
||||
│ ecstore │ │ rio │ │ io-core │
|
||||
│ (87K,core) │ │ (readers) │ │ (zero-copy) │
|
||||
└─────┬──────┘ └─────────────┘ └─────────────┘
|
||||
│
|
||||
┌─────┬──┼──┬─────┬──────┐
|
||||
│ │ │ │ │ │
|
||||
common utils config policy filemeta ...
|
||||
```
|
||||
|
||||
## How to Navigate
|
||||
|
||||
- **"Where does S3 PutObject go?"**
|
||||
`server/` routes → `app/object_usecase` validates → `storage/ecfs` encodes →
|
||||
`ecstore` distributes → `rio` encrypts/compresses → `io-core` writes
|
||||
|
||||
- **"Where are bucket policies enforced?"**
|
||||
`app/bucket_usecase` calls into `crates/policy/`
|
||||
|
||||
- **"Where is replication configured?"**
|
||||
`admin/handlers/replication.rs` and `admin/handlers/site_replication.rs` for API,
|
||||
`ecstore/src/bucket/replication/` for engine
|
||||
|
||||
- **"Where do I add a new admin endpoint?"**
|
||||
Add handler in `admin/handlers/`, register in `admin/router.rs`
|
||||
|
||||
- **"Where do I add a new metric?"**
|
||||
Define descriptor/collector in `crates/obs/src/metrics/`, expose via `/minio/v2/metrics`
|
||||
|
||||
---
|
||||
|
||||
*Inspired by [matklad's ARCHITECTURE.md](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html)
|
||||
and [rust-analyzer's architecture.md](https://github.com/rust-analyzer/rust-analyzer/blob/master/docs/book/src/contributing/architecture.md).*
|
||||
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **Helm Ingress**: `customAnnotations` are now merged with class-specific annotations (nginx/traefik) instead of being ignored when `ingress.className` is set.
|
||||
|
||||
### Added
|
||||
- **OpenStack Keystone Authentication Integration**: Full support for OpenStack Keystone authentication via X-Auth-Token headers
|
||||
- Tower-based middleware (`KeystoneAuthLayer`) self-contained within `rustfs-keystone` crate
|
||||
|
||||
Generated
+2200
-1250
File diff suppressed because it is too large
Load Diff
+121
-98
@@ -31,14 +31,15 @@ members = [
|
||||
"crates/kms", # Key Management Service
|
||||
"crates/lock", # Distributed locking implementation
|
||||
"crates/madmin", # Management dashboard and admin API interface
|
||||
"crates/mcp", # MCP server for S3 operations
|
||||
"crates/metrics", # Metrics collection and reporting
|
||||
"crates/notify", # Notification system for events
|
||||
"crates/obs", # Observability utilities
|
||||
"crates/object-capacity", # Capacity scan and refresh core
|
||||
"crates/policy", # Policy management
|
||||
"crates/protocols", # Protocol implementations (FTPS, SFTP, etc.)
|
||||
"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/s3select-api", # S3 Select API interface
|
||||
"crates/s3select-query", # S3 Select query engine
|
||||
"crates/scanner", # Scanner for data integrity checks and health monitoring
|
||||
@@ -47,16 +48,18 @@ members = [
|
||||
"crates/trusted-proxies", # Trusted proxies management
|
||||
"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
|
||||
]
|
||||
resolver = "2"
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/rustfs/rustfs"
|
||||
rust-version = "1.93.0"
|
||||
version = "0.0.5"
|
||||
version = "1.0.0-beta.1"
|
||||
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"]
|
||||
@@ -68,62 +71,70 @@ unsafe_code = "deny"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
all = "warn"
|
||||
needless_collect = "warn"
|
||||
redundant_clone = "warn"
|
||||
|
||||
[workspace.dependencies]
|
||||
# RustFS Internal Crates
|
||||
rustfs = { path = "./rustfs", version = "0.0.5" }
|
||||
rustfs-heal = { path = "crates/heal", version = "0.0.5" }
|
||||
rustfs-appauth = { path = "crates/appauth", version = "0.0.5" }
|
||||
rustfs-audit = { path = "crates/audit", version = "0.0.5" }
|
||||
rustfs-checksums = { path = "crates/checksums", version = "0.0.5" }
|
||||
rustfs-common = { path = "crates/common", version = "0.0.5" }
|
||||
rustfs-config = { path = "./crates/config", version = "0.0.5" }
|
||||
rustfs-credentials = { path = "crates/credentials", version = "0.0.5" }
|
||||
rustfs-crypto = { path = "crates/crypto", version = "0.0.5" }
|
||||
rustfs-ecstore = { path = "crates/ecstore", version = "0.0.5" }
|
||||
rustfs-filemeta = { path = "crates/filemeta", version = "0.0.5" }
|
||||
rustfs-iam = { path = "crates/iam", version = "0.0.5" }
|
||||
rustfs-keystone = { path = "crates/keystone", version = "0.0.5" }
|
||||
rustfs-kms = { path = "crates/kms", version = "0.0.5" }
|
||||
rustfs-lock = { path = "crates/lock", version = "0.0.5" }
|
||||
rustfs-madmin = { path = "crates/madmin", version = "0.0.5" }
|
||||
rustfs-mcp = { path = "crates/mcp", version = "0.0.5" }
|
||||
rustfs-metrics = { path = "crates/metrics", version = "0.0.5" }
|
||||
rustfs-notify = { path = "crates/notify", version = "0.0.5" }
|
||||
rustfs-obs = { path = "crates/obs", version = "0.0.5" }
|
||||
rustfs-policy = { path = "crates/policy", version = "0.0.5" }
|
||||
rustfs-protos = { path = "crates/protos", version = "0.0.5" }
|
||||
rustfs-rio = { path = "crates/rio", version = "0.0.5" }
|
||||
rustfs-s3select-api = { path = "crates/s3select-api", version = "0.0.5" }
|
||||
rustfs-s3select-query = { path = "crates/s3select-query", version = "0.0.5" }
|
||||
rustfs-scanner = { path = "crates/scanner", version = "0.0.5" }
|
||||
rustfs-signer = { path = "crates/signer", version = "0.0.5" }
|
||||
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "0.0.5" }
|
||||
rustfs-targets = { path = "crates/targets", version = "0.0.5" }
|
||||
rustfs-utils = { path = "crates/utils", version = "0.0.5" }
|
||||
rustfs-workers = { path = "crates/workers", version = "0.0.5" }
|
||||
rustfs-zip = { path = "./crates/zip", version = "0.0.5" }
|
||||
rustfs-protocols = { path = "crates/protocols", version = "0.0.5" }
|
||||
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" }
|
||||
|
||||
# Async Runtime and Networking
|
||||
async-channel = "2.5.0"
|
||||
async-compression = { version = "0.4.41" }
|
||||
async-compression = { version = "0.4.42" }
|
||||
async-recursion = "1.1.1"
|
||||
async-trait = "0.1.89"
|
||||
axum = "0.8.8"
|
||||
async-nats = "0.47.0"
|
||||
axum = "0.8.9"
|
||||
futures = "0.3.32"
|
||||
futures-core = "0.3.32"
|
||||
futures-util = "0.3.32"
|
||||
pollster = "0.4.0"
|
||||
hyper = { version = "1.8.1", features = ["http2", "http1", "server"] }
|
||||
hyper-rustls = { version = "0.27.7", default-features = false, features = ["native-tokio", "http1", "tls12", "logging", "http2", "aws-lc-rs", "webpki-roots"] }
|
||||
pulsar = { version = "6.7.2", default-features = false, features = ["tokio-rustls-runtime"] }
|
||||
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"] }
|
||||
http = "1.4.0"
|
||||
http-body = "1.0.1"
|
||||
http-body-util = "0.1.3"
|
||||
reqwest = { version = "0.13.2", default-features = false, features = ["rustls", "charset", "http2", "system-proxy", "stream", "json", "blocking", "query", "form"] }
|
||||
socket2 = { version = "0.6.2", features = ["all"] }
|
||||
tokio = { version = "1.49.0", features = ["fs", "rt-multi-thread"] }
|
||||
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-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"
|
||||
@@ -141,147 +152,157 @@ byteorder = "1.5.0"
|
||||
flatbuffers = "25.12.19"
|
||||
form_urlencoded = "1.2.2"
|
||||
prost = "0.14.3"
|
||||
quick-xml = "0.39.2"
|
||||
rmcp = { version = "0.17.0" }
|
||||
quick-xml = "0.39.3"
|
||||
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_urlencoded = "0.7.1"
|
||||
schemars = "1.2.1"
|
||||
|
||||
# Cryptography and Security
|
||||
aes-gcm = { version = "0.11.0-rc.3", features = ["rand_core"] }
|
||||
argon2 = { version = "0.6.0-rc.7" }
|
||||
blake3 = { version = "1.8.3", features = ["rayon", "mmap"] }
|
||||
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"
|
||||
hmac = { version = "0.13.0-rc.5" }
|
||||
hmac = { version = "0.13.0" }
|
||||
jsonwebtoken = { version = "10.3.0", features = ["aws_lc_rs"] }
|
||||
openidconnect = { version = "4.0", default-features = false }
|
||||
pbkdf2 = "0.13.0-rc.9"
|
||||
rsa = { version = "0.10.0-rc.16" }
|
||||
rustls = { version = "0.23.37", default-features = false, features = ["aws-lc-rs", "logging", "tls12", "prefer-post-quantum", "std"] }
|
||||
rustls-pki-types = "1.14.0"
|
||||
sha1 = "0.11.0-rc.5"
|
||||
sha2 = "0.11.0-rc.5"
|
||||
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-pki-types = "1.14.1"
|
||||
sha1 = "0.11.0"
|
||||
sha2 = "0.11.0"
|
||||
subtle = "2.6"
|
||||
zeroize = { version = "1.8.2", features = ["derive"] }
|
||||
|
||||
# Time and Date
|
||||
chrono = { version = "0.4.44", features = ["serde"] }
|
||||
humantime = "2.3.0"
|
||||
jiff = { version = "0.2.22", features = ["serde"] }
|
||||
jiff = { version = "0.2.24", features = ["serde"] }
|
||||
time = { version = "0.3.47", features = ["std", "parsing", "formatting", "macros", "serde"] }
|
||||
|
||||
# Utilities and Tools
|
||||
anyhow = "1.0.102"
|
||||
arc-swap = "1.8.2"
|
||||
astral-tokio-tar = "0.5.6"
|
||||
arc-swap = "1.9.1"
|
||||
astral-tokio-tar = "0.6.1"
|
||||
atoi = "2.0.0"
|
||||
atomic_enum = "0.3.0"
|
||||
aws-config = { version = "1.8.14" }
|
||||
aws-credential-types = { version = "1.2.13" }
|
||||
aws-sdk-s3 = { version = "1.124.0", default-features = false, features = ["sigv4a", "default-https-client", "rt-tokio"] }
|
||||
aws-smithy-http-client = { version = "1.1.11", default-features = false, features = ["default-client", "rustls-aws-lc"] }
|
||||
aws-smithy-types = { version = "1.4.5" }
|
||||
aws-config = { version = "1.8.16" }
|
||||
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-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"
|
||||
base64 = "0.22.1"
|
||||
base64-simd = "0.8.0"
|
||||
brotli = "8.0.2"
|
||||
cfg-if = "1.0.4"
|
||||
clap = { version = "4.5.60", features = ["derive", "env"] }
|
||||
clap = { version = "4.6.1", features = ["derive", "env"] }
|
||||
const-str = { version = "1.1.0", features = ["std", "proc"] }
|
||||
convert_case = "0.11.0"
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
crossbeam-queue = "0.3.12"
|
||||
datafusion = "52.2.0"
|
||||
crossbeam-channel = "0.5.15"
|
||||
crossbeam-deque = "0.8.6"
|
||||
crossbeam-utils = "0.8.21"
|
||||
datafusion = "53.1.0"
|
||||
derive_builder = "0.20.2"
|
||||
enumset = "1.1.10"
|
||||
faster-hex = "0.10.0"
|
||||
flate2 = "1.1.9"
|
||||
glob = "0.3.3"
|
||||
google-cloud-storage = "1.8.0"
|
||||
google-cloud-auth = "1.6.0"
|
||||
hashbrown = { version = "0.16.1", features = ["serde", "rayon"] }
|
||||
google-cloud-storage = "1.11.0"
|
||||
google-cloud-auth = "1.9.0"
|
||||
hashbrown = { version = "0.17.0", features = ["serde", "rayon"] }
|
||||
hex = "0.4.3"
|
||||
hex-simd = "0.8.0"
|
||||
highway = { version = "1.3.0" }
|
||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||
lazy_static = "1.5.0"
|
||||
libc = "0.2.182"
|
||||
libc = "0.2.186"
|
||||
libsystemd = "0.7.2"
|
||||
local-ip-address = "0.6.10"
|
||||
local-ip-address = "0.6.12"
|
||||
memmap2 = "0.9.10"
|
||||
lz4 = "1.28.1"
|
||||
matchit = "0.9.1"
|
||||
md-5 = "0.11.0-rc.5"
|
||||
matchit = "0.9.2"
|
||||
md-5 = "0.11.0"
|
||||
md5 = "0.8.0"
|
||||
mime_guess = "2.0.5"
|
||||
moka = { version = "0.12.14", features = ["future"] }
|
||||
moka = { version = "0.12.15", features = ["future"] }
|
||||
netif = "0.1.6"
|
||||
num_cpus = { version = "1.17.0" }
|
||||
nvml-wrapper = "0.12.0"
|
||||
object_store = "0.12.5"
|
||||
nvml-wrapper = "0.12.1"
|
||||
object_store = "0.13.2"
|
||||
parking_lot = "0.12.5"
|
||||
path-absolutize = "3.1.1"
|
||||
path-clean = "1.0.1"
|
||||
percent-encoding = "2.3.2"
|
||||
pin-project-lite = "0.2.17"
|
||||
pretty_assertions = "1.4.1"
|
||||
rand = { version = "0.10.0", features = ["serde"] }
|
||||
ratelimit = "0.10.0"
|
||||
rayon = "1.11.0"
|
||||
reed-solomon-simd = { version = "3.1.0" }
|
||||
rand = { version = "0.10.1", features = ["serde"] }
|
||||
ratelimit = "0.10.1"
|
||||
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 = { version = "0.25.1" }
|
||||
rumqttc = { package = "rumqttc-next", version = "0.30.1", features = ["websocket"] }
|
||||
rustix = { version = "1.1.4", features = ["fs"] }
|
||||
rust-embed = { version = "8.11.0" }
|
||||
rustc-hash = { version = "2.1.1" }
|
||||
s3s = { version = "0.13.0", features = ["minio"] }
|
||||
rustc-hash = { version = "2.1.2" }
|
||||
s3s = { git = "https://github.com/rustfs/s3s", rev = "a3b16608df35aaeed8fff08b4988d03f4ca9445b", features = ["minio"] }
|
||||
serial_test = "3.4.0"
|
||||
shadow-rs = { version = "1.7.0", default-features = false }
|
||||
siphasher = "1.0.2"
|
||||
shadow-rs = { version = "2.0.0", default-features = false }
|
||||
siphasher = "1.0.3"
|
||||
smallvec = { version = "1.15.1", features = ["serde"] }
|
||||
smartstring = "1.0.1"
|
||||
snafu = "0.8.9"
|
||||
snafu = "0.9.0"
|
||||
snap = "1.1.1"
|
||||
starshard = { version = "1.1.0", features = ["rayon", "async", "serde"] }
|
||||
strum = { version = "0.28.0", features = ["derive"] }
|
||||
sysinfo = "0.38.3"
|
||||
sysinfo = "0.38.4"
|
||||
temp-env = "0.3.6"
|
||||
tempfile = "3.26.0"
|
||||
tempfile = "3.27.0"
|
||||
test-case = "3.3.1"
|
||||
thiserror = "2.0.18"
|
||||
tracing = { version = "0.1.44" }
|
||||
tracing-appender = "0.2.4"
|
||||
tracing-appender = "0.2.5"
|
||||
tracing-error = "0.2.1"
|
||||
tracing-opentelemetry = "0.32.1"
|
||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter", "time"] }
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter", "time"] }
|
||||
transform-stream = "0.3.1"
|
||||
url = "2.5.8"
|
||||
urlencoding = "2.1.3"
|
||||
uuid = { version = "1.21.0", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||
vaultrs = { version = "0.7.4" }
|
||||
uuid = { version = "1.23.1", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||
vaultrs = { version = "0.8.0" }
|
||||
walkdir = "2.5.0"
|
||||
wildmatch = { version = "2.6.1", features = ["serde"] }
|
||||
windows = { version = "0.62.2" }
|
||||
xxhash-rust = { version = "0.8.15", features = ["xxh64", "xxh3"] }
|
||||
zip = "8.1.0"
|
||||
zip = "8.6.0"
|
||||
zstd = "0.13.3"
|
||||
|
||||
# Observability and Metrics
|
||||
metrics = "0.24.3"
|
||||
metrics = "0.24.5"
|
||||
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.0", features = ["gzip-http", "reqwest-rustls"] }
|
||||
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"] }
|
||||
|
||||
# FTP and SFTP
|
||||
libunftp = { version = "0.23.0", features = ["experimental"] }
|
||||
unftp-core = "0.1.0"
|
||||
suppaftp = { version = "8.0.2", features = ["tokio", "tokio-rustls-aws-lc-rs"] }
|
||||
suppaftp = { version = "8.0.3", features = ["tokio", "tokio-rustls-aws-lc-rs"] }
|
||||
rcgen = "0.14.7"
|
||||
|
||||
# WebDAV
|
||||
dav-server = "0.11.0"
|
||||
|
||||
# Performance Analysis and Memory Profiling
|
||||
mimalloc = "0.1"
|
||||
# Use tikv-jemallocator as memory allocator and enable performance analysis
|
||||
@@ -291,10 +312,12 @@ tikv-jemalloc-ctl = { version = "0.6", features = ["use_std", "stats", "profilin
|
||||
# Used to generate pprof-compatible memory profiling data and support symbolization and flame graphs
|
||||
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"] }
|
||||
# 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"] }
|
||||
|
||||
[workspace.metadata.cargo-shear]
|
||||
ignored = ["rustfs", "rustfs-mcp"]
|
||||
ignored = ["rustfs"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
|
||||
+17
-10
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
FROM alpine:3.23 AS build
|
||||
FROM alpine:3.23.4 AS build
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG RELEASE=latest
|
||||
@@ -29,8 +29,20 @@ RUN set -eux; \
|
||||
if [ "$RELEASE" = "latest" ]; then \
|
||||
TAG="$(curl -fsSL https://api.github.com/repos/rustfs/rustfs/releases \
|
||||
| grep -o '"tag_name": "[^"]*"' | cut -d'"' -f4 | head -n 1)"; \
|
||||
RELEASE_JSON="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$TAG")"; \
|
||||
else \
|
||||
TAG="$RELEASE"; \
|
||||
RELEASE_JSON="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$TAG" 2>/dev/null || true)"; \
|
||||
if [ -z "$RELEASE_JSON" ]; then \
|
||||
if [ "${TAG#v}" = "$TAG" ]; then \
|
||||
ALT_TAG="v$TAG"; \
|
||||
else \
|
||||
ALT_TAG="${TAG#v}"; \
|
||||
fi; \
|
||||
echo "Primary tag lookup failed, retrying with alternate tag: $ALT_TAG"; \
|
||||
RELEASE_JSON="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$ALT_TAG")"; \
|
||||
TAG="$ALT_TAG"; \
|
||||
fi; \
|
||||
fi; \
|
||||
echo "Using tag: $TAG (arch pattern: $ARCH_SUBSTR)"; \
|
||||
# Find download URL in assets list for this tag that contains arch substring and ends with .zip
|
||||
@@ -54,7 +66,7 @@ RUN set -eux; \
|
||||
rm -rf rustfs.zip /build/.tmp || true
|
||||
|
||||
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.23.4
|
||||
|
||||
ARG RELEASE=latest
|
||||
ARG BUILD_DATE
|
||||
@@ -72,7 +84,8 @@ LABEL name="RustFS" \
|
||||
url="https://rustfs.com" \
|
||||
license="Apache-2.0"
|
||||
|
||||
RUN apk add --no-cache ca-certificates coreutils curl
|
||||
RUN apk update && \
|
||||
apk add --no-cache ca-certificates coreutils curl
|
||||
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=build /build/rustfs /usr/bin/rustfs
|
||||
@@ -86,13 +99,7 @@ RUN addgroup -g 10001 -S rustfs && \
|
||||
chown -R rustfs:rustfs /data /logs && \
|
||||
chmod 0750 /data /logs
|
||||
|
||||
ENV RUSTFS_ADDRESS=":9000" \
|
||||
RUSTFS_CONSOLE_ADDRESS=":9001" \
|
||||
RUSTFS_ACCESS_KEY="rustfsadmin" \
|
||||
RUSTFS_SECRET_KEY="rustfsadmin" \
|
||||
RUSTFS_CONSOLE_ENABLE="true" \
|
||||
RUSTFS_CORS_ALLOWED_ORIGINS="*" \
|
||||
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
|
||||
ENV RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
|
||||
RUSTFS_VOLUMES="/data" \
|
||||
RUSTFS_OBS_LOGGER_LEVEL=warn \
|
||||
RUSTFS_OBS_LOG_DIRECTORY=/logs \
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# 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.
|
||||
|
||||
FROM rust:1.93-trixie
|
||||
|
||||
RUN set -eux; \
|
||||
export DEBIAN_FRONTEND=noninteractive; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
lld \
|
||||
protobuf-compiler \
|
||||
flatbuffers-compiler; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /usr/src/rustfs
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN ./scripts/static.sh
|
||||
RUN cargo run --bin gproto
|
||||
RUN cargo build --release --locked --bin rustfs
|
||||
|
||||
RUN set -eux; \
|
||||
groupadd -g 10001 rustfs; \
|
||||
useradd -u 10001 -g rustfs -M -s /usr/sbin/nologin rustfs
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN set -eux; \
|
||||
mkdir -p /data /logs; \
|
||||
chown -R rustfs:rustfs /data /logs /app; \
|
||||
chmod 0750 /data /logs
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN install -m 0755 /usr/src/rustfs/target/release/rustfs /usr/bin/rustfs && chmod +x /entrypoint.sh
|
||||
|
||||
ENV RUSTFS_VOLUMES="/data" \
|
||||
RUST_LOG="warn" \
|
||||
RUSTFS_OBS_LOG_DIRECTORY="/logs" \
|
||||
RUSTFS_USERNAME="rustfs" \
|
||||
RUSTFS_GROUPNAME="rustfs" \
|
||||
RUSTFS_UID="10001" \
|
||||
RUSTFS_GID="10001"
|
||||
|
||||
EXPOSE 9000 9001
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["/usr/bin/rustfs"]
|
||||
+13
-3
@@ -31,14 +31,25 @@ RUN set -eux; \
|
||||
arm64) ARCH_SUBSTR="aarch64-gnu" ;; \
|
||||
*) echo "Unsupported TARGETARCH=$TARGETARCH" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
\
|
||||
if [ "$RELEASE" = "latest" ]; then \
|
||||
TAG="$(curl -fsSL https://api.github.com/repos/rustfs/rustfs/releases \
|
||||
| grep -o '"tag_name": "[^"]*"' | cut -d'"' -f4 | head -n 1)"; \
|
||||
RELEASE_JSON="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$TAG")"; \
|
||||
else \
|
||||
TAG="$RELEASE"; \
|
||||
RELEASE_JSON="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$TAG" 2>/dev/null || true)"; \
|
||||
if [ -z "$RELEASE_JSON" ]; then \
|
||||
if [ "${TAG#v}" = "$TAG" ]; then \
|
||||
ALT_TAG="v$TAG"; \
|
||||
else \
|
||||
ALT_TAG="${TAG#v}"; \
|
||||
fi; \
|
||||
echo "Primary tag lookup failed, retrying with alternate tag: $ALT_TAG"; \
|
||||
RELEASE_JSON="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$ALT_TAG")"; \
|
||||
TAG="$ALT_TAG"; \
|
||||
fi; \
|
||||
fi; \
|
||||
\
|
||||
echo "Using tag: $TAG (arch pattern: $ARCH_SUBSTR)"; \
|
||||
URL="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$TAG" \
|
||||
| grep -o "\"browser_download_url\": \"[^\"]*${ARCH_SUBSTR}[^\"]*\\.zip\"" \
|
||||
| cut -d'"' -f4 | head -n 1)"; \
|
||||
@@ -97,7 +108,6 @@ ENV RUSTFS_ADDRESS=":9000" \
|
||||
RUSTFS_ACCESS_KEY="rustfsadmin" \
|
||||
RUSTFS_SECRET_KEY="rustfsadmin" \
|
||||
RUSTFS_CONSOLE_ENABLE="true" \
|
||||
RUSTFS_CORS_ALLOWED_ORIGINS="*" \
|
||||
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
|
||||
RUSTFS_VOLUMES="/data" \
|
||||
RUSTFS_OBS_LOGGER_LEVEL=warn \
|
||||
|
||||
+32
-19
@@ -26,15 +26,17 @@
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
ARG TARGETARCH
|
||||
|
||||
# -----------------------------
|
||||
# Build stage
|
||||
# -----------------------------
|
||||
FROM rust:1.91-trixie AS builder
|
||||
FROM rust:1.93-trixie AS builder
|
||||
|
||||
# Re-declare args after FROM
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
ARG TARGETARCH
|
||||
|
||||
# Debug: print platforms
|
||||
RUN echo "Build info -> BUILDPLATFORM=${BUILDPLATFORM}, TARGETPLATFORM=${TARGETPLATFORM}"
|
||||
@@ -60,7 +62,15 @@ RUN set -eux; \
|
||||
|
||||
# Optional: cross toolchain for aarch64 (only when targeting linux/arm64)
|
||||
RUN set -eux; \
|
||||
if [ "${TARGETPLATFORM:-linux/amd64}" = "linux/arm64" ]; then \
|
||||
target_platform="${TARGETPLATFORM:-}"; \
|
||||
if [ -z "${target_platform}" ]; then \
|
||||
case "$(uname -m)" in \
|
||||
x86_64) target_platform="linux/amd64" ;; \
|
||||
aarch64|arm64) target_platform="linux/arm64" ;; \
|
||||
*) target_platform="linux/amd64" ;; \
|
||||
esac; \
|
||||
fi; \
|
||||
if [ "${target_platform}" = "linux/arm64" ]; then \
|
||||
export DEBIAN_FRONTEND=noninteractive; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu; \
|
||||
@@ -79,6 +89,7 @@ ENV CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
|
||||
ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc
|
||||
ENV CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc
|
||||
ENV CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++
|
||||
ENV CARGO_TARGET_DIR=/usr/src/rustfs/target/docker-build
|
||||
|
||||
WORKDIR /usr/src/rustfs
|
||||
|
||||
@@ -112,27 +123,36 @@ ENV CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
||||
# Generate protobuf/flatbuffers code (uses protoc/flatc from distro)
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,target=/usr/src/rustfs/target \
|
||||
--mount=type=cache,target=/usr/src/rustfs/target/docker-build \
|
||||
cargo run --bin gproto
|
||||
|
||||
# Build RustFS (target depends on TARGETPLATFORM)
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,target=/usr/src/rustfs/target \
|
||||
--mount=type=cache,target=/usr/src/rustfs/target/docker-build \
|
||||
set -eux; \
|
||||
case "${TARGETPLATFORM:-linux/amd64}" in \
|
||||
rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; \
|
||||
target_platform="${TARGETPLATFORM:-}"; \
|
||||
if [ -z "${target_platform}" ]; then \
|
||||
case "${TARGETARCH:-$(uname -m)}" in \
|
||||
amd64|x86_64) target_platform="linux/amd64" ;; \
|
||||
arm64|aarch64) target_platform="linux/arm64" ;; \
|
||||
*) echo "Unsupported target architecture: ${TARGETARCH:-$(uname -m)}" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
fi; \
|
||||
case "${target_platform}" in \
|
||||
linux/amd64) \
|
||||
echo "Building for x86_64-unknown-linux-gnu"; \
|
||||
cargo build --release --locked --target x86_64-unknown-linux-gnu --bin rustfs -j "$(nproc)"; \
|
||||
install -m 0755 target/x86_64-unknown-linux-gnu/release/rustfs /usr/local/bin/rustfs \
|
||||
install -m 0755 "${CARGO_TARGET_DIR}/x86_64-unknown-linux-gnu/release/rustfs" /usr/local/bin/rustfs \
|
||||
;; \
|
||||
linux/arm64) \
|
||||
echo "Building for aarch64-unknown-linux-gnu"; \
|
||||
cargo build --release --locked --target aarch64-unknown-linux-gnu --bin rustfs -j "$(nproc)"; \
|
||||
install -m 0755 target/aarch64-unknown-linux-gnu/release/rustfs /usr/local/bin/rustfs \
|
||||
install -m 0755 "${CARGO_TARGET_DIR}/aarch64-unknown-linux-gnu/release/rustfs" /usr/local/bin/rustfs \
|
||||
;; \
|
||||
*) \
|
||||
echo "Unsupported TARGETPLATFORM=${TARGETPLATFORM}" >&2; exit 1 \
|
||||
echo "Unsupported target platform=${target_platform}" >&2; exit 1 \
|
||||
;; \
|
||||
esac
|
||||
|
||||
@@ -157,11 +177,7 @@ WORKDIR /app
|
||||
ENV CARGO_INCREMENTAL=1
|
||||
|
||||
# Ensure we have the same default env vars available
|
||||
ENV RUSTFS_ADDRESS=":9000" \
|
||||
RUSTFS_ACCESS_KEY="rustfsadmin" \
|
||||
RUSTFS_SECRET_KEY="rustfsadmin" \
|
||||
RUSTFS_CONSOLE_ENABLE="true" \
|
||||
RUSTFS_VOLUMES="/data" \
|
||||
ENV RUSTFS_VOLUMES="/data" \
|
||||
RUST_LOG="warn" \
|
||||
RUSTFS_OBS_LOG_DIRECTORY="/logs" \
|
||||
RUSTFS_USERNAME="rustfs" \
|
||||
@@ -182,7 +198,7 @@ CMD ["cargo", "run", "--bin", "rustfs", "--"]
|
||||
# -----------------------------
|
||||
# Runtime stage (Ubuntu minimal)
|
||||
# -----------------------------
|
||||
FROM ubuntu:22.04
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
@@ -199,6 +215,7 @@ RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
tzdata \
|
||||
coreutils; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
@@ -222,11 +239,7 @@ COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /usr/bin/rustfs /entrypoint.sh
|
||||
|
||||
# Default environment (override in docker run/compose as needed)
|
||||
ENV RUSTFS_ADDRESS=":9000" \
|
||||
RUSTFS_ACCESS_KEY="rustfsadmin" \
|
||||
RUSTFS_SECRET_KEY="rustfsadmin" \
|
||||
RUSTFS_CONSOLE_ENABLE="true" \
|
||||
RUSTFS_VOLUMES="/data" \
|
||||
ENV RUSTFS_VOLUMES="/data" \
|
||||
RUST_LOG="warn" \
|
||||
RUSTFS_USERNAME="rustfs" \
|
||||
RUSTFS_GROUPNAME="rustfs" \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[](https://rustfs.com)
|
||||
[](https://rustfs.com)
|
||||
|
||||
<p align="center">RustFS is a high-performance, distributed object storage system built in Rust.</p>
|
||||
|
||||
@@ -42,6 +42,7 @@ Unlike other storage systems, RustFS is released under the permissible Apache 2.
|
||||
- **High Performance**: Built with Rust to ensure maximum speed and resource efficiency.
|
||||
- **Distributed Architecture**: Scalable and fault-tolerant design suitable for large-scale deployments.
|
||||
- **S3 Compatibility**: Seamless integration with existing S3-compatible applications and tools.
|
||||
- **OpenStack Swift API**: Native support for Swift protocol with Keystone authentication.
|
||||
- **OpenStack Keystone Integration**: Native support for OpenStack Keystone authentication with X-Auth-Token headers.
|
||||
- **Data Lake Support**: Optimized for high-throughput big data and AI workloads.
|
||||
- **Open Source**: Licensed under Apache 2.0, encouraging unrestricted community contributions and commercial usage.
|
||||
@@ -56,6 +57,7 @@ Unlike other storage systems, RustFS is released under the permissible Apache 2.
|
||||
| **Event Notifications** | ✅ Available | **Distributed Mode** | 🚧 Under Testing |
|
||||
| **K8s Helm Charts** | ✅ Available | **RustFS KMS** | 🚧 Under Testing |
|
||||
| **Keystone Auth** | ✅ Available | **Multi-Tenancy** | ✅ Available |
|
||||
| **Swift API** | ✅ Available | **Swift Metadata Ops** | 🚧 Partial |
|
||||
|
||||
## RustFS vs MinIO Performance
|
||||
|
||||
@@ -98,28 +100,28 @@ To get started with RustFS, follow these steps:
|
||||
curl -O https://rustfs.com/install_rustfs.sh && bash install_rustfs.sh
|
||||
```
|
||||
|
||||
### 2\. Docker Quick Start (Option 2)
|
||||
### 2. Docker Quick Start (Option 2)
|
||||
|
||||
The RustFS container runs as a non-root user `rustfs` (UID `10001`). If you run Docker with `-v` to mount a host directory, please ensure the host directory owner is set to `10001`, otherwise you will encounter permission denied errors.
|
||||
|
||||
```bash
|
||||
# Create data and logs directories
|
||||
mkdir -p data logs
|
||||
# Create data and logs directories
|
||||
mkdir -p data logs
|
||||
|
||||
# Change the owner of these directories
|
||||
chown -R 10001:10001 data logs
|
||||
# Change the owner of these directories
|
||||
chown -R 10001:10001 data logs
|
||||
|
||||
# Using latest version
|
||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
||||
# Using latest version
|
||||
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:1.0.0-alpha.76
|
||||
# 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
|
||||
```
|
||||
|
||||
If you use [podman](https://github.com/containers/podman) instead of docker, you can install the RustFS with the below command
|
||||
|
||||
```bash
|
||||
podman run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
||||
podman run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
||||
```
|
||||
|
||||
You can also use Docker Compose. Using the `docker-compose.yml` file in the root directory:
|
||||
@@ -134,7 +136,23 @@ Similarly, you can run the command with podman
|
||||
podman compose --profile observability up -d
|
||||
```
|
||||
|
||||
**NOTE**: We recommend reviewing the `docker-compose.yaml` 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.
|
||||
Webhook notification quick start (Docker):
|
||||
|
||||
```bash
|
||||
docker run -d --name rustfs -p 9000:9000 \
|
||||
-e RUSTFS_NOTIFY_ENABLE=true \
|
||||
-e RUSTFS_NOTIFY_WEBHOOK_ENABLE_PRIMARY=on \
|
||||
-e RUSTFS_NOTIFY_WEBHOOK_ENDPOINT_PRIMARY=http://<host-ip>:3020/webhook \
|
||||
-e RUSTFS_NOTIFY_WEBHOOK_QUEUE_DIR_PRIMARY=/tmp/rustfs-events \
|
||||
rustfs/rustfs:latest
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
### 3\. Build from Source (Option 3) - Advanced Users
|
||||
|
||||
@@ -193,6 +211,19 @@ nix build
|
||||
nix run
|
||||
```
|
||||
|
||||
### 6\. X-CMD (Option 6)
|
||||
|
||||
If you are an [x-cmd](https://www.x-cmd.com/install/rustfs) user:
|
||||
|
||||
```bash
|
||||
# Run directly without installing
|
||||
x rustfs
|
||||
|
||||
# Download the binary and install it to the global environment
|
||||
x env use rustfs
|
||||
rustfs --help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Accessing RustFS
|
||||
@@ -204,6 +235,48 @@ nix run
|
||||
|
||||
**NOTE**: To access the RustFS instance via `https`, please refer to the [TLS Configuration Docs](https://docs.rustfs.com/integration/tls-configured.html).
|
||||
|
||||
### OIDC Roles Claim (Microsoft Entra ID)
|
||||
|
||||
RustFS supports mapping an OIDC claim containing role values into the existing
|
||||
authorization pipeline. The `roles_claim` setting is **optional**: when unset or
|
||||
empty, only the `groups` claim contributes to authorization (same as older
|
||||
RustFS releases). For Microsoft Entra ID app roles, set `roles_claim=roles` so
|
||||
both console admin checks and bucket IAM policies can evaluate those roles.
|
||||
|
||||
Example environment configuration (opt-in roles claim):
|
||||
|
||||
```bash
|
||||
RUSTFS_IDENTITY_OPENID_ENABLE=on
|
||||
RUSTFS_IDENTITY_OPENID_CONFIG_URL="https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration"
|
||||
RUSTFS_IDENTITY_OPENID_CLIENT_ID="<client-id>"
|
||||
RUSTFS_IDENTITY_OPENID_CLIENT_SECRET="<client-secret>"
|
||||
RUSTFS_IDENTITY_OPENID_SCOPES="openid,profile,email"
|
||||
RUSTFS_IDENTITY_OPENID_GROUPS_CLAIM="groups"
|
||||
RUSTFS_IDENTITY_OPENID_ROLES_CLAIM="roles"
|
||||
```
|
||||
|
||||
Policy condition example (evaluate app roles directly with `jwt:roles`; when
|
||||
`roles_claim` is configured, RustFS also merges those values into `jwt:groups`
|
||||
for backward compatibility with older policies):
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["admin:*"],
|
||||
"Resource": ["arn:aws:s3:::*"],
|
||||
"Condition": {
|
||||
"ForAnyValue:StringEquals": {
|
||||
"jwt:roles": ["RustFS.ConsoleAdmin"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For detailed documentation, including configuration options, API references, and advanced usage, please visit our [Documentation](https://docs.rustfs.com).
|
||||
|
||||
+35
-5
@@ -100,7 +100,7 @@ curl -O https://rustfs.com/install_rustfs.sh && bash install_rustfs.sh
|
||||
|
||||
### 2\. Docker 快速启动 (选项 2)
|
||||
|
||||
RustFS 容器以非 root 用户 `rustfs` (UID `10001`) 运行。如果您使用 Docker 的 `-v` 参数挂载宿主机目录,请务必确保宿主机目录的所有者已更改为 `1000`,否则会遇到权限拒绝错误。
|
||||
RustFS 容器以非 root 用户 `rustfs` (UID `10001`) 运行。如果您使用 Docker 的 `-v` 参数挂载宿主机目录,请务必确保宿主机目录的所有者已更改为 `10001`,否则会遇到权限拒绝错误。
|
||||
|
||||
```bash
|
||||
# 创建数据和日志目录
|
||||
@@ -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:1.0.0.alpha.68
|
||||
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 Compose。使用根目录下的 `docker-compose.yml` 文件:
|
||||
@@ -122,7 +122,7 @@ RustFS 容器以非 root 用户 `rustfs` (UID `10001`) 运行。如果您使用
|
||||
docker compose --profile observability up -d
|
||||
```
|
||||
|
||||
**注意**: 我们建议您在运行前查看 `docker-compose.yaml` 文件。该文件定义了包括 Grafana、Prometheus 和 Jaeger 在内的多个服务,有助于 RustFS 的可观测性监控。如果您还想启动 Redis 或 Nginx 容器,可以指定相应的 profile。
|
||||
**注意**: 我们建议您在运行前查看 `docker-compose.yml` 文件。该文件定义了包括 Grafana、Prometheus 和 Jaeger 在内的多个服务,有助于 RustFS 的可观测性监控。如果您还想启动 Redis 或 Nginx 容器,可以指定相应的 profile。
|
||||
|
||||
### 3\. 源码编译 (选项 3) - 进阶用户
|
||||
|
||||
@@ -163,11 +163,41 @@ make help-docker # 显示所有 Docker 相关命令
|
||||
|
||||
请按照 [Helm Chart README](https://charts.rustfs.com) 上的说明在 Kubernetes 集群上安装 RustFS。
|
||||
|
||||
### 5\. Nix Flake (Option 5)
|
||||
|
||||
如果你已经 启用了 [Nix Flakes 功能](https://nixos.wiki/wiki/Flakes#Enable_flakes):
|
||||
|
||||
```bash
|
||||
# 直接运行,无需安装
|
||||
nix run github:rustfs/rustfs
|
||||
|
||||
# 编译二进制文件
|
||||
nix build github:rustfs/rustfs
|
||||
./result/bin/rustfs --help
|
||||
|
||||
# 或者从本地检出的代码库运行/编译
|
||||
nix build
|
||||
nix run
|
||||
```
|
||||
|
||||
### 6\. X-CMD (Option 6)
|
||||
|
||||
如果你是 [x-cmd](https://www.x-cmd.com/install/rustfs) 用户:
|
||||
|
||||
```bash
|
||||
# 直接运行,无需安装
|
||||
x rustfs
|
||||
|
||||
# 下载二进制文件并安装到全局环境中
|
||||
x env use rustfs
|
||||
rustfs --help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 访问 RustFS
|
||||
|
||||
1. **访问控制台**: 打开浏览器并访问 `http://localhost:9000` 进入 RustFS 控制台。
|
||||
1. **访问控制台**: 打开浏览器并访问 `http://localhost:9001` 进入 RustFS 控制台。
|
||||
- 默认账号/密码: `rustfsadmin` / `rustfsadmin`
|
||||
2. **创建存储桶**: 使用控制台为您的对象创建一个新的存储桶 (Bucket)。
|
||||
3. **上传对象**: 您可以直接通过控制台上传文件,或使用 S3 兼容的 API/客户端与您的 RustFS 实例进行交互。
|
||||
@@ -198,7 +228,7 @@ make help-docker # 显示所有 Docker 相关命令
|
||||
- **商务合作**: [hello@rustfs.com](mailto:hello@rustfs.com)
|
||||
- **工作机会**: [jobs@rustfs.com](mailto:jobs@rustfs.com)
|
||||
- **一般讨论**: [GitHub Discussions](https://github.com/rustfs/rustfs/discussions)
|
||||
- **贡献指南**: [CONTRIBUTING.md](https://www.google.com/search?q=CONTRIBUTING.md)
|
||||
- **贡献指南**: [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
## 贡献者
|
||||
|
||||
|
||||
@@ -39,9 +39,12 @@ abd = "abd"
|
||||
mak = "mak"
|
||||
gae = "gae"
|
||||
GAE = "GAE"
|
||||
thr = "thr"
|
||||
# s3-tests original test names (cannot be changed)
|
||||
nonexisted = "nonexisted"
|
||||
consts = "consts"
|
||||
# Swift API - company/product names
|
||||
Hashi = "Hashi" # HashiCorp
|
||||
|
||||
[files]
|
||||
extend-exclude = []
|
||||
|
||||
+2
-2
@@ -213,7 +213,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="-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
|
||||
@@ -430,7 +430,7 @@ build_binary() {
|
||||
fi
|
||||
else
|
||||
# Native compilation
|
||||
build_cmd="RUSTFLAGS=-Clink-arg=-lm cargo build"
|
||||
build_cmd="RUSTFLAGS='--cfg tokio_unstable -Clink-arg=-lm' cargo build"
|
||||
fi
|
||||
|
||||
if [ "$BUILD_TYPE" = "release" ]; then
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Crates Instructions
|
||||
|
||||
Applies to all paths under `crates/`.
|
||||
|
||||
## Library Design
|
||||
|
||||
- Treat crate code as reusable library code by default.
|
||||
- Prefer `thiserror` for library-facing error types.
|
||||
- Do not use `unwrap()`, `expect()`, or panic-driven control flow outside tests.
|
||||
|
||||
## Testing
|
||||
|
||||
- Keep unit tests close to the module they test.
|
||||
- Keep integration tests under each crate's `tests/` directory.
|
||||
- Add regression tests for bug fixes and behavior changes.
|
||||
|
||||
## Async and Performance
|
||||
|
||||
- Keep async paths non-blocking.
|
||||
- Move CPU-heavy operations out of async hot paths with `tokio::task::spawn_blocking` when appropriate.
|
||||
@@ -26,10 +26,13 @@ categories = ["web-programming", "development-tools", "authentication"]
|
||||
|
||||
[dependencies]
|
||||
base64-simd = { workspace = true }
|
||||
rsa = { workspace = true }
|
||||
rsa = { workspace = true, features = ["sha2"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
rand.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
+123
-36
@@ -15,9 +15,13 @@
|
||||
use rsa::{
|
||||
Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey,
|
||||
pkcs8::{DecodePrivateKey, DecodePublicKey},
|
||||
pss::{BlindedSigningKey, Signature, VerifyingKey},
|
||||
sha2::Sha256,
|
||||
signature::{RandomizedSigner, Verifier},
|
||||
traits::PublicKeyParts,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{Error, Result};
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
pub struct Token {
|
||||
@@ -25,10 +29,11 @@ pub struct Token {
|
||||
pub expired: u64, // Expiry time (UNIX timestamp)
|
||||
}
|
||||
|
||||
/// Public key generation Token
|
||||
/// [token] Token object
|
||||
/// [key] Public key string
|
||||
/// Returns the encrypted string processed by base64
|
||||
/// Legacy public-key encryption Token encoder.
|
||||
///
|
||||
/// Use `sign_license_token` for license issuance so verifiers only need a
|
||||
/// public key.
|
||||
#[deprecated(note = "use sign_license_token for signed license issuance")]
|
||||
pub fn gencode(token: &Token, key: &str) -> Result<String> {
|
||||
let data = serde_json::to_vec(token)?;
|
||||
let mut rng = rand::rng();
|
||||
@@ -37,35 +42,62 @@ pub fn gencode(token: &Token, key: &str) -> Result<String> {
|
||||
Ok(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&encrypted_data))
|
||||
}
|
||||
|
||||
/// Private key resolution Token
|
||||
/// [token] Encrypted string processed by base64
|
||||
/// [key] Private key string
|
||||
/// Return to the Token object
|
||||
/// Legacy private-key Token decoder.
|
||||
///
|
||||
/// Use `parse_signed_license_token` or `parse_license_with_public_key` for
|
||||
/// license verification so runtime services never need private key material.
|
||||
#[deprecated(note = "use parse_signed_license_token or parse_license_with_public_key for signed license verification")]
|
||||
pub fn parse(token: &str, key: &str) -> Result<Token> {
|
||||
let encrypted_data = base64_simd::URL_SAFE_NO_PAD
|
||||
.decode_to_vec(token.as_bytes())
|
||||
.map_err(Error::other)?;
|
||||
let private_key = RsaPrivateKey::from_pkcs8_pem(key).map_err(Error::other)?;
|
||||
let decrypted_data = private_key.decrypt(Pkcs1v15Encrypt, &encrypted_data).map_err(Error::other)?;
|
||||
let res: Token = serde_json::from_slice(&decrypted_data)?;
|
||||
Ok(res)
|
||||
serde_json::from_slice(&decrypted_data).map_err(Error::other)
|
||||
}
|
||||
|
||||
pub fn parse_license(license: &str) -> Result<Token> {
|
||||
parse(license, TEST_PRIVATE_KEY)
|
||||
// match parse(license, TEST_PRIVATE_KEY) {
|
||||
// Ok(token) => {
|
||||
// if token.expired > SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() {
|
||||
// Ok(token)
|
||||
// } else {
|
||||
// Err("Token expired".into())
|
||||
// }
|
||||
// }
|
||||
// Err(e) => Err(e),
|
||||
// }
|
||||
/// Signs a license token with an RSA private key.
|
||||
///
|
||||
/// The returned token is base64url(signature || payload), where the signature is
|
||||
/// RSASSA-PSS over the JSON payload using SHA-256.
|
||||
pub fn sign_license_token(token: &Token, private_key_pem: &str) -> Result<String> {
|
||||
let payload = serde_json::to_vec(token)?;
|
||||
let mut rng = rand::rng();
|
||||
let private_key = RsaPrivateKey::from_pkcs8_pem(private_key_pem).map_err(Error::other)?;
|
||||
let signing_key = BlindedSigningKey::<Sha256>::new(private_key);
|
||||
let signature: Signature = signing_key.try_sign_with_rng(&mut rng, &payload).map_err(Error::other)?;
|
||||
let signature: Box<[u8]> = signature.into();
|
||||
|
||||
let mut signed_payload = Vec::with_capacity(signature.as_ref().len() + payload.len());
|
||||
signed_payload.extend_from_slice(signature.as_ref());
|
||||
signed_payload.extend_from_slice(&payload);
|
||||
|
||||
Ok(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&signed_payload))
|
||||
}
|
||||
|
||||
static TEST_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCj86SrJIuxSxR6\nBJ/dlJEUIj6NeBRnhLQlCDdovuz61+7kJXVcxaR66w4m8W7SLEUP+IlPtnn6vmiG\n7XMhGNHIr7r1JsEVVLhZmL3tKI66DEZl786ZhG81BWqUlmcooIPS8UEPZNqJXLuz\nVGhxNyVGbj/tV7QC2pSISnKaixc+nrhxvo7w56p5qrm9tik0PjTgfZsUePkoBsSN\npoRkAauS14MAzK6HGB75CzG3dZqXUNWSWVocoWtQbZUwFGXyzU01ammsHQDvc2xu\nK1RQpd1qYH5bOWZ0N0aPFwT0r59HztFXg9sbjsnuhO1A7OiUOkc6iGVuJ0wm/9nA\nwZIBqzgjAgMBAAECggEAPMpeSEbotPhNw2BrllE76ec4omPfzPJbiU+em+wPGoNu\nRJHPDnMKJbl6Kd5jZPKdOOrCnxfd6qcnQsBQa/kz7+GYxMV12l7ra+1Cnujm4v0i\nLTHZvPpp8ZLsjeOmpF3AAzsJEJgon74OqtOlVjVIUPEYKvzV9ijt4gsYq0zfdYv0\nhrTMzyrGM4/UvKLsFIBROAfCeWfA7sXLGH8JhrRAyDrtCPzGtyyAmzoHKHtHafcB\nuyPFw/IP8otAgpDk5iiQPNkH0WwzAQIm12oHuNUa66NwUK4WEjXTnDg8KeWLHHNv\nIfN8vdbZchMUpMIvvkr7is315d8f2cHCB5gEO+GWAQKBgQDR/0xNll+FYaiUKCPZ\nvkOCAd3l5mRhsqnjPQ/6Ul1lAyYWpoJSFMrGGn/WKTa/FVFJRTGbBjwP+Mx10bfb\ngUg2GILDTISUh54fp4zngvTi9w4MWGKXrb7I1jPkM3vbJfC/v2fraQ/r7qHPpO2L\nf6ZbGxasIlSvr37KeGoelwcAQQKBgQDH3hmOTS2Hl6D4EXdq5meHKrfeoicGN7m8\noQK7u8iwn1R9zK5nh6IXxBhKYNXNwdCQtBZVRvFjjZ56SZJb7lKqa1BcTsgJfZCy\nnI3Uu4UykrECAH8AVCVqBXUDJmeA2yE+gDAtYEjvhSDHpUfWxoGHr0B/Oqk2Lxc/\npRy1qV5fYwKBgBWSL/hYVf+RhIuTg/s9/BlCr9SJ0g3nGGRrRVTlWQqjRCpXeFOO\nJzYqSq9pFGKUggEQxoOyJEFPwVDo9gXqRcyov+Xn2kaXl7qQr3yoixc1YZALFDWY\nd1ySBEqQr0xXnV9U/gvEgwotPRnjSzNlLWV2ZuHPtPtG/7M0o1H5GZMBAoGAKr3N\nW0gX53o+my4pCnxRQW+aOIsWq1a5aqRIEFudFGBOUkS2Oz+fI1P1GdrRfhnnfzpz\n2DK+plp/vIkFOpGhrf4bBlJ2psjqa7fdANRFLMaAAfyXLDvScHTQTCcnVUAHQPVq\n2BlSH56pnugyj7SNuLV6pnql+wdhAmRN2m9o1h8CgYAbX2juSr4ioXwnYjOUdrIY\n4+ERvHcXdjoJmmPcAm4y5NbSqLXyU0FQmplNMt2A5LlniWVJ9KNdjAQUt60FZw/+\nr76LdxXaHNZghyx0BOs7mtq5unSQXamZ8KixasfhE9uz3ij1jXjG6hafWkS8/68I\nuWbaZqgvy7a9oPHYlKH7Jg==\n-----END PRIVATE KEY-----\n";
|
||||
/// Verifies and parses a signed license token with an RSA public key.
|
||||
pub fn parse_signed_license_token(token: &str, public_key_pem: &str) -> Result<Token> {
|
||||
let signed_payload = base64_simd::URL_SAFE_NO_PAD
|
||||
.decode_to_vec(token.as_bytes())
|
||||
.map_err(Error::other)?;
|
||||
let public_key = RsaPublicKey::from_public_key_pem(public_key_pem).map_err(Error::other)?;
|
||||
let signature_len = public_key.size();
|
||||
|
||||
if signed_payload.len() <= signature_len {
|
||||
return Err(Error::new(ErrorKind::InvalidData, "license token is missing signed payload"));
|
||||
}
|
||||
|
||||
let (signature, payload) = signed_payload.split_at(signature_len);
|
||||
let signature = Signature::try_from(signature).map_err(Error::other)?;
|
||||
let verifying_key = VerifyingKey::<Sha256>::new(public_key);
|
||||
verifying_key.verify(payload, &signature).map_err(Error::other)?;
|
||||
|
||||
serde_json::from_slice(payload).map_err(Error::other)
|
||||
}
|
||||
|
||||
pub fn parse_license_with_public_key(license: &str, public_key: &str) -> Result<Token> {
|
||||
parse_signed_license_token(license, public_key)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -77,7 +109,7 @@ mod tests {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[test]
|
||||
fn test_gencode_and_parse() {
|
||||
fn test_sign_license_token_and_parse_signed_license_token() {
|
||||
let mut rng = rand::rng();
|
||||
let bits = 2048;
|
||||
let private_key = RsaPrivateKey::new(&mut rng, bits).expect("Failed to generate private key");
|
||||
@@ -91,8 +123,31 @@ mod tests {
|
||||
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now
|
||||
};
|
||||
|
||||
let encoded = gencode(&token, &public_key_pem).expect("Failed to encode token");
|
||||
let encoded = sign_license_token(&token, &private_key_pem).expect("Failed to encode token");
|
||||
|
||||
let decoded = parse_signed_license_token(&encoded, &public_key_pem).expect("Failed to decode token");
|
||||
|
||||
assert_eq!(token.name, decoded.name);
|
||||
assert_eq!(token.expired, decoded.expired);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn test_legacy_gencode_and_parse_roundtrip() {
|
||||
let mut rng = rand::rng();
|
||||
let bits = 2048;
|
||||
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 token = Token {
|
||||
name: "test_app".to_string(),
|
||||
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600,
|
||||
};
|
||||
|
||||
let encoded = gencode(&token, &public_key_pem).expect("Failed to encode token");
|
||||
let decoded = parse(&encoded, &private_key_pem).expect("Failed to decode token");
|
||||
|
||||
assert_eq!(token.name, decoded.name);
|
||||
@@ -100,28 +155,60 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_token() {
|
||||
fn test_parse_signed_license_token_rejects_tampered_payload() {
|
||||
let mut rng = rand::rng();
|
||||
let private_key_pem = RsaPrivateKey::new(&mut rng, 2048)
|
||||
.expect("Failed to generate private key")
|
||||
.to_pkcs8_pem(LineEnding::LF)
|
||||
.unwrap();
|
||||
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 token = Token {
|
||||
name: "test_app".to_string(),
|
||||
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600,
|
||||
};
|
||||
|
||||
let invalid_token = "invalid_base64_token";
|
||||
let result = parse(invalid_token, &private_key_pem);
|
||||
let encoded = sign_license_token(&token, &private_key_pem).expect("Failed to encode token");
|
||||
let mut signed_payload = base64_simd::URL_SAFE_NO_PAD
|
||||
.decode_to_vec(encoded.as_bytes())
|
||||
.expect("Failed to decode signed payload");
|
||||
let last_byte = signed_payload.last_mut().expect("Signed payload should not be empty");
|
||||
*last_byte ^= 0x01;
|
||||
let tampered = base64_simd::URL_SAFE_NO_PAD.encode_to_string(&signed_payload);
|
||||
|
||||
let result = parse_signed_license_token(&tampered, &public_key_pem);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gencode_with_invalid_key() {
|
||||
fn test_source_does_not_embed_private_key() {
|
||||
let source = include_str!("token.rs");
|
||||
let forbidden = ["BEGIN", "PRIVATE KEY"].join(" ");
|
||||
|
||||
assert!(!source.contains(&forbidden));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_signed_license_token_rejects_invalid_token() {
|
||||
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 invalid_token = "invalid_base64_token";
|
||||
let result = parse_signed_license_token(invalid_token, &public_key_pem);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
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
|
||||
};
|
||||
|
||||
let invalid_key = "invalid_public_key";
|
||||
let result = gencode(&token, invalid_key);
|
||||
let invalid_key = "invalid_private_key";
|
||||
let result = sign_license_token(&token, invalid_key);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
@@ -29,6 +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 }
|
||||
chrono = { workspace = true }
|
||||
const-str = { workspace = true }
|
||||
@@ -40,9 +41,14 @@ serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync", "fs", "rt-multi-thread", "rt", "time", "macros"] }
|
||||
tracing = { workspace = true, features = ["std", "attributes"] }
|
||||
url = { workspace = true }
|
||||
rumqttc = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
temp-env = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use hashbrown::HashMap;
|
||||
use rustfs_targets::EventName;
|
||||
use rustfs_s3_common::EventName;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
+67
-157
@@ -14,24 +14,19 @@
|
||||
|
||||
use crate::AuditEntry;
|
||||
use async_trait::async_trait;
|
||||
use hashbrown::HashSet;
|
||||
use rumqttc::QoS;
|
||||
use rustfs_config::audit::{AUDIT_MQTT_KEYS, AUDIT_WEBHOOK_KEYS, ENV_AUDIT_MQTT_KEYS, ENV_AUDIT_WEBHOOK_KEYS};
|
||||
use rustfs_config::{
|
||||
AUDIT_DEFAULT_DIR, DEFAULT_LIMIT, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR,
|
||||
MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TOPIC, MQTT_USERNAME, RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT,
|
||||
WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR,
|
||||
WEBHOOK_QUEUE_LIMIT, WEBHOOK_SKIP_TLS_VERIFY,
|
||||
};
|
||||
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::{mqtt::MQTTArgs, webhook::WebhookArgs},
|
||||
target::TargetType,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, warn};
|
||||
use url::Url;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Trait for creating targets from configuration
|
||||
#[async_trait]
|
||||
@@ -45,10 +40,6 @@ pub trait TargetFactory: Send + Sync {
|
||||
/// 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>;
|
||||
|
||||
/// Returns a set of valid configuration env field names for this target type.
|
||||
/// This is used to filter environment variables.
|
||||
fn get_valid_env_fields(&self) -> HashSet<String>;
|
||||
}
|
||||
|
||||
/// Factory for creating Webhook targets
|
||||
@@ -57,71 +48,18 @@ 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> {
|
||||
// All config values are now read directly from the merged `config` KVS.
|
||||
let endpoint = config
|
||||
.lookup(WEBHOOK_ENDPOINT)
|
||||
.ok_or_else(|| TargetError::Configuration("Missing webhook endpoint".to_string()))?;
|
||||
let parsed_endpoint = endpoint.trim();
|
||||
let endpoint_url = Url::parse(parsed_endpoint)
|
||||
.map_err(|e| TargetError::Configuration(format!("Invalid endpoint URL: {e} (value: '{parsed_endpoint}')")))?;
|
||||
|
||||
let args = WebhookArgs {
|
||||
enable: true, // If we are here, it's already enabled.
|
||||
endpoint: endpoint_url,
|
||||
auth_token: config.lookup(WEBHOOK_AUTH_TOKEN).unwrap_or_default(),
|
||||
queue_dir: config.lookup(WEBHOOK_QUEUE_DIR).unwrap_or(AUDIT_DEFAULT_DIR.to_string()),
|
||||
queue_limit: config
|
||||
.lookup(WEBHOOK_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(DEFAULT_LIMIT),
|
||||
client_cert: config.lookup(WEBHOOK_CLIENT_CERT).unwrap_or_default(),
|
||||
client_key: config.lookup(WEBHOOK_CLIENT_KEY).unwrap_or_default(),
|
||||
client_ca: config.lookup(WEBHOOK_CLIENT_CA).unwrap_or_default(),
|
||||
skip_tls_verify: config
|
||||
.lookup(WEBHOOK_SKIP_TLS_VERIFY)
|
||||
.and_then(|v| v.parse::<bool>().ok())
|
||||
.unwrap_or(RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT),
|
||||
target_type: rustfs_targets::target::TargetType::AuditLog,
|
||||
};
|
||||
|
||||
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> {
|
||||
// Validation also uses the merged `config` KVS directly.
|
||||
let endpoint = config
|
||||
.lookup(WEBHOOK_ENDPOINT)
|
||||
.ok_or_else(|| TargetError::Configuration("Missing webhook endpoint".to_string()))?;
|
||||
debug!("endpoint: {}", endpoint);
|
||||
let parsed_endpoint = endpoint.trim();
|
||||
Url::parse(parsed_endpoint)
|
||||
.map_err(|e| TargetError::Configuration(format!("Invalid endpoint URL: {e} (value: '{parsed_endpoint}')")))?;
|
||||
|
||||
let client_cert = config.lookup(WEBHOOK_CLIENT_CERT).unwrap_or_default();
|
||||
let client_key = config.lookup(WEBHOOK_CLIENT_KEY).unwrap_or_default();
|
||||
|
||||
if client_cert.is_empty() != client_key.is_empty() {
|
||||
return Err(TargetError::Configuration(
|
||||
"Both client_cert and client_key must be specified together".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let queue_dir = config.lookup(WEBHOOK_QUEUE_DIR).unwrap_or(AUDIT_DEFAULT_DIR.to_string());
|
||||
if !queue_dir.is_empty() && !std::path::Path::new(&queue_dir).is_absolute() {
|
||||
return Err(TargetError::Configuration("Webhook queue directory must be an absolute path".to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
validate_webhook_config(config, AUDIT_DEFAULT_DIR)
|
||||
}
|
||||
|
||||
fn get_valid_fields(&self) -> HashSet<String> {
|
||||
AUDIT_WEBHOOK_KEYS.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
fn get_valid_env_fields(&self) -> HashSet<String> {
|
||||
ENV_AUDIT_WEBHOOK_KEYS.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Factory for creating MQTT targets
|
||||
@@ -130,101 +68,73 @@ pub struct MQTTTargetFactory;
|
||||
#[async_trait]
|
||||
impl TargetFactory for MQTTTargetFactory {
|
||||
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
||||
let broker = config
|
||||
.lookup(MQTT_BROKER)
|
||||
.ok_or_else(|| TargetError::Configuration("Missing MQTT broker".to_string()))?;
|
||||
let broker_url = Url::parse(&broker)
|
||||
.map_err(|e| TargetError::Configuration(format!("Invalid broker URL: {e} (value: '{broker}')")))?;
|
||||
|
||||
let topic = config
|
||||
.lookup(MQTT_TOPIC)
|
||||
.ok_or_else(|| TargetError::Configuration("Missing MQTT topic".to_string()))?;
|
||||
|
||||
let args = MQTTArgs {
|
||||
enable: true, // Assumed enabled.
|
||||
broker: broker_url,
|
||||
topic,
|
||||
qos: config
|
||||
.lookup(MQTT_QOS)
|
||||
.and_then(|v| v.parse::<u8>().ok())
|
||||
.map(|q| match q {
|
||||
0 => QoS::AtMostOnce,
|
||||
1 => QoS::AtLeastOnce,
|
||||
2 => QoS::ExactlyOnce,
|
||||
_ => QoS::AtLeastOnce,
|
||||
})
|
||||
.unwrap_or(QoS::AtLeastOnce),
|
||||
username: config.lookup(MQTT_USERNAME).unwrap_or_default(),
|
||||
password: config.lookup(MQTT_PASSWORD).unwrap_or_default(),
|
||||
max_reconnect_interval: config
|
||||
.lookup(MQTT_RECONNECT_INTERVAL)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.map(Duration::from_secs)
|
||||
.unwrap_or_else(|| Duration::from_secs(5)),
|
||||
keep_alive: config
|
||||
.lookup(MQTT_KEEP_ALIVE_INTERVAL)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.map(Duration::from_secs)
|
||||
.unwrap_or_else(|| Duration::from_secs(30)),
|
||||
queue_dir: config.lookup(MQTT_QUEUE_DIR).unwrap_or(AUDIT_DEFAULT_DIR.to_string()),
|
||||
queue_limit: config
|
||||
.lookup(MQTT_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(DEFAULT_LIMIT),
|
||||
target_type: rustfs_targets::target::TargetType::AuditLog,
|
||||
};
|
||||
|
||||
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 validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||
let broker = config
|
||||
.lookup(MQTT_BROKER)
|
||||
.ok_or_else(|| TargetError::Configuration("Missing MQTT broker".to_string()))?;
|
||||
let url = Url::parse(&broker)
|
||||
.map_err(|e| TargetError::Configuration(format!("Invalid broker URL: {e} (value: '{broker}')")))?;
|
||||
|
||||
match url.scheme() {
|
||||
"tcp" | "ssl" | "ws" | "wss" | "mqtt" | "mqtts" => {}
|
||||
_ => {
|
||||
return Err(TargetError::Configuration("Unsupported broker URL scheme".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
if config.lookup(MQTT_TOPIC).is_none() {
|
||||
return Err(TargetError::Configuration("Missing MQTT topic".to_string()));
|
||||
}
|
||||
|
||||
if let Some(qos_str) = config.lookup(MQTT_QOS) {
|
||||
let qos = qos_str
|
||||
.parse::<u8>()
|
||||
.map_err(|_| TargetError::Configuration("Invalid QoS value".to_string()))?;
|
||||
if qos > 2 {
|
||||
return Err(TargetError::Configuration("QoS must be 0, 1, or 2".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
let queue_dir = config.lookup(MQTT_QUEUE_DIR).unwrap_or_default();
|
||||
if !queue_dir.is_empty() {
|
||||
if !std::path::Path::new(&queue_dir).is_absolute() {
|
||||
return Err(TargetError::Configuration("MQTT queue directory must be an absolute path".to_string()));
|
||||
}
|
||||
if let Some(qos_str) = config.lookup(MQTT_QOS)
|
||||
&& qos_str == "0"
|
||||
{
|
||||
warn!("Using queue_dir with QoS 0 may result in event loss");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
validate_mqtt_config(config)
|
||||
}
|
||||
|
||||
fn get_valid_fields(&self) -> HashSet<String> {
|
||||
AUDIT_MQTT_KEYS.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_valid_env_fields(&self) -> HashSet<String> {
|
||||
ENV_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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::{AuditEntry, AuditResult, AuditSystem};
|
||||
use crate::{AuditEntry, AuditResult, AuditSystem, system::AuditTargetMetricSnapshot};
|
||||
use rustfs_ecstore::config::Config;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
@@ -89,6 +89,15 @@ pub async fn reload_audit_config(config: Config) -> AuditResult<()> {
|
||||
with_audit_system!(|system: Arc<AuditSystem>| async move { system.reload_config(config).await })
|
||||
}
|
||||
|
||||
/// Returns per-target audit delivery metrics for Prometheus collection.
|
||||
pub async fn audit_target_metrics() -> Vec<AuditTargetMetricSnapshot> {
|
||||
if let Some(system) = audit_system() {
|
||||
system.snapshot_target_metrics().await
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the global audit system is running
|
||||
pub async fn is_audit_system_running() -> bool {
|
||||
if let Some(system) = audit_system() {
|
||||
|
||||
@@ -31,4 +31,4 @@ pub use error::{AuditError, AuditResult};
|
||||
pub use global::*;
|
||||
pub use observability::{AuditMetrics, AuditMetricsReport, PerformanceValidation};
|
||||
pub use registry::AuditRegistry;
|
||||
pub use system::AuditSystem;
|
||||
pub use system::{AuditSystem, AuditTargetMetricSnapshot};
|
||||
|
||||
+28
-215
@@ -14,18 +14,19 @@
|
||||
|
||||
use crate::{
|
||||
AuditEntry, AuditError, AuditResult,
|
||||
factory::{MQTTTargetFactory, TargetFactory, WebhookTargetFactory},
|
||||
factory::{
|
||||
KafkaTargetFactory, MQTTTargetFactory, NATSTargetFactory, PulsarTargetFactory, TargetFactory, WebhookTargetFactory,
|
||||
},
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, ENV_PREFIX, EnableState, audit::AUDIT_ROUTE_PREFIX};
|
||||
use hashbrown::HashMap;
|
||||
use rustfs_config::audit::AUDIT_ROUTE_PREFIX;
|
||||
use rustfs_ecstore::config::{Config, KVS};
|
||||
use rustfs_targets::arn::TargetID;
|
||||
use rustfs_targets::{Target, TargetError, target::ChannelTargetType};
|
||||
use std::str::FromStr;
|
||||
use rustfs_targets::{Target, TargetError, config::collect_target_configs, target::ChannelTargetType};
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tracing::{error, info};
|
||||
|
||||
/// Registry for managing audit targets
|
||||
pub struct AuditRegistry {
|
||||
@@ -52,6 +53,9 @@ impl AuditRegistry {
|
||||
// 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
|
||||
}
|
||||
@@ -105,227 +109,31 @@ impl AuditRegistry {
|
||||
&self,
|
||||
config: &Config,
|
||||
) -> AuditResult<Vec<Box<dyn Target<AuditEntry> + Send + Sync>>> {
|
||||
// Collect only environment variables with the relevant prefix to reduce memory usage
|
||||
let all_env: Vec<(String, String)> = std::env::vars().filter(|(key, _)| key.starts_with(ENV_PREFIX)).collect();
|
||||
// A collection of asynchronous tasks for concurrently executing target creation
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
// let final_config = config.clone(); // Clone a configuration for aggregating the final result
|
||||
// Record the defaults for each segment so that the segment can eventually be rebuilt
|
||||
let mut section_defaults: HashMap<String, KVS> = HashMap::new();
|
||||
// 1. Traverse all registered plants and process them by target type
|
||||
for (target_type, factory) in &self.factories {
|
||||
tracing::Span::current().record("target_type", target_type.as_str());
|
||||
info!("Start working on target types...");
|
||||
|
||||
// 2. Prepare the configuration source
|
||||
// 2.1. Get the configuration segment in the file, e.g. 'audit_webhook'
|
||||
let section_name = format!("{AUDIT_ROUTE_PREFIX}{target_type}").to_lowercase();
|
||||
let file_configs = config.0.get(§ion_name).cloned().unwrap_or_default();
|
||||
// 2.2. Get the default configuration for that type
|
||||
let default_cfg = file_configs.get(DEFAULT_DELIMITER).cloned().unwrap_or_default();
|
||||
debug!(?default_cfg, "Get the default configuration");
|
||||
|
||||
// Save defaults for eventual write back
|
||||
section_defaults.insert(section_name.clone(), default_cfg.clone());
|
||||
|
||||
// *** Optimization point 1: Get all legitimate fields of the current target type ***
|
||||
let valid_fields = factory.get_valid_fields();
|
||||
debug!(?valid_fields, "Get the legitimate configuration fields");
|
||||
|
||||
// 3. Resolve instance IDs and configuration overrides from environment variables
|
||||
let mut instance_ids_from_env = HashSet::new();
|
||||
// 3.1. Instance discovery: Based on the '..._ENABLE_INSTANCEID' format
|
||||
let enable_prefix =
|
||||
format!("{ENV_PREFIX}{AUDIT_ROUTE_PREFIX}{target_type}{DEFAULT_DELIMITER}{ENABLE_KEY}{DEFAULT_DELIMITER}")
|
||||
.to_uppercase();
|
||||
for (key, value) in &all_env {
|
||||
if EnableState::from_str(value).ok().map(|s| s.is_enabled()).unwrap_or(false)
|
||||
&& let Some(id) = key.strip_prefix(&enable_prefix)
|
||||
&& !id.is_empty()
|
||||
{
|
||||
instance_ids_from_env.insert(id.to_lowercase());
|
||||
}
|
||||
}
|
||||
|
||||
// 3.2. Parse all relevant environment variable configurations
|
||||
// 3.2.1. Build environment variable prefixes such as 'RUSTFS_AUDIT_WEBHOOK_'
|
||||
let env_prefix = format!("{ENV_PREFIX}{AUDIT_ROUTE_PREFIX}{target_type}{DEFAULT_DELIMITER}").to_uppercase();
|
||||
// 3.2.2. 'env_overrides' is used to store configurations parsed from environment variables in the format: {instance id -> {field -> value}}
|
||||
let mut env_overrides: HashMap<String, HashMap<String, String>> = HashMap::new();
|
||||
for (key, value) in &all_env {
|
||||
if let Some(rest) = key.strip_prefix(&env_prefix) {
|
||||
// Use rsplitn to split from the right side to properly extract the INSTANCE_ID at the end
|
||||
// Format: <FIELD_NAME>_<INSTANCE_ID> or <FIELD_NAME>
|
||||
let mut parts = rest.rsplitn(2, DEFAULT_DELIMITER);
|
||||
|
||||
// The first part from the right is INSTANCE_ID
|
||||
let instance_id_part = parts.next().unwrap_or(DEFAULT_DELIMITER);
|
||||
// The remaining part is FIELD_NAME
|
||||
let field_name_part = parts.next();
|
||||
|
||||
let (field_name, instance_id) = match field_name_part {
|
||||
// Case 1: The format is <FIELD_NAME>_<INSTANCE_ID>
|
||||
// e.g., rest = "ENDPOINT_PRIMARY" -> field_name="ENDPOINT", instance_id="PRIMARY"
|
||||
Some(field) => (field.to_lowercase(), instance_id_part.to_lowercase()),
|
||||
// Case 2: The format is <FIELD_NAME> (without INSTANCE_ID)
|
||||
// e.g., rest = "ENABLE" -> field_name="ENABLE", instance_id="" (Universal configuration `_ DEFAULT_DELIMITER`)
|
||||
None => (instance_id_part.to_lowercase(), DEFAULT_DELIMITER.to_string()),
|
||||
};
|
||||
|
||||
// *** Optimization point 2: Verify whether the parsed field_name is legal ***
|
||||
if !field_name.is_empty() && valid_fields.contains(&field_name) {
|
||||
debug!(
|
||||
instance_id = %if instance_id.is_empty() { DEFAULT_DELIMITER } else { &instance_id },
|
||||
%field_name,
|
||||
%value,
|
||||
"Parsing to environment variables"
|
||||
);
|
||||
env_overrides
|
||||
.entry(instance_id)
|
||||
.or_default()
|
||||
.insert(field_name, value.clone());
|
||||
} else {
|
||||
// Ignore illegal field names
|
||||
warn!(
|
||||
field_name = %field_name,
|
||||
"Ignore environment variable fields, not found in the list of valid fields for target type {}",
|
||||
target_type
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!(?env_overrides, "Complete the environment variable analysis");
|
||||
|
||||
// 4. Determine all instance IDs that need to be processed
|
||||
let mut all_instance_ids: HashSet<String> =
|
||||
file_configs.keys().filter(|k| *k != DEFAULT_DELIMITER).cloned().collect();
|
||||
all_instance_ids.extend(instance_ids_from_env);
|
||||
debug!(?all_instance_ids, "Determine all instance IDs");
|
||||
|
||||
// 5. Merge configurations and create tasks for each instance
|
||||
for id in all_instance_ids {
|
||||
// 5.1. Merge configuration, priority: Environment variables > File instance configuration > File default configuration
|
||||
let mut merged_config = default_cfg.clone();
|
||||
// Instance-specific configuration in application files
|
||||
if let Some(file_instance_cfg) = file_configs.get(&id) {
|
||||
merged_config.extend(file_instance_cfg.clone());
|
||||
}
|
||||
// Application instance-specific environment variable configuration
|
||||
if let Some(env_instance_cfg) = env_overrides.get(&id) {
|
||||
// Convert HashMap<String, String> to KVS
|
||||
let mut kvs_from_env = KVS::new();
|
||||
for (k, v) in env_instance_cfg {
|
||||
kvs_from_env.insert(k.clone(), v.clone());
|
||||
}
|
||||
merged_config.extend(kvs_from_env);
|
||||
}
|
||||
debug!(instance_id = %id, ?merged_config, "Complete configuration merge");
|
||||
|
||||
// 5.2. Check if the instance is enabled
|
||||
let enabled = merged_config
|
||||
.lookup(ENABLE_KEY)
|
||||
.map(|v| {
|
||||
EnableState::from_str(v.as_str())
|
||||
.ok()
|
||||
.map(|s| s.is_enabled())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if enabled {
|
||||
info!(instance_id = %id, "Target is enabled, ready to create a task");
|
||||
// 5.3. Create asynchronous tasks for enabled instances
|
||||
let target_type_clone = target_type.clone();
|
||||
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;
|
||||
(target_type_clone, tid, result, Arc::clone(&merged_config_arc))
|
||||
});
|
||||
} else {
|
||||
info!(instance_id = %id, "Skip the disabled target and will be removed from the final configuration");
|
||||
// Remove disabled target from final configuration
|
||||
// final_config.0.entry(section_name.clone()).or_default().remove(&id);
|
||||
}
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Concurrently execute all creation tasks and collect results
|
||||
let mut successful_targets = Vec::new();
|
||||
let mut successful_configs = Vec::new();
|
||||
while let Some((target_type, id, result, final_config)) = tasks.next().await {
|
||||
while let Some((id, result)) = tasks.next().await {
|
||||
match result {
|
||||
Ok(target) => {
|
||||
info!(target_type = %target_type, instance_id = %id, "Create a target successfully");
|
||||
info!(target_type = %target.id().name, instance_id = %id, "Create a target successfully");
|
||||
successful_targets.push(target);
|
||||
successful_configs.push((target_type, id, final_config));
|
||||
}
|
||||
Err(e) => {
|
||||
error!(target_type = %target_type, instance_id = %id, error = %e, "Failed to create a target");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Aggregate new configuration and write back to system configuration
|
||||
if !successful_configs.is_empty() || !section_defaults.is_empty() {
|
||||
info!(
|
||||
"Prepare to update {} successfully created target configurations to the system configuration...",
|
||||
successful_configs.len()
|
||||
);
|
||||
|
||||
let mut successes_by_section: HashMap<String, HashMap<String, KVS>> = HashMap::new();
|
||||
|
||||
for (target_type, id, kvs) in successful_configs {
|
||||
let section_name = format!("{AUDIT_ROUTE_PREFIX}{target_type}").to_lowercase();
|
||||
successes_by_section
|
||||
.entry(section_name)
|
||||
.or_default()
|
||||
.insert(id.to_lowercase(), (*kvs).clone());
|
||||
}
|
||||
|
||||
let mut new_config = config.clone();
|
||||
// Collection of segments that need to be processed: Collect all segments where default items exist or where successful instances exist
|
||||
let mut sections: HashSet<String> = HashSet::new();
|
||||
sections.extend(section_defaults.keys().cloned());
|
||||
sections.extend(successes_by_section.keys().cloned());
|
||||
|
||||
for section in sections {
|
||||
let mut section_map: std::collections::HashMap<String, KVS> = std::collections::HashMap::new();
|
||||
// Add default item
|
||||
if let Some(default_kvs) = section_defaults.get(§ion)
|
||||
&& !default_kvs.is_empty()
|
||||
{
|
||||
section_map.insert(DEFAULT_DELIMITER.to_string(), default_kvs.clone());
|
||||
}
|
||||
|
||||
// Add successful instance item
|
||||
if let Some(instances) = successes_by_section.get(§ion) {
|
||||
for (id, kvs) in instances {
|
||||
section_map.insert(id.clone(), kvs.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Empty breaks are removed and non-empty breaks are replaced entirely.
|
||||
if section_map.is_empty() {
|
||||
new_config.0.remove(§ion);
|
||||
} else {
|
||||
new_config.0.insert(section, section_map);
|
||||
}
|
||||
}
|
||||
|
||||
let Some(store) = rustfs_ecstore::global::new_object_layer_fn() else {
|
||||
return Err(AuditError::StorageNotAvailable(
|
||||
"Failed to save target configuration: server storage not initialized".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
match rustfs_ecstore::config::com::save_server_config(store, &new_config).await {
|
||||
Ok(_) => {
|
||||
info!("The new configuration was saved to the system successfully.")
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to save the new configuration: {}", e);
|
||||
return Err(AuditError::SaveConfig(Box::new(e)));
|
||||
error!(instance_id = %id, error = %e, "Failed to create a target");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,6 +173,11 @@ impl AuditRegistry {
|
||||
self.targets.get(id).map(|t| t.as_ref())
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Lists all target IDs
|
||||
///
|
||||
/// # Returns
|
||||
@@ -387,8 +200,8 @@ impl AuditRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
return Err(AuditError::Target(errors.into_iter().next().unwrap()));
|
||||
if let Some(error) = errors.into_iter().next() {
|
||||
return Err(AuditError::Target(error));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+194
-107
@@ -13,16 +13,25 @@
|
||||
// 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,
|
||||
target::{EntityTarget, QueuedPayload},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tokio::sync::{Mutex, RwLock, mpsc};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct AuditTargetMetricSnapshot {
|
||||
pub failed_messages: u64,
|
||||
pub queue_length: u64,
|
||||
pub target_id: String,
|
||||
pub total_messages: u64,
|
||||
}
|
||||
|
||||
/// State of the audit system
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AuditSystemState {
|
||||
@@ -39,6 +48,8 @@ pub struct AuditSystem {
|
||||
registry: Arc<Mutex<AuditRegistry>>,
|
||||
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<()>>>>,
|
||||
}
|
||||
|
||||
impl Default for AuditSystem {
|
||||
@@ -54,6 +65,7 @@ impl AuditSystem {
|
||||
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())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,27 +122,7 @@ impl AuditSystem {
|
||||
|
||||
// Initialize all targets
|
||||
for target in targets {
|
||||
let target_id = target.id().to_string();
|
||||
if let Err(e) = target.init().await {
|
||||
error!(target_id = %target_id, error = %e, "Failed to initialize audit target");
|
||||
} else {
|
||||
// After successful initialization, if enabled and there is a store, start the send from storage task
|
||||
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<EntityTarget<AuditEntry>, Error = StoreError, Key = Key> + Send> =
|
||||
store.boxed_clone();
|
||||
let target_arc: Arc<dyn Target<AuditEntry> + Send + Sync> = Arc::from(target.clone_dyn());
|
||||
self.start_audit_stream_with_batching(store_clone, target_arc);
|
||||
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, target);
|
||||
}
|
||||
self.init_and_register_target(target, &mut registry).await;
|
||||
}
|
||||
|
||||
// Update state to running
|
||||
@@ -214,6 +206,9 @@ 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 {
|
||||
@@ -258,52 +253,49 @@ impl AuditSystem {
|
||||
let state = self.state.read().await;
|
||||
|
||||
match *state {
|
||||
AuditSystemState::Running => {
|
||||
// Continue with dispatch
|
||||
info!("Dispatching audit log entry");
|
||||
}
|
||||
AuditSystemState::Running => {}
|
||||
AuditSystemState::Paused => {
|
||||
// Skip dispatch when paused
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
// Don't dispatch when not running
|
||||
return Err(AuditError::NotInitialized("Audit system is not running".to_string()));
|
||||
}
|
||||
}
|
||||
drop(state);
|
||||
|
||||
let registry = self.registry.lock().await;
|
||||
let target_keys = registry.list_targets();
|
||||
// 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(());
|
||||
}
|
||||
if target_keys.is_empty() {
|
||||
warn!("No audit targets configured for dispatch");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Dispatch to all targets concurrently
|
||||
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 in target_keys {
|
||||
if let Some(target) = registry.get_target(&target_key) {
|
||||
let entry_clone = Arc::clone(&entry);
|
||||
let target_key_clone = target_key.clone();
|
||||
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(),
|
||||
};
|
||||
|
||||
// Create EntityTarget for the audit log entry
|
||||
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, // Default, should be derived from entry
|
||||
data: (*entry_clone).clone(),
|
||||
};
|
||||
let task = async move {
|
||||
let result = target.save(Arc::new(entity_target)).await;
|
||||
(target_key, result)
|
||||
};
|
||||
|
||||
let task = async move {
|
||||
let result = target.save(Arc::new(entity_target)).await;
|
||||
(target_key_clone, result)
|
||||
};
|
||||
|
||||
tasks.push(task);
|
||||
}
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
// Execute all dispatch tasks
|
||||
@@ -359,39 +351,45 @@ impl AuditSystem {
|
||||
}
|
||||
drop(state);
|
||||
|
||||
let registry = self.registry.lock().await;
|
||||
let target_keys = registry.list_targets();
|
||||
// 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(());
|
||||
}
|
||||
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 in target_keys {
|
||||
if let Some(target) = registry.get_target(&target_key) {
|
||||
let entries_clone: Vec<_> = entries.iter().map(Arc::clone).collect();
|
||||
let target_key_clone = target_key.clone();
|
||||
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),
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
(target_key_clone, success_count, errors)
|
||||
};
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
let results = futures::future::join_all(tasks).await;
|
||||
@@ -417,6 +415,60 @@ impl AuditSystem {
|
||||
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
|
||||
@@ -427,9 +479,10 @@ impl AuditSystem {
|
||||
/// 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<EntityTarget<AuditEntry>, Error = StoreError, Key = Key> + Send>,
|
||||
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 {
|
||||
@@ -442,6 +495,12 @@ impl AuditSystem {
|
||||
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 => {}
|
||||
_ => {
|
||||
@@ -452,11 +511,22 @@ impl AuditSystem {
|
||||
|
||||
let keys: Vec<Key> = store.list();
|
||||
if keys.is_empty() {
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
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;
|
||||
|
||||
@@ -475,8 +545,14 @@ impl AuditSystem {
|
||||
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;
|
||||
}
|
||||
@@ -490,6 +566,7 @@ impl AuditSystem {
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -497,6 +574,8 @@ impl AuditSystem {
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
});
|
||||
|
||||
cancel_tx
|
||||
}
|
||||
|
||||
/// Enables a specific target
|
||||
@@ -594,6 +673,31 @@ impl AuditSystem {
|
||||
registry.list_targets()
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// 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());
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
snapshots.sort_by(|a, b| a.target_id.cmp(&b.target_id));
|
||||
snapshots
|
||||
}
|
||||
|
||||
/// Gets information about a specific target
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -616,9 +720,11 @@ impl AuditSystem {
|
||||
pub async fn reload_config(&self, new_config: Config) -> AuditResult<()> {
|
||||
info!("Reloading audit system configuration");
|
||||
|
||||
// Record config reload
|
||||
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;
|
||||
@@ -636,28 +742,9 @@ impl AuditSystem {
|
||||
Ok(targets) => {
|
||||
info!(target_count = targets.len(), "Reloaded audit targets successfully");
|
||||
|
||||
// Initialize all new targets
|
||||
for target in targets {
|
||||
let target_id = target.id().to_string();
|
||||
if let Err(e) = target.init().await {
|
||||
error!(target_id = %target_id, error = %e, "Failed to initialize reloaded audit target");
|
||||
} else {
|
||||
// Same starts the storage stream after a heavy load
|
||||
if target.is_enabled() {
|
||||
if let Some(store) = target.store() {
|
||||
info!(target_id = %target_id, "Start audit stream processing for target (reload)");
|
||||
let store_clone: Box<dyn Store<EntityTarget<AuditEntry>, Error = StoreError, Key = Key> + Send> =
|
||||
store.boxed_clone();
|
||||
let target_arc: Arc<dyn Target<AuditEntry> + Send + Sync> = Arc::from(target.clone_dyn());
|
||||
self.start_audit_stream_with_batching(store_clone, target_arc);
|
||||
info!(target_id = %target_id, "Audit stream processing started (reload)");
|
||||
} else {
|
||||
info!(target_id = %target_id, "No store configured, skip audit stream processing (reload)");
|
||||
}
|
||||
} else {
|
||||
info!(target_id = %target_id, "Target disabled, skip audit stream processing (reload)");
|
||||
}
|
||||
registry.add_target(target.id().to_string(), target);
|
||||
if let Some(target_id) = self.init_and_register_target(target, &mut registry).await {
|
||||
info!(target_id = %target_id, "Target initialized (reload)");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,12 @@ fn test_mqtt_valid_fields() {
|
||||
"reconnect_interval",
|
||||
"queue_dir",
|
||||
"queue_limit",
|
||||
"tls_policy",
|
||||
"tls_ca",
|
||||
"tls_client_cert",
|
||||
"tls_client_key",
|
||||
"tls_trust_leaf_as_ca",
|
||||
"ws_path_allowlist",
|
||||
];
|
||||
|
||||
// This tests the MQTT configuration fields we support
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
use rustfs_audit::*;
|
||||
use rustfs_ecstore::config::{Config, KVS};
|
||||
use std::collections::HashMap;
|
||||
use temp_env::with_vars;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_audit_system_creation() {
|
||||
@@ -35,34 +36,42 @@ async fn test_config_parsing_webhook() {
|
||||
let mut config = Config(HashMap::new());
|
||||
let mut audit_webhook_section = HashMap::new();
|
||||
|
||||
// Create default configuration
|
||||
let mut default_kvs = KVS::new();
|
||||
default_kvs.insert("enable".to_string(), "on".to_string());
|
||||
default_kvs.insert("endpoint".to_string(), "http://localhost:3020/webhook".to_string());
|
||||
|
||||
default_kvs.insert("enable".to_string(), "off".to_string());
|
||||
default_kvs.insert("endpoint".to_string(), "".to_string());
|
||||
audit_webhook_section.insert("_".to_string(), default_kvs);
|
||||
let mut instance_kvs = KVS::new();
|
||||
instance_kvs.insert("enable".to_string(), "on".to_string());
|
||||
instance_kvs.insert("endpoint".to_string(), "http://localhost:3020/webhook".to_string());
|
||||
audit_webhook_section.insert("primary".to_string(), instance_kvs);
|
||||
config.0.insert("audit_webhook".to_string(), audit_webhook_section);
|
||||
|
||||
let registry = AuditRegistry::new();
|
||||
|
||||
// This should not fail even if server storage is not initialized
|
||||
// as it's an integration test
|
||||
let result = registry.create_audit_targets_from_config(&config).await;
|
||||
assert!(result.is_ok(), "audit target creation should not require server storage");
|
||||
}
|
||||
|
||||
// We expect this to fail due to server storage not being initialized
|
||||
// but the parsing should work correctly
|
||||
match result {
|
||||
Err(AuditError::StorageNotAvailable(_)) => {
|
||||
// This is expected in test environment
|
||||
}
|
||||
Err(e) => {
|
||||
// Other errors might indicate parsing issues
|
||||
println!("Unexpected error: {e}");
|
||||
}
|
||||
Ok(_) => {
|
||||
// Unexpected success in test environment without server storage
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_env_only_audit_target_does_not_require_server_storage() {
|
||||
with_vars(
|
||||
[
|
||||
("RUSTFS_AUDIT_WEBHOOK_ENABLE_PRIMARY", Some("on")),
|
||||
("RUSTFS_AUDIT_WEBHOOK_ENDPOINT_PRIMARY", Some("http://localhost:3020/webhook")),
|
||||
],
|
||||
|| {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("failed to create tokio runtime");
|
||||
runtime.block_on(async {
|
||||
let config = Config(HashMap::new());
|
||||
let registry = AuditRegistry::new();
|
||||
let result = registry.create_audit_targets_from_config(&config).await;
|
||||
assert!(result.is_ok(), "env-only audit target creation should not require server storage");
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -159,7 +159,8 @@ async fn test_audit_log_dispatch_performance() {
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// Dispatch audit log (should be fast since no targets are configured)
|
||||
// Dispatch audit log against an unstarted system state. Empty config keeps
|
||||
// the audit system stopped, so dispatch should fail fast without targets.
|
||||
let result = system.dispatch(Arc::new(audit_entry)).await;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
@@ -168,8 +169,10 @@ async fn test_audit_log_dispatch_performance() {
|
||||
// Should be very fast (sub-millisecond for no targets)
|
||||
assert!(elapsed < Duration::from_millis(100), "Dispatch took too long: {elapsed:?}");
|
||||
|
||||
// Should succeed even with no targets
|
||||
assert!(result.is_ok(), "Dispatch should succeed with no targets");
|
||||
assert!(
|
||||
matches!(result, Err(AuditError::NotInitialized(_))),
|
||||
"Dispatch on a stopped system should return NotInitialized, got: {result:?}"
|
||||
);
|
||||
|
||||
// Clean up
|
||||
let _ = system.close().await;
|
||||
@@ -186,11 +189,11 @@ async fn test_system_state_transitions() {
|
||||
let config = rustfs_ecstore::config::Config(std::collections::HashMap::new());
|
||||
let start_result = system.start(config).await;
|
||||
|
||||
// Should be running (or failed due to server storage)
|
||||
// Empty config keeps the audit system stopped even when start() succeeds.
|
||||
let state = system.get_state().await;
|
||||
match start_result {
|
||||
Ok(_) => {
|
||||
assert_eq!(state, rustfs_audit::system::AuditSystemState::Running);
|
||||
assert_eq!(state, rustfs_audit::system::AuditSystemState::Stopped);
|
||||
}
|
||||
Err(_) => {
|
||||
// Expected in test environment due to server storage not being initialized
|
||||
|
||||
@@ -29,27 +29,21 @@ async fn test_complete_audit_system_lifecycle() {
|
||||
assert_eq!(system.get_state().await, system::AuditSystemState::Stopped);
|
||||
assert!(!system.is_running().await);
|
||||
|
||||
// 2. Start with empty config (will fail due to no server storage in test)
|
||||
// 2. Start with empty config. The current implementation returns Ok(())
|
||||
// but keeps the system stopped when no audit targets are enabled.
|
||||
let config = Config(HashMap::new());
|
||||
let start_result = system.start(config).await;
|
||||
|
||||
// Should fail in test environment but state handling should work
|
||||
// State handling should remain consistent for both empty-config success and
|
||||
// storage-unavailable failure paths.
|
||||
match start_result {
|
||||
Err(AuditError::StorageNotAvailable(_)) => {
|
||||
// Expected in test environment
|
||||
assert_eq!(system.get_state().await, system::AuditSystemState::Stopped);
|
||||
}
|
||||
Ok(_) => {
|
||||
// If it somehow succeeds, verify running state
|
||||
assert_eq!(system.get_state().await, system::AuditSystemState::Running);
|
||||
assert!(system.is_running().await);
|
||||
|
||||
// Test pause/resume
|
||||
system.pause().await.expect("Should pause successfully");
|
||||
assert_eq!(system.get_state().await, system::AuditSystemState::Paused);
|
||||
|
||||
system.resume().await.expect("Should resume successfully");
|
||||
assert_eq!(system.get_state().await, system::AuditSystemState::Running);
|
||||
assert_eq!(system.get_state().await, system::AuditSystemState::Stopped);
|
||||
assert!(!system.is_running().await);
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("Unexpected error: {e}");
|
||||
|
||||
@@ -36,3 +36,6 @@ sha2 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
@@ -349,7 +349,7 @@ mod tests {
|
||||
fn base64_encoded_checksum_to_hex_string(header_value: &HeaderValue) -> String {
|
||||
let decoded_checksum = base64::decode(header_value.to_str().unwrap()).unwrap();
|
||||
let decoded_checksum = decoded_checksum.into_iter().fold(String::new(), |mut acc, byte| {
|
||||
write!(acc, "{byte:02X?}").expect("string will always be writeable");
|
||||
write!(acc, "{byte:02X?}").expect("string will always be writable");
|
||||
acc
|
||||
});
|
||||
|
||||
|
||||
@@ -41,3 +41,6 @@ rmp-serde = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
s3s = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
// 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 std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::{Duration, Instant};
|
||||
use uuid::Uuid;
|
||||
|
||||
const CAPACITY_SCOPE_REGISTRY_SOFT_LIMIT: usize = 2_048;
|
||||
const CAPACITY_SCOPE_REGISTRY_HARD_LIMIT: usize = 4_096;
|
||||
const CAPACITY_SCOPE_TTL: Duration = Duration::from_secs(300);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct CapacityScopeDisk {
|
||||
pub endpoint: String,
|
||||
pub drive_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct CapacityScope {
|
||||
pub disks: Vec<CapacityScopeDisk>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CapacityScopeEntry {
|
||||
scope: CapacityScope,
|
||||
recorded_at: Instant,
|
||||
}
|
||||
|
||||
fn capacity_scope_registry() -> &'static Mutex<HashMap<Uuid, CapacityScopeEntry>> {
|
||||
static REGISTRY: OnceLock<Mutex<HashMap<Uuid, CapacityScopeEntry>>> = OnceLock::new();
|
||||
REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
fn global_dirty_scope_registry() -> &'static Mutex<HashSet<CapacityScopeDisk>> {
|
||||
static REGISTRY: OnceLock<Mutex<HashSet<CapacityScopeDisk>>> = OnceLock::new();
|
||||
REGISTRY.get_or_init(|| Mutex::new(HashSet::new()))
|
||||
}
|
||||
|
||||
fn prune_expired_entries(entries: &mut HashMap<Uuid, CapacityScopeEntry>, now: Instant) {
|
||||
entries.retain(|_, entry| now.duration_since(entry.recorded_at) <= CAPACITY_SCOPE_TTL);
|
||||
}
|
||||
|
||||
fn enforce_hard_limit(entries: &mut HashMap<Uuid, CapacityScopeEntry>, max_len: usize) {
|
||||
if entries.len() < max_len {
|
||||
return;
|
||||
}
|
||||
|
||||
let evict_count = entries.len() - max_len + 1;
|
||||
let mut eviction_order: Vec<_> = entries.iter().map(|(token, entry)| (*token, entry.recorded_at)).collect();
|
||||
eviction_order.sort_unstable_by_key(|(_, recorded_at)| *recorded_at);
|
||||
|
||||
for (token, _) in eviction_order.into_iter().take(evict_count) {
|
||||
entries.remove(&token);
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_capacity_scopes(existing: &mut CapacityScope, incoming: CapacityScope) {
|
||||
let mut seen: HashSet<CapacityScopeDisk> = existing.disks.iter().cloned().collect();
|
||||
for disk in incoming.disks {
|
||||
if seen.insert(disk.clone()) {
|
||||
existing.disks.push(disk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_capacity_scope(token: Uuid, scope: CapacityScope) {
|
||||
let now = Instant::now();
|
||||
let mut entries = capacity_scope_registry().lock().unwrap_or_else(|p| p.into_inner());
|
||||
if !entries.contains_key(&token) && entries.len() >= CAPACITY_SCOPE_REGISTRY_SOFT_LIMIT {
|
||||
prune_expired_entries(&mut entries, now);
|
||||
enforce_hard_limit(&mut entries, CAPACITY_SCOPE_REGISTRY_HARD_LIMIT);
|
||||
}
|
||||
if let Some(entry) = entries.get_mut(&token) {
|
||||
merge_capacity_scopes(&mut entry.scope, scope);
|
||||
entry.recorded_at = now;
|
||||
} else {
|
||||
entries.insert(token, CapacityScopeEntry { scope, recorded_at: now });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_capacity_scope(token: Uuid) -> Option<CapacityScope> {
|
||||
let now = Instant::now();
|
||||
let mut entries = capacity_scope_registry().lock().unwrap_or_else(|p| p.into_inner());
|
||||
let entry = entries.remove(&token)?;
|
||||
if now.duration_since(entry.recorded_at) > CAPACITY_SCOPE_TTL {
|
||||
return None;
|
||||
}
|
||||
Some(entry.scope)
|
||||
}
|
||||
|
||||
pub fn record_global_dirty_scope(scope: CapacityScope) {
|
||||
if scope.disks.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut dirty_scopes = global_dirty_scope_registry().lock().unwrap_or_else(|p| p.into_inner());
|
||||
dirty_scopes.extend(scope.disks);
|
||||
}
|
||||
|
||||
pub fn drain_global_dirty_scopes() -> Vec<CapacityScopeDisk> {
|
||||
let mut dirty_scopes = global_dirty_scope_registry().lock().unwrap_or_else(|p| p.into_inner());
|
||||
dirty_scopes.drain().collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
|
||||
fn test_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
fn clear_capacity_scope_registry_for_test() {
|
||||
capacity_scope_registry()
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.clear();
|
||||
global_dirty_scope_registry()
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.clear();
|
||||
}
|
||||
|
||||
fn poison_capacity_scope_registry_for_test() {
|
||||
let _ = thread::spawn(|| {
|
||||
let _guard = capacity_scope_registry()
|
||||
.lock()
|
||||
.expect("capacity scope registry lock should succeed");
|
||||
panic!("poison capacity scope registry");
|
||||
})
|
||||
.join();
|
||||
}
|
||||
|
||||
fn poison_global_dirty_scope_registry_for_test() {
|
||||
let _ = thread::spawn(|| {
|
||||
let _guard = global_dirty_scope_registry()
|
||||
.lock()
|
||||
.expect("global dirty scope registry lock should succeed");
|
||||
panic!("poison global dirty scope registry");
|
||||
})
|
||||
.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_and_take_capacity_scope_round_trips() {
|
||||
let _guard = test_lock().lock().expect("test lock poisoned");
|
||||
clear_capacity_scope_registry_for_test();
|
||||
let token = Uuid::new_v4();
|
||||
let scope = CapacityScope {
|
||||
disks: vec![CapacityScopeDisk {
|
||||
endpoint: "node-a".to_string(),
|
||||
drive_path: "/tmp/disk-a".to_string(),
|
||||
}],
|
||||
};
|
||||
|
||||
record_capacity_scope(token, scope.clone());
|
||||
|
||||
assert_eq!(take_capacity_scope(token), Some(scope));
|
||||
assert_eq!(take_capacity_scope(token), None);
|
||||
clear_capacity_scope_registry_for_test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_capacity_scope_merges_disks_for_same_token() {
|
||||
let _guard = test_lock().lock().expect("test lock poisoned");
|
||||
clear_capacity_scope_registry_for_test();
|
||||
let token = Uuid::new_v4();
|
||||
record_capacity_scope(
|
||||
token,
|
||||
CapacityScope {
|
||||
disks: vec![CapacityScopeDisk {
|
||||
endpoint: "node-a".to_string(),
|
||||
drive_path: "/tmp/disk-a".to_string(),
|
||||
}],
|
||||
},
|
||||
);
|
||||
record_capacity_scope(
|
||||
token,
|
||||
CapacityScope {
|
||||
disks: vec![
|
||||
CapacityScopeDisk {
|
||||
endpoint: "node-b".to_string(),
|
||||
drive_path: "/tmp/disk-b".to_string(),
|
||||
},
|
||||
CapacityScopeDisk {
|
||||
endpoint: "node-a".to_string(),
|
||||
drive_path: "/tmp/disk-a".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
let scope = take_capacity_scope(token).expect("scope should exist");
|
||||
assert_eq!(scope.disks.len(), 2);
|
||||
assert!(scope.disks.iter().any(|disk| disk.endpoint == "node-a"));
|
||||
assert!(scope.disks.iter().any(|disk| disk.endpoint == "node-b"));
|
||||
clear_capacity_scope_registry_for_test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_capacity_scope_enforces_hard_limit() {
|
||||
let _guard = test_lock().lock().expect("test lock poisoned");
|
||||
clear_capacity_scope_registry_for_test();
|
||||
|
||||
for _ in 0..(CAPACITY_SCOPE_REGISTRY_HARD_LIMIT + 32) {
|
||||
record_capacity_scope(
|
||||
Uuid::new_v4(),
|
||||
CapacityScope {
|
||||
disks: vec![CapacityScopeDisk {
|
||||
endpoint: "node-a".to_string(),
|
||||
drive_path: "/tmp/disk-a".to_string(),
|
||||
}],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let entries = capacity_scope_registry()
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
assert!(entries.len() <= CAPACITY_SCOPE_REGISTRY_HARD_LIMIT);
|
||||
drop(entries);
|
||||
clear_capacity_scope_registry_for_test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_capacity_scope_recovers_from_poisoned_registry() {
|
||||
let _guard = test_lock().lock().expect("test lock poisoned");
|
||||
clear_capacity_scope_registry_for_test();
|
||||
poison_capacity_scope_registry_for_test();
|
||||
let token = Uuid::new_v4();
|
||||
let scope = CapacityScope {
|
||||
disks: vec![CapacityScopeDisk {
|
||||
endpoint: "node-a".to_string(),
|
||||
drive_path: "/tmp/disk-a".to_string(),
|
||||
}],
|
||||
};
|
||||
|
||||
record_capacity_scope(token, scope.clone());
|
||||
|
||||
assert_eq!(take_capacity_scope(token), Some(scope));
|
||||
clear_capacity_scope_registry_for_test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_and_drain_global_dirty_scope_round_trips() {
|
||||
let _guard = test_lock().lock().expect("test lock poisoned");
|
||||
clear_capacity_scope_registry_for_test();
|
||||
record_global_dirty_scope(CapacityScope {
|
||||
disks: vec![CapacityScopeDisk {
|
||||
endpoint: "node-a".to_string(),
|
||||
drive_path: "/tmp/disk-a".to_string(),
|
||||
}],
|
||||
});
|
||||
record_global_dirty_scope(CapacityScope {
|
||||
disks: vec![
|
||||
CapacityScopeDisk {
|
||||
endpoint: "node-b".to_string(),
|
||||
drive_path: "/tmp/disk-b".to_string(),
|
||||
},
|
||||
CapacityScopeDisk {
|
||||
endpoint: "node-a".to_string(),
|
||||
drive_path: "/tmp/disk-a".to_string(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let drained = drain_global_dirty_scopes();
|
||||
assert_eq!(drained.len(), 2);
|
||||
assert!(drained.iter().any(|disk| disk.endpoint == "node-a"));
|
||||
assert!(drained.iter().any(|disk| disk.endpoint == "node-b"));
|
||||
assert!(drain_global_dirty_scopes().is_empty());
|
||||
clear_capacity_scope_registry_for_test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_global_dirty_scope_recovers_from_poisoned_registry() {
|
||||
let _guard = test_lock().lock().expect("test lock poisoned");
|
||||
clear_capacity_scope_registry_for_test();
|
||||
poison_global_dirty_scope_registry_for_test();
|
||||
|
||||
record_global_dirty_scope(CapacityScope {
|
||||
disks: vec![CapacityScopeDisk {
|
||||
endpoint: "node-a".to_string(),
|
||||
drive_path: "/tmp/disk-a".to_string(),
|
||||
}],
|
||||
});
|
||||
|
||||
let drained = drain_global_dirty_scopes();
|
||||
assert_eq!(drained.len(), 1);
|
||||
assert_eq!(drained[0].endpoint, "node-a");
|
||||
clear_capacity_scope_registry_for_test();
|
||||
}
|
||||
}
|
||||
@@ -51,14 +51,14 @@ impl AllTierStats {
|
||||
pub fn add_sizes(&mut self, tiers: HashMap<String, TierStats>) {
|
||||
for (tier, st) in tiers {
|
||||
self.tiers
|
||||
.insert(tier.clone(), self.tiers.get(&tier).unwrap_or(&TierStats::default()).add(&st));
|
||||
.insert(tier.clone(), self.tiers.get(&tier).copied().unwrap_or_default().add(&st));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: AllTierStats) {
|
||||
for (tier, st) in other.tiers {
|
||||
self.tiers
|
||||
.insert(tier.clone(), self.tiers.get(&tier).unwrap_or(&TierStats::default()).add(&st));
|
||||
.insert(tier.clone(), self.tiers.get(&tier).copied().unwrap_or_default().add(&st));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,10 +458,7 @@ impl DataUsageEntry {
|
||||
self.size += other.size;
|
||||
|
||||
if let Some(o_rep) = &other.replication_stats {
|
||||
if self.replication_stats.is_none() {
|
||||
self.replication_stats = Some(ReplicationAllStats::default());
|
||||
}
|
||||
let s_rep = self.replication_stats.as_mut().unwrap();
|
||||
let s_rep = self.replication_stats.get_or_insert_with(ReplicationAllStats::default);
|
||||
s_rep.targets.clear();
|
||||
s_rep.replica_size += o_rep.replica_size;
|
||||
s_rep.replica_count += o_rep.replica_count;
|
||||
@@ -586,7 +583,7 @@ impl DataUsageCache {
|
||||
return Some(root);
|
||||
}
|
||||
let mut flat = self.flatten(&root);
|
||||
if flat.replication_stats.is_some() && flat.replication_stats.as_ref().unwrap().empty() {
|
||||
if flat.replication_stats.as_ref().is_some_and(|stats| stats.empty()) {
|
||||
flat.replication_stats = None;
|
||||
}
|
||||
Some(flat)
|
||||
@@ -676,10 +673,12 @@ impl DataUsageCache {
|
||||
let mut leaves = Vec::new();
|
||||
let mut remove = total - limit;
|
||||
add(self, path, &mut leaves);
|
||||
leaves.sort_by(|a, b| a.objects.cmp(&b.objects));
|
||||
leaves.sort_by_key(|a| a.objects);
|
||||
|
||||
while remove > 0 && !leaves.is_empty() {
|
||||
let e = leaves.first().unwrap();
|
||||
let Some(e) = leaves.first() else {
|
||||
break;
|
||||
};
|
||||
let candidate = e.path.clone();
|
||||
if candidate == *path && !compact_self {
|
||||
break;
|
||||
@@ -703,12 +702,9 @@ impl DataUsageCache {
|
||||
}
|
||||
|
||||
pub fn total_children_rec(&self, path: &str) -> usize {
|
||||
let root = self.find(path);
|
||||
|
||||
if root.is_none() {
|
||||
let Some(root) = self.find(path) else {
|
||||
return 0;
|
||||
}
|
||||
let root = root.unwrap();
|
||||
};
|
||||
if root.children.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
@@ -721,31 +717,36 @@ impl DataUsageCache {
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, o: &DataUsageCache) {
|
||||
let mut existing_root = self.root();
|
||||
let other_root = o.root();
|
||||
if existing_root.is_none() && other_root.is_none() {
|
||||
return;
|
||||
}
|
||||
if other_root.is_none() {
|
||||
return;
|
||||
}
|
||||
if existing_root.is_none() {
|
||||
let Some(mut existing_root) = self.root() else {
|
||||
if o.root().is_none() {
|
||||
return;
|
||||
}
|
||||
*self = o.clone();
|
||||
return;
|
||||
}
|
||||
if o.info.last_update.gt(&self.info.last_update) {
|
||||
};
|
||||
|
||||
let Some(other_root) = o.root() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if o.info.last_update > self.info.last_update {
|
||||
self.info.last_update = o.info.last_update;
|
||||
}
|
||||
|
||||
existing_root.as_mut().unwrap().merge(other_root.as_ref().unwrap());
|
||||
self.cache.insert(hash_path(&self.info.name).key(), existing_root.unwrap());
|
||||
let e_hash = self.root_hash();
|
||||
for key in other_root.as_ref().unwrap().children.iter() {
|
||||
let entry = &o.cache[key];
|
||||
existing_root.merge(&other_root);
|
||||
self.cache.insert(hash_path(&self.info.name).key(), existing_root);
|
||||
|
||||
let root_hash = self.root_hash();
|
||||
for key in other_root.children.iter() {
|
||||
let Some(entry) = o.cache.get(key) else {
|
||||
continue;
|
||||
};
|
||||
let flat = o.flatten(entry);
|
||||
let mut existing = self.cache[key].clone();
|
||||
existing.merge(&flat);
|
||||
self.replace_hashed(&DataUsageHash(key.clone()), &Some(e_hash.clone()), &existing);
|
||||
if let Some(existing) = self.cache.get_mut(key) {
|
||||
existing.merge(&flat);
|
||||
} else {
|
||||
self.replace_hashed(&DataUsageHash(key.clone()), &Some(root_hash.clone()), &flat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1096,7 +1097,7 @@ impl DataUsageInfo {
|
||||
|
||||
/// Add bucket usage info
|
||||
pub fn add_bucket_usage(&mut self, bucket: String, usage: BucketUsageInfo) {
|
||||
self.buckets_usage.insert(bucket.clone(), usage);
|
||||
self.buckets_usage.insert(bucket, usage);
|
||||
self.buckets_count = self.buckets_usage.len() as u64;
|
||||
self.last_update = Some(SystemTime::now());
|
||||
}
|
||||
@@ -1141,10 +1142,12 @@ impl DataUsageInfo {
|
||||
self.buckets_count = self.buckets_usage.len() as u64;
|
||||
|
||||
// Update last update time
|
||||
if let Some(other_update) = other.last_update
|
||||
&& (self.last_update.is_none() || other_update > self.last_update.unwrap())
|
||||
{
|
||||
self.last_update = Some(other_update);
|
||||
if let Some(other_update) = other.last_update {
|
||||
match self.last_update {
|
||||
None => self.last_update = Some(other_update),
|
||||
Some(self_update) if other_update > self_update => self.last_update = Some(other_update),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1285,4 +1288,59 @@ mod tests {
|
||||
assert_eq!(summary1.total_size, 300);
|
||||
assert_eq!(summary1.versions, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_data_usage_cache_merge_adds_missing_child() {
|
||||
let mut base = DataUsageCache::default();
|
||||
base.info.name = "bucket".to_string();
|
||||
base.replace("bucket", "", DataUsageEntry::default());
|
||||
|
||||
let mut other = DataUsageCache::default();
|
||||
other.info.name = "bucket".to_string();
|
||||
let child = DataUsageEntry {
|
||||
size: 42,
|
||||
..Default::default()
|
||||
};
|
||||
other.replace("bucket/child", "bucket", child);
|
||||
|
||||
base.merge(&other);
|
||||
|
||||
let root = base.find("bucket").expect("root bucket should exist");
|
||||
assert_eq!(root.size, 0);
|
||||
let child_entry = base.find("bucket/child").expect("merged child should be added");
|
||||
assert_eq!(child_entry.size, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_data_usage_cache_merge_accumulates_existing_child() {
|
||||
let mut base = DataUsageCache::default();
|
||||
base.info.name = "bucket".to_string();
|
||||
base.replace(
|
||||
"bucket/child",
|
||||
"bucket",
|
||||
DataUsageEntry {
|
||||
size: 10,
|
||||
objects: 1,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let mut other = DataUsageCache::default();
|
||||
other.info.name = "bucket".to_string();
|
||||
other.replace(
|
||||
"bucket/child",
|
||||
"bucket",
|
||||
DataUsageEntry {
|
||||
size: 20,
|
||||
objects: 2,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
base.merge(&other);
|
||||
|
||||
let child_entry = base.find("bucket/child").expect("child should remain after merge");
|
||||
assert_eq!(child_entry.size, 30);
|
||||
assert_eq!(child_entry.objects, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use std::{
|
||||
fmt::{self, Display},
|
||||
sync::OnceLock,
|
||||
};
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tokio::sync::{broadcast, mpsc, oneshot};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub const HEAL_DELETE_DANGLING: bool = true;
|
||||
@@ -206,11 +206,59 @@ pub struct HealOpts {
|
||||
pub set: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HealAdmissionDropReason {
|
||||
QueueFull,
|
||||
PolicyDropped,
|
||||
}
|
||||
|
||||
impl HealAdmissionDropReason {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::QueueFull => "queue_full",
|
||||
Self::PolicyDropped => "policy_dropped",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HealAdmissionResult {
|
||||
Accepted,
|
||||
Merged,
|
||||
Full,
|
||||
Dropped(HealAdmissionDropReason),
|
||||
}
|
||||
|
||||
impl HealAdmissionResult {
|
||||
pub fn result_label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Accepted => "accepted",
|
||||
Self::Merged => "merged",
|
||||
Self::Full => "full",
|
||||
Self::Dropped(_) => "dropped",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reason_label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Dropped(reason) => reason.as_str(),
|
||||
_ => "none",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_admitted(self) -> bool {
|
||||
matches!(self, Self::Accepted | Self::Merged)
|
||||
}
|
||||
}
|
||||
|
||||
/// Heal channel command type
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub enum HealChannelCommand {
|
||||
/// Start a new heal task
|
||||
Start(HealChannelRequest),
|
||||
Start {
|
||||
request: HealChannelRequest,
|
||||
response_tx: oneshot::Sender<Result<HealAdmissionResult, String>>,
|
||||
},
|
||||
/// Query heal task status
|
||||
Query { heal_path: String, client_token: String },
|
||||
/// Cancel heal task
|
||||
@@ -296,12 +344,13 @@ type HealResponseSender = broadcast::Sender<HealChannelResponse>;
|
||||
static GLOBAL_HEAL_RESPONSE_SENDER: OnceLock<HealResponseSender> = OnceLock::new();
|
||||
|
||||
/// Initialize global heal channel
|
||||
pub fn init_heal_channel() -> HealChannelReceiver {
|
||||
pub fn init_heal_channel() -> Result<HealChannelReceiver, &'static str> {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
GLOBAL_HEAL_CHANNEL_SENDER
|
||||
.set(tx)
|
||||
.expect("Heal channel sender already initialized");
|
||||
rx
|
||||
if GLOBAL_HEAL_CHANNEL_SENDER.set(tx).is_ok() {
|
||||
Ok(rx)
|
||||
} else {
|
||||
Err("Heal channel sender already initialized")
|
||||
}
|
||||
}
|
||||
|
||||
/// Get global heal channel sender
|
||||
@@ -338,9 +387,22 @@ pub fn subscribe_heal_responses() -> broadcast::Receiver<HealChannelResponse> {
|
||||
heal_response_sender().subscribe()
|
||||
}
|
||||
|
||||
/// Send heal start request and wait for structured admission feedback.
|
||||
pub async fn send_heal_request_with_admission(request: HealChannelRequest) -> Result<HealAdmissionResult, String> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
send_heal_command(HealChannelCommand::Start { request, response_tx }).await?;
|
||||
response_rx
|
||||
.await
|
||||
.map_err(|e| format!("Failed to receive heal admission response: {e}"))?
|
||||
}
|
||||
|
||||
/// Send heal start request
|
||||
pub async fn send_heal_request(request: HealChannelRequest) -> Result<(), String> {
|
||||
send_heal_command(HealChannelCommand::Start(request)).await
|
||||
match send_heal_request_with_admission(request).await? {
|
||||
HealAdmissionResult::Accepted | HealAdmissionResult::Merged => Ok(()),
|
||||
HealAdmissionResult::Full => Err("Heal request queue is full".to_string()),
|
||||
HealAdmissionResult::Dropped(reason) => Err(format!("Heal request dropped: {}", reason.as_str())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send heal query request
|
||||
@@ -449,7 +511,7 @@ pub fn lc_has_active_rules(config: &BucketLifecycleConfiguration, prefix: &str)
|
||||
}
|
||||
|
||||
if let Some(e) = &rule.noncurrent_version_expiration {
|
||||
if let Some(true) = e.noncurrent_days.map(|d| d > 0) {
|
||||
if e.noncurrent_days.is_some() {
|
||||
return true;
|
||||
}
|
||||
if let Some(true) = e.newer_noncurrent_versions.map(|d| d > 0) {
|
||||
@@ -541,6 +603,19 @@ pub async fn send_heal_disk(set_disk_id: String, priority: Option<HealChannelPri
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn heal_admission_result_labels_are_stable() {
|
||||
assert_eq!(HealAdmissionResult::Accepted.result_label(), "accepted");
|
||||
assert_eq!(HealAdmissionResult::Merged.result_label(), "merged");
|
||||
assert_eq!(HealAdmissionResult::Full.result_label(), "full");
|
||||
assert_eq!(
|
||||
HealAdmissionResult::Dropped(HealAdmissionDropReason::QueueFull).reason_label(),
|
||||
"queue_full"
|
||||
);
|
||||
assert!(HealAdmissionResult::Merged.is_admitted());
|
||||
assert!(!HealAdmissionResult::Full.is_admitted());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn heal_response_broadcast_reaches_subscriber() {
|
||||
let mut receiver = subscribe_heal_responses();
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -155,10 +155,7 @@ impl LastMinuteLatency {
|
||||
}
|
||||
|
||||
pub fn add(&mut self, t: &Duration) {
|
||||
let sec = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
let sec = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
|
||||
self.forward_to(sec);
|
||||
let win_idx = sec % 60;
|
||||
self.totals[win_idx as usize].add(t);
|
||||
@@ -174,10 +171,7 @@ impl LastMinuteLatency {
|
||||
|
||||
pub fn get_total(&mut self) -> AccElem {
|
||||
let mut res = AccElem::default();
|
||||
let sec = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
let sec = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
|
||||
self.forward_to(sec);
|
||||
for elem in self.totals.iter() {
|
||||
res.merge(elem);
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
// 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;
|
||||
|
||||
+222
-210
@@ -22,12 +22,12 @@ use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
Arc, OnceLock,
|
||||
Arc, Mutex, OnceLock,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum IlmAction {
|
||||
@@ -110,7 +110,7 @@ pub fn global_metrics() -> &'static Arc<Metrics> {
|
||||
GLOBAL_METRICS.get_or_init(|| Arc::new(Metrics::new()))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, PartialOrd)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Metric {
|
||||
// START Realtime metrics, that only records
|
||||
// last minute latencies and total operation count.
|
||||
@@ -188,7 +188,6 @@ impl Metric {
|
||||
if index >= Self::Last as usize {
|
||||
return None;
|
||||
}
|
||||
// Safe conversion using match instead of unsafe transmute
|
||||
match index {
|
||||
0 => Some(Self::ReadMetadata),
|
||||
1 => Some(Self::CheckMissing),
|
||||
@@ -220,16 +219,48 @@ impl Metric {
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-safe wrapper for LastMinuteLatency with atomic operations
|
||||
#[derive(Default)]
|
||||
// ---------------------------------------------------------------------------
|
||||
// LockedLastMinuteLatency
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Uses std::sync::Mutex instead of tokio::sync::Mutex.
|
||||
//
|
||||
// Rationale: the critical section is a handful of integer additions inside
|
||||
// LastMinuteLatency::add_all / get_total — no I/O, no blocking syscalls, no
|
||||
// awaiting. A std blocking mutex is cheaper (no task-wakeup overhead) and,
|
||||
// crucially, lets every caller be *synchronous*. That eliminates the need to
|
||||
// spawn a background task just to record a duration, which was the only reason
|
||||
// tokio::spawn appeared in log/time/time_size/time_n/time_ilm.
|
||||
//
|
||||
// Note: vec![LockedLastMinuteLatency::default(); N] with the old Arc-based
|
||||
// Clone made every element share the *same* inner mutex — a latent bug where
|
||||
// all metrics slots wrote to one counter. The new Clone creates a fresh
|
||||
// independent Mutex per element, matching the intent.
|
||||
|
||||
/// Thread-safe wrapper for LastMinuteLatency backed by a std blocking mutex.
|
||||
pub struct LockedLastMinuteLatency {
|
||||
// Arc so Clone is cheap *and* each cloned value stays independent (its own
|
||||
// allocation). We never hand out the Arc to two Metrics at once; the Arc
|
||||
// is purely a convenience for the Clone impl below.
|
||||
latency: Arc<Mutex<LastMinuteLatency>>,
|
||||
}
|
||||
|
||||
impl Default for LockedLastMinuteLatency {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// Produce a fresh, independent slot — *not* a shared alias. This is what
|
||||
// vec![val; N] and #[derive(Clone)] on the parent struct both need.
|
||||
impl Clone for LockedLastMinuteLatency {
|
||||
fn clone(&self) -> Self {
|
||||
let inner = match self.latency.lock() {
|
||||
Ok(guard) => guard.clone(),
|
||||
Err(poisoned) => poisoned.into_inner().clone(),
|
||||
};
|
||||
Self {
|
||||
latency: Arc::clone(&self.latency),
|
||||
latency: Arc::new(Mutex::new(inner)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,14 +272,13 @@ impl LockedLastMinuteLatency {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a duration measurement
|
||||
pub async fn add(&self, duration: Duration) {
|
||||
self.add_size(duration, 0).await;
|
||||
/// Record a duration sample (no size).
|
||||
pub fn add(&self, duration: Duration) {
|
||||
self.add_size(duration, 0);
|
||||
}
|
||||
|
||||
/// Add a duration measurement with size
|
||||
pub async fn add_size(&self, duration: Duration, size: u64) {
|
||||
let mut latency = self.latency.lock().await;
|
||||
/// Record a duration sample with an associated byte count.
|
||||
pub fn add_size(&self, duration: Duration, size: u64) {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
@@ -259,17 +289,27 @@ impl LockedLastMinuteLatency {
|
||||
total: duration.as_secs(),
|
||||
size,
|
||||
};
|
||||
latency.add_all(now, &elem);
|
||||
|
||||
match self.latency.lock() {
|
||||
Ok(mut guard) => guard.add_all(now, &elem),
|
||||
Err(poisoned) => poisoned.into_inner().add_all(now, &elem),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get total accumulated metrics for the last minute
|
||||
pub async fn total(&self) -> AccElem {
|
||||
let mut latency = self.latency.lock().await;
|
||||
latency.get_total()
|
||||
/// Return accumulated totals for the last minute window.
|
||||
pub fn total(&self) -> AccElem {
|
||||
match self.latency.lock() {
|
||||
Ok(mut latency) => latency.get_total(),
|
||||
Err(poisoned) => poisoned.into_inner().get_total(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Current path tracker for monitoring active scan paths
|
||||
// ---------------------------------------------------------------------------
|
||||
// CurrentPathTracker — unchanged, still uses tokio::sync::RwLock because it
|
||||
// lives inside async path-update callbacks.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct CurrentPathTracker {
|
||||
current_path: Arc<RwLock<String>>,
|
||||
}
|
||||
@@ -290,17 +330,16 @@ impl CurrentPathTracker {
|
||||
}
|
||||
}
|
||||
|
||||
/// Main scanner metrics structure
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metrics
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct Metrics {
|
||||
// All fields must be accessed atomically and aligned.
|
||||
operations: Vec<AtomicU64>,
|
||||
latency: Vec<LockedLastMinuteLatency>,
|
||||
actions: Vec<AtomicU64>,
|
||||
actions_latency: Vec<LockedLastMinuteLatency>,
|
||||
// Current paths contains disk -> tracker mappings
|
||||
current_paths: Arc<RwLock<HashMap<String, Arc<CurrentPathTracker>>>>,
|
||||
|
||||
// Cycle information
|
||||
cycle_info: Arc<RwLock<Option<CurrentCycle>>>,
|
||||
}
|
||||
|
||||
@@ -331,8 +370,6 @@ const OTEL_SCANNER_CYCLES: &str = "rustfs_scanner_cycles_total";
|
||||
const OTEL_SCANNER_CYCLE_DURATION_SECONDS: &str = "rustfs_scanner_cycle_duration_seconds";
|
||||
const OTEL_SCANNER_BUCKET_DRIVE_DURATION_SECONDS: &str = "rustfs_scanner_bucket_drive_duration_seconds";
|
||||
|
||||
/// Emit an OTEL counter increment for the given scanner metric.
|
||||
/// ScanCycle and ScanBucketDrive are handled by dedicated emit functions with labels.
|
||||
fn emit_otel_counter(metric: usize, count: u64) {
|
||||
match Metric::from_index(metric) {
|
||||
Some(Metric::ScanObject) => {
|
||||
@@ -345,8 +382,6 @@ fn emit_otel_counter(metric: usize, count: u64) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit OTel metrics for a completed scan cycle.
|
||||
/// Counter with result label + gauge for last successful cycle duration.
|
||||
pub fn emit_scan_cycle_complete(success: bool, duration: Duration) {
|
||||
let result = if success { "success" } else { "error" };
|
||||
metrics::counter!(OTEL_SCANNER_CYCLES, "result" => result).increment(1);
|
||||
@@ -355,8 +390,6 @@ pub fn emit_scan_cycle_complete(success: bool, duration: Duration) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit OTel metrics for a completed bucket-drive scan.
|
||||
/// Counter with result/bucket/disk labels + histogram for duration.
|
||||
pub fn emit_scan_bucket_drive_complete(success: bool, bucket: &str, disk: &str, duration: Duration) {
|
||||
let result = if success { "success" } else { "error" };
|
||||
metrics::counter!(
|
||||
@@ -376,226 +409,202 @@ pub fn emit_scan_bucket_drive_complete(success: bool, bucket: &str, disk: &str,
|
||||
|
||||
impl Metrics {
|
||||
pub fn new() -> Self {
|
||||
let operations = (0..Metric::Last as usize).map(|_| AtomicU64::new(0)).collect();
|
||||
|
||||
let latency = (0..Metric::LastRealtime as usize)
|
||||
.map(|_| LockedLastMinuteLatency::new())
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
operations,
|
||||
latency,
|
||||
operations: (0..Metric::Last as usize).map(|_| AtomicU64::new(0)).collect(),
|
||||
// Each slot gets its own fresh LockedLastMinuteLatency so that
|
||||
// different metrics never accidentally share state.
|
||||
latency: (0..Metric::LastRealtime as usize)
|
||||
.map(|_| LockedLastMinuteLatency::new())
|
||||
.collect(),
|
||||
actions: (0..IlmAction::ActionCount as usize).map(|_| AtomicU64::new(0)).collect(),
|
||||
actions_latency: vec![LockedLastMinuteLatency::default(); IlmAction::ActionCount as usize],
|
||||
actions_latency: (0..IlmAction::ActionCount as usize)
|
||||
.map(|_| LockedLastMinuteLatency::new())
|
||||
.collect(),
|
||||
current_paths: Arc::new(RwLock::new(HashMap::new())),
|
||||
cycle_info: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Log scanner action with custom metadata - compatible with existing usage
|
||||
// -----------------------------------------------------------------------
|
||||
// Metric recording helpers
|
||||
//
|
||||
// All of these are now pure sync closures. No tokio::spawn, no heap-
|
||||
// allocated future, no scheduler overhead — just an atomic increment and a
|
||||
// std-mutex lock for fewer than ~10 ns of work.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Return a closure that records one observation of `metric` (with
|
||||
/// optional caller-supplied metadata). Call it once the operation ends.
|
||||
pub fn log(metric: Metric) -> impl Fn(&HashMap<String, String>) {
|
||||
let metric = metric as usize;
|
||||
let start_time = SystemTime::now();
|
||||
let metric_idx = metric as usize;
|
||||
let start = SystemTime::now();
|
||||
move |_custom: &HashMap<String, String>| {
|
||||
let duration = SystemTime::now().duration_since(start_time).unwrap_or_default();
|
||||
|
||||
// Update operation count
|
||||
global_metrics().operations[metric].fetch_add(1, Ordering::Relaxed);
|
||||
emit_otel_counter(metric, 1);
|
||||
|
||||
// Update latency for realtime metrics (spawn async task for this)
|
||||
if (metric) < Metric::LastRealtime as usize {
|
||||
let metric_index = metric;
|
||||
tokio::spawn(async move {
|
||||
global_metrics().latency[metric_index].add(duration).await;
|
||||
});
|
||||
}
|
||||
|
||||
// Log trace metrics
|
||||
if metric as u8 > Metric::StartTrace as u8 {
|
||||
//debug!(metric = metric.as_str(), duration_ms = duration.as_millis(), "Scanner trace metric");
|
||||
let duration = SystemTime::now().duration_since(start).unwrap_or_default();
|
||||
global_metrics().operations[metric_idx].fetch_add(1, Ordering::Relaxed);
|
||||
emit_otel_counter(metric_idx, 1);
|
||||
if metric_idx < Metric::LastRealtime as usize {
|
||||
global_metrics().latency[metric_idx].add(duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Time scanner action with size - returns function that takes size
|
||||
/// Return a closure that records one observation of `metric` together with
|
||||
/// a byte count. Call `done(size_bytes)` when the operation ends.
|
||||
pub fn time_size(metric: Metric) -> impl Fn(u64) {
|
||||
let metric = metric as usize;
|
||||
let start_time = SystemTime::now();
|
||||
let metric_idx = metric as usize;
|
||||
let start = SystemTime::now();
|
||||
move |size: u64| {
|
||||
let duration = SystemTime::now().duration_since(start_time).unwrap_or_default();
|
||||
|
||||
// Update operation count
|
||||
global_metrics().operations[metric].fetch_add(1, Ordering::Relaxed);
|
||||
emit_otel_counter(metric, 1);
|
||||
|
||||
// Update latency for realtime metrics with size (spawn async task)
|
||||
if (metric) < Metric::LastRealtime as usize {
|
||||
let metric_index = metric;
|
||||
tokio::spawn(async move {
|
||||
global_metrics().latency[metric_index].add_size(duration, size).await;
|
||||
});
|
||||
let duration = SystemTime::now().duration_since(start).unwrap_or_default();
|
||||
global_metrics().operations[metric_idx].fetch_add(1, Ordering::Relaxed);
|
||||
emit_otel_counter(metric_idx, 1);
|
||||
if metric_idx < Metric::LastRealtime as usize {
|
||||
global_metrics().latency[metric_idx].add_size(duration, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Time a scanner action - returns a closure to call when done
|
||||
/// Return a closure that records one observation of `metric`.
|
||||
/// Call `done()` when the operation ends.
|
||||
pub fn time(metric: Metric) -> impl Fn() {
|
||||
let metric = metric as usize;
|
||||
let start_time = SystemTime::now();
|
||||
let metric_idx = metric as usize;
|
||||
let start = SystemTime::now();
|
||||
move || {
|
||||
let duration = SystemTime::now().duration_since(start_time).unwrap_or_default();
|
||||
|
||||
// Update operation count
|
||||
global_metrics().operations[metric].fetch_add(1, Ordering::Relaxed);
|
||||
emit_otel_counter(metric, 1);
|
||||
|
||||
// Update latency for realtime metrics (spawn async task)
|
||||
if (metric) < Metric::LastRealtime as usize {
|
||||
let metric_index = metric;
|
||||
tokio::spawn(async move {
|
||||
global_metrics().latency[metric_index].add(duration).await;
|
||||
});
|
||||
let duration = SystemTime::now().duration_since(start).unwrap_or_default();
|
||||
global_metrics().operations[metric_idx].fetch_add(1, Ordering::Relaxed);
|
||||
emit_otel_counter(metric_idx, 1);
|
||||
if metric_idx < Metric::LastRealtime as usize {
|
||||
global_metrics().latency[metric_idx].add(duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Time N scanner actions - returns function that takes count, then returns completion function
|
||||
/// Return a two-stage closure: first call takes an item count, second call
|
||||
/// (returned closure) fires when the batch of `count` operations ends.
|
||||
pub fn time_n(metric: Metric) -> Box<dyn Fn(usize) -> Box<dyn Fn() + Send + Sync> + Send + Sync> {
|
||||
let metric = metric as usize;
|
||||
let start_time = SystemTime::now();
|
||||
let metric_idx = metric as usize;
|
||||
let start = SystemTime::now();
|
||||
Box::new(move |count: usize| {
|
||||
Box::new(move || {
|
||||
let duration = SystemTime::now().duration_since(start_time).unwrap_or_default();
|
||||
|
||||
// Update operation count
|
||||
global_metrics().operations[metric].fetch_add(count as u64, Ordering::Relaxed);
|
||||
emit_otel_counter(metric, count as u64);
|
||||
|
||||
// Update latency for realtime metrics (spawn async task)
|
||||
if (metric) < Metric::LastRealtime as usize {
|
||||
let metric_index = metric;
|
||||
tokio::spawn(async move {
|
||||
global_metrics().latency[metric_index].add(duration).await;
|
||||
});
|
||||
let duration = SystemTime::now().duration_since(start).unwrap_or_default();
|
||||
global_metrics().operations[metric_idx].fetch_add(count as u64, Ordering::Relaxed);
|
||||
emit_otel_counter(metric_idx, count as u64);
|
||||
if metric_idx < Metric::LastRealtime as usize {
|
||||
global_metrics().latency[metric_idx].add(duration);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Time ILM action with versions - returns function that takes versions, then returns completion function
|
||||
/// Return a two-stage closure for ILM actions: first call takes a version
|
||||
/// count, second call fires when the action completes.
|
||||
pub fn time_ilm(a: IlmAction) -> Box<dyn Fn(u64) -> Box<dyn Fn() + Send + Sync> + Send + Sync> {
|
||||
let a_clone = a as usize;
|
||||
if a_clone == IlmAction::NoneAction as usize || a_clone >= IlmAction::ActionCount as usize {
|
||||
return Box::new(move |_: u64| Box::new(move || {}));
|
||||
let a_idx = a as usize;
|
||||
if a_idx == IlmAction::NoneAction as usize || a_idx >= IlmAction::ActionCount as usize {
|
||||
return Box::new(|_| Box::new(|| {}));
|
||||
}
|
||||
let start = SystemTime::now();
|
||||
Box::new(move |versions: u64| {
|
||||
Box::new(move || {
|
||||
let duration = SystemTime::now().duration_since(start).unwrap_or(Duration::from_secs(0));
|
||||
tokio::spawn(async move {
|
||||
global_metrics().actions[a_clone].fetch_add(versions, Ordering::Relaxed);
|
||||
global_metrics().actions_latency[a_clone].add(duration).await;
|
||||
});
|
||||
let duration = SystemTime::now().duration_since(start).unwrap_or_default();
|
||||
global_metrics().actions[a_idx].fetch_add(versions, Ordering::Relaxed);
|
||||
global_metrics().actions_latency[a_idx].add(duration);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Increment time with specific duration
|
||||
pub async fn inc_time(metric: Metric, duration: Duration) {
|
||||
let metric = metric as usize;
|
||||
// Update operation count
|
||||
global_metrics().operations[metric].fetch_add(1, Ordering::Relaxed);
|
||||
emit_otel_counter(metric, 1);
|
||||
|
||||
// Update latency for realtime metrics
|
||||
if (metric) < Metric::LastRealtime as usize {
|
||||
global_metrics().latency[metric].add(duration).await;
|
||||
/// Record a single observation of `metric` with a caller-supplied duration.
|
||||
/// No longer async — nothing inside requires it.
|
||||
pub fn inc_time(metric: Metric, duration: Duration) {
|
||||
let metric_idx = metric as usize;
|
||||
global_metrics().operations[metric_idx].fetch_add(1, Ordering::Relaxed);
|
||||
emit_otel_counter(metric_idx, 1);
|
||||
if metric_idx < Metric::LastRealtime as usize {
|
||||
global_metrics().latency[metric_idx].add(duration);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get lifetime operation count for a metric
|
||||
// -----------------------------------------------------------------------
|
||||
// Read-side helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Lifetime operation count for `metric`.
|
||||
pub fn lifetime(&self, metric: Metric) -> u64 {
|
||||
let metric = metric as usize;
|
||||
if (metric) >= Metric::Last as usize {
|
||||
let idx = metric as usize;
|
||||
if idx >= Metric::Last as usize {
|
||||
return 0;
|
||||
}
|
||||
self.operations[metric].load(Ordering::Relaxed)
|
||||
self.operations[idx].load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Get last minute statistics for a metric
|
||||
pub async fn last_minute(&self, metric: Metric) -> AccElem {
|
||||
let metric = metric as usize;
|
||||
if (metric) >= Metric::LastRealtime as usize {
|
||||
/// Last-minute accumulated stats for a realtime metric.
|
||||
/// No longer async — LockedLastMinuteLatency::total() is now synchronous.
|
||||
pub fn last_minute(&self, metric: Metric) -> AccElem {
|
||||
let idx = metric as usize;
|
||||
if idx >= Metric::LastRealtime as usize {
|
||||
return AccElem::default();
|
||||
}
|
||||
self.latency[metric].total().await
|
||||
self.latency[idx].total()
|
||||
}
|
||||
|
||||
/// Set current cycle information
|
||||
/// Replace the current cycle record.
|
||||
pub async fn set_cycle(&self, cycle: Option<CurrentCycle>) {
|
||||
*self.cycle_info.write().await = cycle;
|
||||
}
|
||||
|
||||
/// Get current cycle information
|
||||
/// Read the current cycle record.
|
||||
pub async fn get_cycle(&self) -> Option<CurrentCycle> {
|
||||
self.cycle_info.read().await.clone()
|
||||
}
|
||||
|
||||
/// Get current active paths
|
||||
/// Snapshot of every path currently being scanned.
|
||||
pub async fn get_current_paths(&self) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
let paths = self.current_paths.read().await;
|
||||
|
||||
let mut result = Vec::with_capacity(paths.len());
|
||||
for (disk, tracker) in paths.iter() {
|
||||
let path = tracker.get_path().await;
|
||||
result.push(format!("{disk}/{path}"));
|
||||
result.push(format!("{disk}/{}", tracker.get_path().await));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Get number of active drives
|
||||
/// Number of drives with an active scan in progress.
|
||||
pub async fn active_drives(&self) -> usize {
|
||||
self.current_paths.read().await.len()
|
||||
}
|
||||
|
||||
/// Generate metrics report
|
||||
/// Build a full metrics report snapshot.
|
||||
pub async fn report(&self) -> M_ScannerMetrics {
|
||||
let mut metrics = M_ScannerMetrics::default();
|
||||
let mut m = M_ScannerMetrics::default();
|
||||
|
||||
// Set cycle information
|
||||
if let Some(cycle) = self.get_cycle().await {
|
||||
metrics.current_cycle = cycle.current;
|
||||
metrics.cycles_completed_at = cycle.cycle_completed;
|
||||
metrics.current_started = cycle.started;
|
||||
m.current_cycle = cycle.current;
|
||||
m.cycles_completed_at = cycle.cycle_completed;
|
||||
m.current_started = cycle.started;
|
||||
}
|
||||
|
||||
// Replace default start time with global init time if it's the placeholder
|
||||
if let Some(init_time) = crate::get_global_init_time().await {
|
||||
metrics.current_started = init_time;
|
||||
m.current_started = init_time;
|
||||
}
|
||||
|
||||
metrics.collected_at = Utc::now();
|
||||
metrics.active_paths = self.get_current_paths().await;
|
||||
m.collected_at = Utc::now();
|
||||
m.active_paths = self.get_current_paths().await;
|
||||
|
||||
// Lifetime operations
|
||||
// Lifetime operation counts
|
||||
for i in 0..Metric::Last as usize {
|
||||
let count = self.operations[i].load(Ordering::Relaxed);
|
||||
if count > 0
|
||||
&& let Some(metric) = Metric::from_index(i)
|
||||
{
|
||||
metrics.life_time_ops.insert(metric.as_str().to_string(), count);
|
||||
m.life_time_ops.insert(metric.as_str().to_string(), count);
|
||||
}
|
||||
}
|
||||
|
||||
// Last minute statistics for realtime metrics
|
||||
// Last-minute stats for realtime metrics — now plain sync calls
|
||||
for i in 0..Metric::LastRealtime as usize {
|
||||
let last_min = self.latency[i].total().await;
|
||||
let last_min = self.latency[i].total();
|
||||
if last_min.n > 0
|
||||
&& let Some(metric) = Metric::from_index(i)
|
||||
{
|
||||
metrics.last_minute.actions.insert(
|
||||
m.last_minute.actions.insert(
|
||||
metric.as_str().to_string(),
|
||||
TimedAction {
|
||||
count: last_min.n,
|
||||
@@ -606,23 +615,23 @@ impl Metrics {
|
||||
}
|
||||
}
|
||||
|
||||
// Lifetime ILM operations
|
||||
// Lifetime ILM counts
|
||||
for i in 0..IlmAction::ActionCount as usize {
|
||||
let count = self.actions[i].load(Ordering::Relaxed);
|
||||
if count > 0
|
||||
&& let Some(action) = IlmAction::from_index(i)
|
||||
{
|
||||
metrics.life_time_ilm.insert(action.as_str().to_string(), count);
|
||||
m.life_time_ilm.insert(action.as_str().to_string(), count);
|
||||
}
|
||||
}
|
||||
|
||||
// Last minute ILM latency
|
||||
// Last-minute ILM latency — plain sync calls
|
||||
for i in 0..IlmAction::ActionCount as usize {
|
||||
let last_min = self.actions_latency[i].total().await;
|
||||
let last_min = self.actions_latency[i].total();
|
||||
if last_min.n > 0
|
||||
&& let Some(action) = IlmAction::from_index(i)
|
||||
{
|
||||
metrics.last_minute.ilm.insert(
|
||||
m.last_minute.ilm.insert(
|
||||
action.as_str().to_string(),
|
||||
TimedAction {
|
||||
count: last_min.n,
|
||||
@@ -633,56 +642,65 @@ impl Metrics {
|
||||
}
|
||||
}
|
||||
|
||||
metrics
|
||||
m
|
||||
}
|
||||
}
|
||||
|
||||
// Type aliases for compatibility with existing code
|
||||
pub type UpdateCurrentPathFn = Arc<dyn Fn(&str) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
|
||||
pub type CloseDiskFn = Arc<dyn Fn() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
|
||||
|
||||
/// Create a current path updater for tracking scan progress
|
||||
pub fn current_path_updater(disk: &str, initial: &str) -> (UpdateCurrentPathFn, CloseDiskFn) {
|
||||
let tracker = Arc::new(CurrentPathTracker::new(initial.to_string()));
|
||||
let disk_name = disk.to_string();
|
||||
|
||||
// Store the tracker in global metrics
|
||||
let tracker_clone = Arc::clone(&tracker);
|
||||
let disk_clone = disk_name.clone();
|
||||
tokio::spawn(async move {
|
||||
global_metrics().current_paths.write().await.insert(disk_clone, tracker_clone);
|
||||
});
|
||||
|
||||
let update_fn = {
|
||||
let tracker = Arc::clone(&tracker);
|
||||
Arc::new(move |path: &str| -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
let tracker = Arc::clone(&tracker);
|
||||
let path = path.to_string();
|
||||
Box::pin(async move {
|
||||
tracker.update_path(path).await;
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let done_fn = {
|
||||
let disk_name = disk_name.clone();
|
||||
Arc::new(move || -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
let disk_name = disk_name.clone();
|
||||
Box::pin(async move {
|
||||
global_metrics().current_paths.write().await.remove(&disk_name);
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
(update_fn, done_fn)
|
||||
}
|
||||
|
||||
impl Default for Metrics {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Path tracking helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub type UpdateCurrentPathFn = Arc<dyn Fn(&str) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
|
||||
pub type CloseDiskFn = Arc<dyn Fn() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
|
||||
|
||||
/// Register a new disk in the global path tracker and return two callbacks:
|
||||
/// one to update the current path and one to deregister the disk when done.
|
||||
pub fn current_path_updater(disk: &str, initial: &str) -> (UpdateCurrentPathFn, CloseDiskFn) {
|
||||
let tracker = Arc::new(CurrentPathTracker::new(initial.to_string()));
|
||||
let disk_name = disk.to_string();
|
||||
|
||||
let tracker_clone = Arc::clone(&tracker);
|
||||
let disk_insert = disk_name.clone();
|
||||
tokio::spawn(async move {
|
||||
global_metrics()
|
||||
.current_paths
|
||||
.write()
|
||||
.await
|
||||
.insert(disk_insert, tracker_clone);
|
||||
});
|
||||
|
||||
let update_fn: UpdateCurrentPathFn = {
|
||||
let tracker = Arc::clone(&tracker);
|
||||
Arc::new(move |path: &str| {
|
||||
let tracker = Arc::clone(&tracker);
|
||||
let path = path.to_string();
|
||||
Box::pin(async move { tracker.update_path(path).await })
|
||||
})
|
||||
};
|
||||
|
||||
let done_fn: CloseDiskFn = {
|
||||
let disk = disk_name;
|
||||
Arc::new(move || {
|
||||
let disk = disk.clone();
|
||||
Box::pin(async move {
|
||||
global_metrics().current_paths.write().await.remove(&disk);
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
(update_fn, done_fn)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CloseDiskGuard
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct CloseDiskGuard(CloseDiskFn);
|
||||
|
||||
impl CloseDiskGuard {
|
||||
@@ -697,16 +715,10 @@ impl CloseDiskGuard {
|
||||
|
||||
impl Drop for CloseDiskGuard {
|
||||
fn drop(&mut self) {
|
||||
// Drop cannot be async, so we spawn the async cleanup task
|
||||
// The task will run in the background and complete asynchronously
|
||||
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||
let close_fn = self.0.clone();
|
||||
handle.spawn(async move {
|
||||
close_fn().await;
|
||||
});
|
||||
} else {
|
||||
// If we're not in a tokio runtime context, we can't spawn
|
||||
// This is a best-effort cleanup, so we just skip it
|
||||
handle.spawn(async move { close_fn().await });
|
||||
}
|
||||
// If there is no runtime we are in a test or shutdown path; skip cleanup.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,23 @@ impl GlobalReadiness {
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.status.load(Ordering::SeqCst) == SystemStage::FullReady as u8
|
||||
}
|
||||
|
||||
/// Get the current system stage
|
||||
/// # Returns
|
||||
/// The current SystemStage of the service
|
||||
pub fn current_stage(&self) -> SystemStage {
|
||||
match self.status.load(Ordering::SeqCst) {
|
||||
0 => SystemStage::Booting,
|
||||
1 => SystemStage::StorageReady,
|
||||
2 => SystemStage::IamReady,
|
||||
3 => SystemStage::FullReady,
|
||||
invalid => {
|
||||
debug_assert!(false, "GlobalReadiness::current_stage: invalid status value {}", invalid);
|
||||
// Fallback to the most conservative stage on invalid values
|
||||
SystemStage::Booting
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
[package]
|
||||
name = "rustfs-concurrency"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage.workspace = true
|
||||
description = "Concurrency management for RustFS - timeout, locking, backpressure, and I/O scheduling"
|
||||
keywords = ["rustfs", "concurrency", "timeout", "backpressure", "scheduling"]
|
||||
categories = ["concurrency", "filesystem"]
|
||||
|
||||
[dependencies]
|
||||
# Internal crates
|
||||
rustfs-io-core = { workspace = true }
|
||||
rustfs-io-metrics = { workspace = true }
|
||||
|
||||
# Async runtime
|
||||
tokio = { workspace = true, features = ["sync", "time", "rt"] }
|
||||
tokio-util = { workspace = true }
|
||||
|
||||
# Error handling
|
||||
thiserror = { workspace = true }
|
||||
|
||||
# Logging
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["test-util","macros","rt-multi-thread"] }
|
||||
|
||||
[features]
|
||||
default = ["timeout", "lock", "deadlock", "backpressure", "scheduler"]
|
||||
|
||||
# Feature modules
|
||||
timeout = []
|
||||
lock = []
|
||||
deadlock = []
|
||||
backpressure = []
|
||||
scheduler = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
@@ -0,0 +1,224 @@
|
||||
// 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.
|
||||
|
||||
//! Backpressure management
|
||||
|
||||
use rustfs_io_core::{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 {
|
||||
/// Buffer size in bytes
|
||||
pub buffer_size: usize,
|
||||
/// High watermark percentage
|
||||
pub high_watermark: u32,
|
||||
/// Low watermark percentage
|
||||
pub low_watermark: u32,
|
||||
}
|
||||
|
||||
impl Default for BackpressureConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buffer_size: 4 * 1024 * 1024, // 4MB
|
||||
high_watermark: 80,
|
||||
low_watermark: 50,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BackpressureConfig {
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Calculate low watermark threshold in bytes
|
||||
pub fn low_watermark_bytes(&self) -> usize {
|
||||
(self.buffer_size as u64 * self.low_watermark as u64 / 100) as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// Backpressure manager
|
||||
pub struct BackpressureManager {
|
||||
config: BackpressureConfig,
|
||||
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 {
|
||||
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,
|
||||
};
|
||||
|
||||
Self {
|
||||
config,
|
||||
monitor: Arc::new(CoreBackpressureMonitor::new(core_config)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &BackpressureConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Get the monitor
|
||||
pub fn monitor(&self) -> Arc<CoreBackpressureMonitor> {
|
||||
self.monitor.clone()
|
||||
}
|
||||
|
||||
/// Create a backpressure pipe
|
||||
pub fn create_pipe(&self) -> BackpressurePipe {
|
||||
BackpressurePipe::new(self.config.clone(), self.monitor.clone())
|
||||
}
|
||||
|
||||
/// Get current state
|
||||
pub fn state(&self) -> BackpressureState {
|
||||
self.monitor.state()
|
||||
}
|
||||
|
||||
/// Check if backpressure is active
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.monitor.is_active()
|
||||
}
|
||||
}
|
||||
|
||||
/// Backpressure pipe wrapping tokio's duplex
|
||||
pub struct BackpressurePipe {
|
||||
reader: DuplexStream,
|
||||
writer: DuplexStream,
|
||||
config: BackpressureConfig,
|
||||
monitor: Arc<CoreBackpressureMonitor>,
|
||||
created_at: Instant,
|
||||
}
|
||||
|
||||
impl BackpressurePipe {
|
||||
fn new(config: BackpressureConfig, monitor: Arc<CoreBackpressureMonitor>) -> Self {
|
||||
let (reader, writer) = duplex(config.buffer_size);
|
||||
|
||||
Self {
|
||||
reader,
|
||||
writer,
|
||||
config,
|
||||
monitor,
|
||||
created_at: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the reader end
|
||||
pub fn reader(&mut self) -> &mut DuplexStream {
|
||||
&mut self.reader
|
||||
}
|
||||
|
||||
/// Get the writer end
|
||||
pub fn writer(&mut self) -> &mut DuplexStream {
|
||||
&mut self.writer
|
||||
}
|
||||
|
||||
/// Split into reader and writer
|
||||
pub fn into_split(self) -> (DuplexStream, DuplexStream) {
|
||||
(self.reader, self.writer)
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &BackpressureConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Get current state
|
||||
pub fn state(&self) -> BackpressureState {
|
||||
self.monitor.state()
|
||||
}
|
||||
|
||||
/// Get the age of this pipe
|
||||
pub fn age(&self) -> std::time::Duration {
|
||||
self.created_at.elapsed()
|
||||
}
|
||||
|
||||
/// Check if should apply backpressure
|
||||
pub fn should_apply_backpressure(&self) -> bool {
|
||||
let should = self.monitor.should_apply_backpressure();
|
||||
if should {
|
||||
backpressure_metrics::record_backpressure_activation();
|
||||
}
|
||||
should
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
assert_eq!(config.buffer_size, 4 * 1024 * 1024);
|
||||
assert!(config.high_watermark > config.low_watermark);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backpressure_manager() {
|
||||
let manager = BackpressureManager::new(1024, 80, 50);
|
||||
assert_eq!(manager.state(), BackpressureState::Normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backpressure_pipe() {
|
||||
let manager = BackpressureManager::new(1024, 80, 50);
|
||||
let pipe = manager.create_pipe();
|
||||
assert_eq!(pipe.state(), BackpressureState::Normal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// 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.
|
||||
|
||||
//! Configuration for concurrency management
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
/// Feature flags for concurrency modules
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ConcurrencyFeatures {
|
||||
/// Enable timeout control
|
||||
pub timeout: bool,
|
||||
/// Enable lock optimization
|
||||
pub lock: bool,
|
||||
/// Enable deadlock detection
|
||||
pub deadlock: bool,
|
||||
/// Enable backpressure management
|
||||
pub backpressure: bool,
|
||||
/// Enable I/O scheduling
|
||||
pub scheduler: bool,
|
||||
}
|
||||
|
||||
impl Default for ConcurrencyFeatures {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timeout: cfg!(feature = "timeout"),
|
||||
lock: cfg!(feature = "lock"),
|
||||
deadlock: cfg!(feature = "deadlock"),
|
||||
backpressure: cfg!(feature = "backpressure"),
|
||||
scheduler: cfg!(feature = "scheduler"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConcurrencyFeatures {
|
||||
/// Create with all features enabled
|
||||
pub fn all() -> Self {
|
||||
Self {
|
||||
timeout: true,
|
||||
lock: true,
|
||||
deadlock: true,
|
||||
backpressure: true,
|
||||
scheduler: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with no features enabled
|
||||
pub fn none() -> Self {
|
||||
Self {
|
||||
timeout: false,
|
||||
lock: false,
|
||||
deadlock: false,
|
||||
backpressure: false,
|
||||
scheduler: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if any feature is enabled
|
||||
pub fn any_enabled(&self) -> bool {
|
||||
self.timeout || self.lock || self.deadlock || self.backpressure || self.scheduler
|
||||
}
|
||||
}
|
||||
|
||||
/// Main configuration for concurrency management
|
||||
#[derive(Debug, Clone)]
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConcurrencyConfig {
|
||||
/// Create configuration from environment variables
|
||||
pub fn from_env() -> Self {
|
||||
let mut config = Self::default();
|
||||
|
||||
// Read from environment if available
|
||||
if let Ok(val) = std::env::var("RUSTFS_TIMEOUT_DEFAULT")
|
||||
&& let Ok(secs) = val.parse::<u64>()
|
||||
{
|
||||
config.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);
|
||||
}
|
||||
|
||||
if let Ok(val) = std::env::var("RUSTFS_BACKPRESSURE_BUFFER_SIZE")
|
||||
&& let Ok(size) = val.parse::<usize>()
|
||||
{
|
||||
config.backpressure_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
|
||||
}
|
||||
|
||||
/// Validate configuration
|
||||
pub fn validate(&self) -> Result<(), ConfigError> {
|
||||
if self.default_timeout > self.max_timeout {
|
||||
return Err(ConfigError::InvalidTimeout("default_timeout cannot exceed max_timeout".to_string()));
|
||||
}
|
||||
|
||||
if self.high_watermark <= self.low_watermark || self.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()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration error
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
/// Invalid timeout configuration
|
||||
#[error("Invalid timeout config: {0}")]
|
||||
InvalidTimeout(String),
|
||||
|
||||
/// Invalid backpressure configuration
|
||||
#[error("Invalid backpressure config: {0}")]
|
||||
InvalidBackpressure(String),
|
||||
|
||||
/// Invalid scheduler configuration
|
||||
#[error("Invalid scheduler config: {0}")]
|
||||
InvalidScheduler(String),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let config = ConcurrencyConfig::default();
|
||||
assert!(config.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_timeout() {
|
||||
let config = ConcurrencyConfig {
|
||||
default_timeout: Duration::from_secs(100),
|
||||
max_timeout: Duration::from_secs(50),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
config.validate().is_err(),
|
||||
"validate() should return an error when default_timeout > max_timeout"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_features() {
|
||||
let features = ConcurrencyFeatures::all();
|
||||
assert!(features.any_enabled());
|
||||
|
||||
let features = ConcurrencyFeatures::none();
|
||||
assert!(!features.any_enabled());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// 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.
|
||||
|
||||
//! Deadlock detection management
|
||||
|
||||
use rustfs_io_core::{DeadlockDetector as CoreDeadlockDetector, 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 {
|
||||
/// Enable deadlock detection
|
||||
pub enabled: bool,
|
||||
/// Check interval
|
||||
pub check_interval: Duration,
|
||||
/// Hang threshold
|
||||
pub hang_threshold: Duration,
|
||||
}
|
||||
|
||||
impl Default for DeadlockConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
check_interval: Duration::from_secs(10),
|
||||
hang_threshold: Duration::from_secs(60),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deadlock manager
|
||||
pub struct DeadlockManager {
|
||||
config: DeadlockConfig,
|
||||
detector: Arc<CoreDeadlockDetector>,
|
||||
running: Arc<tokio::sync::Mutex<bool>>,
|
||||
}
|
||||
|
||||
impl DeadlockManager {
|
||||
/// Create a new deadlock manager
|
||||
pub fn new(enabled: bool, check_interval: Duration, hang_threshold: Duration) -> Self {
|
||||
let config = DeadlockConfig {
|
||||
enabled,
|
||||
check_interval,
|
||||
hang_threshold,
|
||||
};
|
||||
|
||||
let core_config = rustfs_io_core::DeadlockDetectorConfig {
|
||||
enabled,
|
||||
detection_interval: check_interval,
|
||||
max_hold_time: hang_threshold,
|
||||
};
|
||||
|
||||
Self {
|
||||
config,
|
||||
detector: Arc::new(CoreDeadlockDetector::new(core_config)),
|
||||
running: Arc::new(tokio::sync::Mutex::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &DeadlockConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Get the core detector
|
||||
pub fn detector(&self) -> Arc<CoreDeadlockDetector> {
|
||||
self.detector.clone()
|
||||
}
|
||||
|
||||
/// Start the deadlock detection background task
|
||||
pub async fn start(&self) {
|
||||
if !self.config.enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut running = self.running.lock().await;
|
||||
if *running {
|
||||
return;
|
||||
}
|
||||
*running = true;
|
||||
drop(running);
|
||||
|
||||
tracing::info!("Deadlock detection started");
|
||||
}
|
||||
|
||||
/// Stop the deadlock detection
|
||||
pub async fn stop(&self) {
|
||||
let mut running = self.running.lock().await;
|
||||
*running = false;
|
||||
|
||||
tracing::info!("Deadlock detection stopped");
|
||||
}
|
||||
|
||||
/// Create a request tracker
|
||||
pub fn track_request(&self, request_id: String, description: String) -> RequestTracker {
|
||||
RequestTracker::new(request_id, description, self.detector.clone())
|
||||
}
|
||||
|
||||
/// Register a lock
|
||||
pub fn register_lock(&self, lock_type: LockType) -> u64 {
|
||||
self.detector.register_lock(lock_type)
|
||||
}
|
||||
|
||||
/// Unregister a lock
|
||||
pub fn unregister_lock(&self, lock_id: u64) {
|
||||
self.detector.unregister_lock(lock_id);
|
||||
}
|
||||
|
||||
/// Detect deadlock
|
||||
pub fn detect_deadlock(&self) -> Option<Vec<u64>> {
|
||||
let result = self.detector.detect_deadlock();
|
||||
if let Some(ref cycle) = result {
|
||||
deadlock_metrics::record_deadlock_detected(cycle.len());
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Request tracker for tracking resources
|
||||
pub struct RequestTracker {
|
||||
request_id: String,
|
||||
description: String,
|
||||
start_time: Instant,
|
||||
resources: HashMap<String, Vec<String>>,
|
||||
detector: Arc<CoreDeadlockDetector>,
|
||||
}
|
||||
|
||||
impl RequestTracker {
|
||||
fn new(request_id: String, description: String, detector: Arc<CoreDeadlockDetector>) -> Self {
|
||||
let start_time = Instant::now();
|
||||
detector.register_request(&request_id, 1); // Use placeholder thread ID
|
||||
|
||||
Self {
|
||||
request_id,
|
||||
description,
|
||||
start_time,
|
||||
resources: HashMap::new(),
|
||||
detector,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the request ID
|
||||
pub fn request_id(&self) -> &str {
|
||||
&self.request_id
|
||||
}
|
||||
|
||||
/// Get the description
|
||||
pub fn description(&self) -> &str {
|
||||
&self.description
|
||||
}
|
||||
|
||||
/// Get the elapsed time
|
||||
pub fn elapsed(&self) -> Duration {
|
||||
self.start_time.elapsed()
|
||||
}
|
||||
|
||||
/// Record a lock acquisition
|
||||
pub fn record_lock_acquire(&mut self, lock_id: u64, resource: String) {
|
||||
self.resources.entry("locks".to_string()).or_default().push(resource);
|
||||
self.detector.record_acquire(lock_id, 1); // Use placeholder thread ID
|
||||
deadlock_metrics::record_lock_acquisition("read");
|
||||
}
|
||||
|
||||
/// Record a lock release
|
||||
pub fn record_lock_release(&mut self, lock_id: u64) {
|
||||
self.detector.record_release(lock_id);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RequestTracker {
|
||||
fn drop(&mut self) {
|
||||
self.detector.unregister_request(&self.request_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deadlock_manager_creation() {
|
||||
let manager = DeadlockManager::new(false, Duration::from_secs(10), Duration::from_secs(60));
|
||||
assert!(!manager.config().enabled);
|
||||
}
|
||||
|
||||
#[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());
|
||||
|
||||
assert_eq!(tracker.request_id(), "req-1");
|
||||
assert_eq!(tracker.description(), "test request");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// 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.
|
||||
|
||||
//! # RustFS Concurrency Management
|
||||
//!
|
||||
//! This crate provides comprehensive concurrency management for RustFS,
|
||||
//! including timeout control, lock optimization, deadlock detection,
|
||||
//! backpressure management, and I/O scheduling.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! All features are controlled by feature flags and can be enabled/disabled at compile time:
|
||||
//!
|
||||
//! - **timeout**: Dynamic timeout calculation based on data size and transfer rate
|
||||
//! - **lock**: Early lock release to reduce contention
|
||||
//! - **deadlock**: Request tracking and cycle detection
|
||||
//! - **backpressure**: Buffer-based flow control
|
||||
//! - **scheduler**: Adaptive buffer sizing and priority queuing
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! ```text
|
||||
//! rustfs-concurrency (Business Layer)
|
||||
//! ├── timeout (Timeout Control)
|
||||
//! ├── lock (Lock Optimization)
|
||||
//! ├── deadlock (Deadlock Detection)
|
||||
//! ├── backpressure (Backpressure Management)
|
||||
//! └── scheduler (I/O Scheduling)
|
||||
//! │
|
||||
//! ├── rustfs-io-core (Core Algorithms)
|
||||
//! └── rustfs-io-metrics (Metrics Collection)
|
||||
//! ```
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use rustfs_concurrency::{ConcurrencyConfig, ConcurrencyManager};
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! // Create manager with all features enabled
|
||||
//! let config = ConcurrencyConfig::default();
|
||||
//! let manager = ConcurrencyManager::new(config);
|
||||
//!
|
||||
//! // Start services
|
||||
//! manager.start().await;
|
||||
//!
|
||||
//! // Use timeout control (if enabled)
|
||||
//! if manager.is_timeout_enabled() {
|
||||
//! let timeout_manager = manager.timeout();
|
||||
//! let _ = timeout_manager;
|
||||
//! }
|
||||
//!
|
||||
//! // Use lock optimization (if enabled)
|
||||
//! if manager.is_lock_enabled() {
|
||||
//! let lock_manager = manager.lock();
|
||||
//! let _ = lock_manager;
|
||||
//! }
|
||||
//!
|
||||
//! // Stop services
|
||||
//! manager.stop().await;
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(unsafe_code)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
// Re-export core types from io-core
|
||||
pub use rustfs_io_core::{
|
||||
// Backpressure types
|
||||
BackpressureConfig as CoreBackpressureConfig,
|
||||
BackpressureMonitor as CoreBackpressureMonitor,
|
||||
BackpressureState,
|
||||
|
||||
// Deadlock types
|
||||
DeadlockDetector as CoreDeadlockDetector,
|
||||
IoLoadLevel,
|
||||
IoLoadMetrics,
|
||||
IoPriority,
|
||||
// Scheduler types
|
||||
IoScheduler,
|
||||
IoSchedulingContext,
|
||||
LockInfo,
|
||||
LockOptimizer as CoreLockOptimizer,
|
||||
|
||||
// Lock types
|
||||
LockStats as CoreLockStats,
|
||||
LockType,
|
||||
// Timeout types
|
||||
OperationProgress,
|
||||
TimeoutError,
|
||||
TimeoutStats,
|
||||
WaitGraphEdge,
|
||||
|
||||
calculate_adaptive_timeout,
|
||||
estimate_bytes_per_second,
|
||||
};
|
||||
|
||||
// Module declarations with feature gates
|
||||
#[cfg(feature = "timeout")]
|
||||
mod timeout;
|
||||
|
||||
#[cfg(feature = "lock")]
|
||||
mod lock;
|
||||
|
||||
#[cfg(feature = "deadlock")]
|
||||
mod deadlock;
|
||||
|
||||
#[cfg(feature = "backpressure")]
|
||||
mod backpressure;
|
||||
|
||||
#[cfg(feature = "scheduler")]
|
||||
mod scheduler;
|
||||
|
||||
// Public module exports with feature gates
|
||||
#[cfg(feature = "timeout")]
|
||||
pub use timeout::{TimeoutConfig, TimeoutGuard, TimeoutManager};
|
||||
|
||||
#[cfg(feature = "lock")]
|
||||
pub use lock::{LockConfig, LockManager, LockScopeGuard, OptimizedLockGuard};
|
||||
|
||||
#[cfg(feature = "deadlock")]
|
||||
pub use deadlock::{DeadlockConfig, DeadlockManager, RequestTracker};
|
||||
|
||||
#[cfg(feature = "backpressure")]
|
||||
pub use backpressure::{BackpressureConfig, BackpressureManager, BackpressurePipe};
|
||||
|
||||
#[cfg(feature = "scheduler")]
|
||||
pub use scheduler::{IoStrategy, SchedulerConfig, SchedulerManager};
|
||||
|
||||
// Configuration
|
||||
mod config;
|
||||
pub use config::{ConcurrencyConfig, ConcurrencyFeatures};
|
||||
|
||||
// Manager
|
||||
mod manager;
|
||||
pub use manager::{ConcurrencyManager, GetObjectQueueSnapshot};
|
||||
|
||||
// Prelude for convenient imports
|
||||
pub mod prelude {
|
||||
//! Prelude module for convenient imports
|
||||
|
||||
#[cfg(feature = "timeout")]
|
||||
pub use crate::timeout::{TimeoutConfig, TimeoutGuard, TimeoutManager};
|
||||
|
||||
#[cfg(feature = "lock")]
|
||||
pub use crate::lock::{LockConfig, LockManager, LockScopeGuard, OptimizedLockGuard};
|
||||
|
||||
#[cfg(feature = "deadlock")]
|
||||
pub use crate::deadlock::{DeadlockConfig, DeadlockManager, RequestTracker};
|
||||
|
||||
#[cfg(feature = "backpressure")]
|
||||
pub use crate::backpressure::{BackpressureConfig, BackpressureManager, BackpressurePipe};
|
||||
|
||||
#[cfg(feature = "scheduler")]
|
||||
pub use crate::scheduler::{IoStrategy, SchedulerConfig, SchedulerManager};
|
||||
|
||||
pub use crate::{ConcurrencyConfig, ConcurrencyFeatures, ConcurrencyManager};
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// 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.
|
||||
|
||||
//! Lock optimization management
|
||||
|
||||
use rustfs_io_core::{LockOptimizer as CoreLockOptimizer, LockStats};
|
||||
use rustfs_io_metrics::lock_metrics;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Lock configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LockConfig {
|
||||
/// Enable lock optimization
|
||||
pub enabled: bool,
|
||||
/// Lock acquisition timeout
|
||||
pub acquire_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for LockConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
acquire_timeout: Duration::from_secs(5),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock manager
|
||||
pub struct LockManager {
|
||||
config: LockConfig,
|
||||
optimizer: Arc<CoreLockOptimizer>,
|
||||
}
|
||||
|
||||
impl LockManager {
|
||||
/// Create a new lock manager
|
||||
pub fn new(enabled: bool, acquire_timeout: Duration) -> Self {
|
||||
let config = LockConfig {
|
||||
enabled,
|
||||
acquire_timeout,
|
||||
};
|
||||
|
||||
let core_config = rustfs_io_core::LockOptimizeConfig {
|
||||
enabled,
|
||||
acquire_timeout,
|
||||
max_hold_time_warning: Duration::from_millis(100),
|
||||
adaptive_spin: true,
|
||||
max_spin_iterations: 1000,
|
||||
};
|
||||
|
||||
Self {
|
||||
config,
|
||||
optimizer: Arc::new(CoreLockOptimizer::new(core_config)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &LockConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Get the core optimizer
|
||||
pub fn optimizer(&self) -> Arc<CoreLockOptimizer> {
|
||||
self.optimizer.clone()
|
||||
}
|
||||
|
||||
/// Get lock statistics
|
||||
pub fn stats(&self) -> &LockStats {
|
||||
self.optimizer.stats()
|
||||
}
|
||||
|
||||
/// Optimize a lock guard
|
||||
pub fn optimize<G>(&self, guard: G, resource: impl Into<String>) -> OptimizedLockGuard<G> {
|
||||
OptimizedLockGuard::new(guard, resource, self.optimizer.clone())
|
||||
}
|
||||
|
||||
/// Check if optimization is enabled
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.config.enabled
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimized lock guard with early release support
|
||||
pub struct OptimizedLockGuard<G> {
|
||||
guard: Option<G>,
|
||||
acquire_time: Instant,
|
||||
released: bool,
|
||||
resource: String,
|
||||
optimizer: Arc<CoreLockOptimizer>,
|
||||
}
|
||||
|
||||
impl<G> OptimizedLockGuard<G> {
|
||||
fn new(guard: G, resource: impl Into<String>, optimizer: Arc<CoreLockOptimizer>) -> Self {
|
||||
optimizer.on_acquire();
|
||||
lock_metrics::record_lock_optimization_enabled(optimizer.config().enabled);
|
||||
|
||||
Self {
|
||||
guard: Some(guard),
|
||||
acquire_time: Instant::now(),
|
||||
released: false,
|
||||
resource: resource.into(),
|
||||
optimizer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the lock hold time
|
||||
pub fn hold_time(&self) -> Duration {
|
||||
self.acquire_time.elapsed()
|
||||
}
|
||||
|
||||
/// Check if the lock has been released
|
||||
pub fn is_released(&self) -> bool {
|
||||
self.released
|
||||
}
|
||||
|
||||
/// Release the lock early
|
||||
pub fn early_release(&mut self) {
|
||||
if self.released {
|
||||
return;
|
||||
}
|
||||
|
||||
let hold_time = self.hold_time();
|
||||
self.guard.take();
|
||||
self.released = true;
|
||||
|
||||
self.optimizer.on_release(hold_time);
|
||||
lock_metrics::record_lock_hold_time(hold_time);
|
||||
|
||||
tracing::debug!(
|
||||
resource = %self.resource,
|
||||
hold_time_ms = hold_time.as_millis(),
|
||||
"Lock released early (optimization active)"
|
||||
);
|
||||
}
|
||||
|
||||
/// Get a reference to the underlying guard
|
||||
pub fn as_ref(&self) -> Option<&G> {
|
||||
if self.released { None } else { self.guard.as_ref() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<G> Drop for OptimizedLockGuard<G> {
|
||||
fn drop(&mut self) {
|
||||
if !self.released {
|
||||
let hold_time = self.hold_time();
|
||||
self.guard.take();
|
||||
self.released = true;
|
||||
|
||||
self.optimizer.on_release(hold_time);
|
||||
lock_metrics::record_lock_hold_time(hold_time);
|
||||
|
||||
tracing::debug!(
|
||||
resource = %self.resource,
|
||||
hold_time_ms = hold_time.as_millis(),
|
||||
"Lock released on drop (normal release)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock scope guard for RAII semantics
|
||||
pub struct LockScopeGuard<G> {
|
||||
guard: Option<G>,
|
||||
}
|
||||
|
||||
impl<G> LockScopeGuard<G> {
|
||||
/// Create a new scope guard
|
||||
pub fn new(guard: G) -> Self {
|
||||
Self { guard: Some(guard) }
|
||||
}
|
||||
|
||||
/// Get a reference to the guard
|
||||
pub fn as_ref(&self) -> Option<&G> {
|
||||
self.guard.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<G> Drop for LockScopeGuard<G> {
|
||||
fn drop(&mut self) {
|
||||
self.guard.take();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[test]
|
||||
fn test_lock_manager_creation() {
|
||||
let manager = LockManager::new(true, Duration::from_secs(5));
|
||||
assert!(manager.is_enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optimized_lock_guard() {
|
||||
let manager = LockManager::new(true, Duration::from_secs(5));
|
||||
let mutex = Mutex::new(42);
|
||||
let guard = mutex.lock().unwrap();
|
||||
|
||||
let optimized = manager.optimize(guard, "test_resource");
|
||||
assert!(!optimized.is_released());
|
||||
|
||||
let mut optimized = optimized;
|
||||
optimized.early_release();
|
||||
assert!(optimized.is_released());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
// 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.
|
||||
|
||||
//! Main concurrency manager
|
||||
|
||||
use crate::config::ConcurrencyConfig;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Snapshot of disk permit queue usage for GetObject orchestration.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct GetObjectQueueSnapshot {
|
||||
/// Total permits configured for disk reads.
|
||||
pub total_permits: usize,
|
||||
/// Permits currently in use.
|
||||
pub permits_in_use: usize,
|
||||
}
|
||||
|
||||
impl GetObjectQueueSnapshot {
|
||||
/// Create a queue snapshot from total and available permits.
|
||||
pub fn from_available_permits(total_permits: usize, available_permits: usize) -> Self {
|
||||
Self {
|
||||
total_permits,
|
||||
permits_in_use: total_permits.saturating_sub(available_permits),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return currently available permits.
|
||||
pub fn permits_available(&self) -> usize {
|
||||
self.total_permits.saturating_sub(self.permits_in_use)
|
||||
}
|
||||
|
||||
/// Return queue utilization percentage in the 0-100 range.
|
||||
pub fn utilization_percent(&self) -> f64 {
|
||||
if self.total_permits == 0 {
|
||||
0.0
|
||||
} else {
|
||||
(self.permits_in_use as f64 / self.total_permits as f64) * 100.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Return whether the queue is considered congested.
|
||||
pub fn is_congested(&self, threshold_percent: f64) -> bool {
|
||||
self.utilization_percent() > threshold_percent
|
||||
}
|
||||
}
|
||||
|
||||
/// Main concurrency manager that provides access to all concurrency features
|
||||
pub struct ConcurrencyManager {
|
||||
config: ConcurrencyConfig,
|
||||
|
||||
#[cfg(feature = "timeout")]
|
||||
timeout: Arc<crate::timeout::TimeoutManager>,
|
||||
|
||||
#[cfg(feature = "lock")]
|
||||
lock: Arc<crate::lock::LockManager>,
|
||||
|
||||
#[cfg(feature = "deadlock")]
|
||||
deadlock: Arc<crate::deadlock::DeadlockManager>,
|
||||
|
||||
#[cfg(feature = "backpressure")]
|
||||
backpressure: Arc<crate::backpressure::BackpressureManager>,
|
||||
|
||||
#[cfg(feature = "scheduler")]
|
||||
scheduler: Arc<crate::scheduler::SchedulerManager>,
|
||||
}
|
||||
|
||||
impl ConcurrencyManager {
|
||||
/// Create a new concurrency manager with the given configuration
|
||||
pub fn new(config: ConcurrencyConfig) -> Self {
|
||||
// Validate configuration
|
||||
if let Err(e) = config.validate() {
|
||||
panic!("Invalid concurrency configuration: {}", e);
|
||||
}
|
||||
|
||||
Self {
|
||||
#[cfg(feature = "timeout")]
|
||||
timeout: Arc::new(crate::timeout::TimeoutManager::new(
|
||||
config.default_timeout,
|
||||
config.max_timeout,
|
||||
config.enable_dynamic_timeout,
|
||||
)),
|
||||
|
||||
#[cfg(feature = "lock")]
|
||||
lock: Arc::new(crate::lock::LockManager::new(
|
||||
config.enable_lock_optimization,
|
||||
config.lock_acquire_timeout,
|
||||
)),
|
||||
|
||||
#[cfg(feature = "deadlock")]
|
||||
deadlock: Arc::new(crate::deadlock::DeadlockManager::new(
|
||||
config.enable_deadlock_detection,
|
||||
config.deadlock_check_interval,
|
||||
config.hang_threshold,
|
||||
)),
|
||||
|
||||
#[cfg(feature = "backpressure")]
|
||||
backpressure: Arc::new(crate::backpressure::BackpressureManager::new(
|
||||
config.backpressure_buffer_size,
|
||||
config.high_watermark,
|
||||
config.low_watermark,
|
||||
)),
|
||||
|
||||
#[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,
|
||||
)),
|
||||
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with default configuration
|
||||
pub fn with_defaults() -> Self {
|
||||
Self::new(ConcurrencyConfig::default())
|
||||
}
|
||||
|
||||
/// Create from environment variables
|
||||
pub fn from_env() -> Self {
|
||||
Self::new(ConcurrencyConfig::from_env())
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &ConcurrencyConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Feature enable checks
|
||||
// ============================================
|
||||
|
||||
/// Check if timeout feature is enabled
|
||||
pub fn is_timeout_enabled(&self) -> bool {
|
||||
#[cfg(feature = "timeout")]
|
||||
{
|
||||
self.config.features.timeout
|
||||
}
|
||||
#[cfg(not(feature = "timeout"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if lock feature is enabled
|
||||
pub fn is_lock_enabled(&self) -> bool {
|
||||
#[cfg(feature = "lock")]
|
||||
{
|
||||
self.config.features.lock
|
||||
}
|
||||
#[cfg(not(feature = "lock"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if deadlock feature is enabled
|
||||
pub fn is_deadlock_enabled(&self) -> bool {
|
||||
#[cfg(feature = "deadlock")]
|
||||
{
|
||||
self.config.features.deadlock
|
||||
}
|
||||
#[cfg(not(feature = "deadlock"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if backpressure feature is enabled
|
||||
pub fn is_backpressure_enabled(&self) -> bool {
|
||||
#[cfg(feature = "backpressure")]
|
||||
{
|
||||
self.config.features.backpressure
|
||||
}
|
||||
#[cfg(not(feature = "backpressure"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if scheduler feature is enabled
|
||||
pub fn is_scheduler_enabled(&self) -> bool {
|
||||
#[cfg(feature = "scheduler")]
|
||||
{
|
||||
self.config.features.scheduler
|
||||
}
|
||||
#[cfg(not(feature = "scheduler"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Feature accessors
|
||||
// ============================================
|
||||
|
||||
/// Get timeout manager
|
||||
#[cfg(feature = "timeout")]
|
||||
pub fn timeout(&self) -> Arc<crate::timeout::TimeoutManager> {
|
||||
self.timeout.clone()
|
||||
}
|
||||
|
||||
/// Get lock manager
|
||||
#[cfg(feature = "lock")]
|
||||
pub fn lock(&self) -> Arc<crate::lock::LockManager> {
|
||||
self.lock.clone()
|
||||
}
|
||||
|
||||
/// Get deadlock manager
|
||||
#[cfg(feature = "deadlock")]
|
||||
pub fn deadlock(&self) -> Arc<crate::deadlock::DeadlockManager> {
|
||||
self.deadlock.clone()
|
||||
}
|
||||
|
||||
/// Get backpressure manager
|
||||
#[cfg(feature = "backpressure")]
|
||||
pub fn backpressure(&self) -> Arc<crate::backpressure::BackpressureManager> {
|
||||
self.backpressure.clone()
|
||||
}
|
||||
|
||||
/// Get scheduler manager
|
||||
#[cfg(feature = "scheduler")]
|
||||
pub fn scheduler(&self) -> Arc<crate::scheduler::SchedulerManager> {
|
||||
self.scheduler.clone()
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Lifecycle management
|
||||
// ============================================
|
||||
|
||||
/// Start all enabled services (e.g., deadlock detection background task)
|
||||
pub async fn start(&self) {
|
||||
#[cfg(feature = "deadlock")]
|
||||
{
|
||||
if self.config.enable_deadlock_detection {
|
||||
self.deadlock.start().await;
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Concurrency manager started (timeout={}, lock={}, deadlock={}, backpressure={}, scheduler={})",
|
||||
self.is_timeout_enabled(),
|
||||
self.is_lock_enabled(),
|
||||
self.is_deadlock_enabled(),
|
||||
self.is_backpressure_enabled(),
|
||||
self.is_scheduler_enabled()
|
||||
);
|
||||
}
|
||||
|
||||
/// Stop all services
|
||||
pub async fn stop(&self) {
|
||||
#[cfg(feature = "deadlock")]
|
||||
{
|
||||
self.deadlock.stop().await;
|
||||
}
|
||||
|
||||
tracing::info!("Concurrency manager stopped");
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ConcurrencyManager {
|
||||
fn default() -> Self {
|
||||
Self::with_defaults()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_queue_snapshot() {
|
||||
let snapshot = GetObjectQueueSnapshot::from_available_permits(64, 16);
|
||||
assert_eq!(snapshot.permits_in_use, 48);
|
||||
assert_eq!(snapshot.permits_available(), 16);
|
||||
assert!(snapshot.is_congested(70.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manager_creation() {
|
||||
let manager = ConcurrencyManager::with_defaults();
|
||||
assert!(manager.config().validate().is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_manager_lifecycle() {
|
||||
let manager = ConcurrencyManager::with_defaults();
|
||||
manager.start().await;
|
||||
manager.stop().await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_feature_checks() {
|
||||
let manager = ConcurrencyManager::with_defaults();
|
||||
|
||||
// These should return the feature flag status
|
||||
let _ = manager.is_timeout_enabled();
|
||||
let _ = manager.is_lock_enabled();
|
||||
let _ = manager.is_deadlock_enabled();
|
||||
let _ = manager.is_backpressure_enabled();
|
||||
let _ = manager.is_scheduler_enabled();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// 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.
|
||||
|
||||
//! I/O scheduler management
|
||||
|
||||
use rustfs_io_core::{
|
||||
IoLoadLevel, IoPriority, IoScheduler as CoreIoScheduler, IoSchedulingContext,
|
||||
io_profile::{AccessPattern, StorageMedia},
|
||||
};
|
||||
use rustfs_io_metrics::io_metrics;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Scheduler configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SchedulerConfig {
|
||||
/// Base buffer size
|
||||
pub base_buffer_size: usize,
|
||||
/// Maximum buffer size
|
||||
pub max_buffer_size: usize,
|
||||
/// High priority threshold
|
||||
pub high_priority_threshold: usize,
|
||||
/// Low priority threshold
|
||||
pub low_priority_threshold: usize,
|
||||
}
|
||||
|
||||
impl Default for SchedulerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scheduler manager
|
||||
pub struct SchedulerManager {
|
||||
config: SchedulerConfig,
|
||||
scheduler: Arc<CoreIoScheduler>,
|
||||
}
|
||||
|
||||
impl SchedulerManager {
|
||||
/// Create a new scheduler manager
|
||||
pub fn new(
|
||||
base_buffer_size: usize,
|
||||
max_buffer_size: usize,
|
||||
high_priority_threshold: usize,
|
||||
low_priority_threshold: usize,
|
||||
) -> Self {
|
||||
let config = SchedulerConfig {
|
||||
base_buffer_size,
|
||||
max_buffer_size,
|
||||
high_priority_threshold,
|
||||
low_priority_threshold,
|
||||
};
|
||||
|
||||
let core_config = rustfs_io_core::IoSchedulerConfig::default();
|
||||
|
||||
Self {
|
||||
config,
|
||||
scheduler: Arc::new(CoreIoScheduler::new(core_config)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &SchedulerConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Get the scheduler
|
||||
pub fn scheduler(&self) -> Arc<CoreIoScheduler> {
|
||||
self.scheduler.clone()
|
||||
}
|
||||
|
||||
/// Create an I/O strategy
|
||||
pub fn create_strategy(&self) -> IoStrategy {
|
||||
IoStrategy::new(self.config.clone(), self.scheduler.clone())
|
||||
}
|
||||
|
||||
/// Calculate buffer size
|
||||
pub fn calculate_buffer_size(
|
||||
&self,
|
||||
file_size: i64,
|
||||
media: StorageMedia,
|
||||
pattern: AccessPattern,
|
||||
load: IoLoadLevel,
|
||||
concurrent: usize,
|
||||
) -> usize {
|
||||
let strategy = self.create_strategy();
|
||||
strategy.calculate_buffer_size(file_size, media, pattern, load, concurrent)
|
||||
}
|
||||
|
||||
/// Get I/O priority
|
||||
pub fn get_priority(&self, size: i64) -> IoPriority {
|
||||
IoPriority::from_size(size, self.config.high_priority_threshold, self.config.low_priority_threshold)
|
||||
}
|
||||
}
|
||||
|
||||
/// I/O strategy
|
||||
pub struct IoStrategy {
|
||||
config: SchedulerConfig,
|
||||
scheduler: Arc<CoreIoScheduler>,
|
||||
}
|
||||
|
||||
impl IoStrategy {
|
||||
fn new(config: SchedulerConfig, scheduler: Arc<CoreIoScheduler>) -> Self {
|
||||
Self { config, scheduler }
|
||||
}
|
||||
|
||||
/// Calculate buffer size with multi-factor strategy
|
||||
pub fn calculate_buffer_size(
|
||||
&self,
|
||||
file_size: i64,
|
||||
media: StorageMedia,
|
||||
pattern: AccessPattern,
|
||||
load: IoLoadLevel,
|
||||
concurrent: usize,
|
||||
) -> usize {
|
||||
// Create scheduling context
|
||||
let _ctx = IoSchedulingContext::new(file_size, self.config.base_buffer_size)
|
||||
.with_sequential(matches!(pattern, AccessPattern::Sequential))
|
||||
.with_media(media);
|
||||
|
||||
// Get base buffer size from core scheduler
|
||||
let permit_wait = Duration::from_millis(10); // Default wait time
|
||||
let is_sequential = matches!(pattern, AccessPattern::Sequential);
|
||||
let core_strategy = self.scheduler.calculate_strategy(file_size, permit_wait, is_sequential);
|
||||
let base_size = core_strategy.buffer_size;
|
||||
|
||||
// Apply multi-factor adjustments
|
||||
let adjusted_size = self.apply_adjustments(base_size, media, pattern, load, concurrent);
|
||||
|
||||
// Record metrics
|
||||
io_metrics::record_io_scheduler_decision(adjusted_size, load.as_str(), pattern.as_str());
|
||||
|
||||
adjusted_size.min(self.config.max_buffer_size)
|
||||
}
|
||||
|
||||
fn apply_adjustments(
|
||||
&self,
|
||||
base_size: usize,
|
||||
media: StorageMedia,
|
||||
pattern: AccessPattern,
|
||||
load: IoLoadLevel,
|
||||
concurrent: usize,
|
||||
) -> usize {
|
||||
let mut size = base_size;
|
||||
|
||||
// Media adjustment
|
||||
size = match media {
|
||||
StorageMedia::Nvme => (size as f64 * 1.5) as usize,
|
||||
StorageMedia::Ssd => (size as f64 * 1.2) as usize,
|
||||
StorageMedia::Hdd => size,
|
||||
_ => size,
|
||||
};
|
||||
|
||||
// Pattern adjustment
|
||||
size = match pattern {
|
||||
AccessPattern::Sequential => (size as f64 * 1.5) as usize,
|
||||
AccessPattern::Random => (size as f64 * 0.5) as usize,
|
||||
_ => size,
|
||||
};
|
||||
|
||||
// Load adjustment
|
||||
size = match load {
|
||||
IoLoadLevel::Low => (size as f64 * 1.2) as usize,
|
||||
IoLoadLevel::Medium => size,
|
||||
IoLoadLevel::High => (size as f64 * 0.7) as usize,
|
||||
IoLoadLevel::Critical => (size as f64 * 0.5) as usize,
|
||||
};
|
||||
|
||||
// Concurrency adjustment
|
||||
if concurrent > 10 {
|
||||
size = (size as f64 * 0.8) as usize;
|
||||
}
|
||||
|
||||
size
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &SchedulerConfig {
|
||||
&self.config
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_scheduler_config() {
|
||||
let config = SchedulerConfig::default();
|
||||
assert!(config.base_buffer_size < config.max_buffer_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scheduler_manager() {
|
||||
let manager = SchedulerManager::new(1024, 4096, 512, 2048);
|
||||
let priority = manager.get_priority(100);
|
||||
assert!(priority.is_high());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_io_strategy() {
|
||||
let manager = SchedulerManager::new(1024, 4096, 512, 2048);
|
||||
let strategy = manager.create_strategy();
|
||||
|
||||
let size = strategy.calculate_buffer_size(1024 * 1024, StorageMedia::Ssd, AccessPattern::Sequential, IoLoadLevel::Low, 1);
|
||||
|
||||
assert!(size > 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// 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.
|
||||
|
||||
//! Timeout management for operations
|
||||
|
||||
use rustfs_io_core::{TimeoutError, calculate_adaptive_timeout};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
/// Timeout configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TimeoutConfig {
|
||||
/// Default timeout duration
|
||||
pub default_timeout: Duration,
|
||||
/// Maximum timeout duration
|
||||
pub max_timeout: Duration,
|
||||
/// Enable dynamic timeout calculation
|
||||
pub enable_dynamic: bool,
|
||||
}
|
||||
|
||||
impl Default for TimeoutConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_timeout: Duration::from_secs(30),
|
||||
max_timeout: Duration::from_secs(300),
|
||||
enable_dynamic: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Timeout manager
|
||||
pub struct TimeoutManager {
|
||||
config: TimeoutConfig,
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &TimeoutConfig {
|
||||
&self.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)
|
||||
}
|
||||
|
||||
/// Wrap an operation with timeout control
|
||||
pub async fn wrap_operation<F, T, E>(&self, operation: F, timeout: Option<Duration>) -> Result<T, TimeoutError>
|
||||
where
|
||||
F: std::future::Future<Output = Result<T, E>>,
|
||||
E: Into<TimeoutError>,
|
||||
{
|
||||
let timeout = timeout.unwrap_or(self.config.default_timeout);
|
||||
|
||||
match tokio::time::timeout(timeout, operation).await {
|
||||
Ok(Ok(result)) => Ok(result),
|
||||
Ok(Err(e)) => Err(e.into()),
|
||||
Err(_) => Err(TimeoutError::TimedOut(timeout)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
}
|
||||
|
||||
/// Timeout guard for manual timeout control
|
||||
pub struct TimeoutGuard {
|
||||
timeout: Duration,
|
||||
start: Instant,
|
||||
cancel_token: CancellationToken,
|
||||
}
|
||||
|
||||
impl TimeoutGuard {
|
||||
fn new(timeout: Duration) -> Self {
|
||||
Self {
|
||||
timeout,
|
||||
start: Instant::now(),
|
||||
cancel_token: CancellationToken::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if timeout has elapsed
|
||||
pub fn is_timed_out(&self) -> bool {
|
||||
self.start.elapsed() > self.timeout
|
||||
}
|
||||
|
||||
/// Get remaining time
|
||||
pub fn remaining(&self) -> Duration {
|
||||
self.timeout.saturating_sub(self.start.elapsed())
|
||||
}
|
||||
|
||||
/// Get the cancellation token
|
||||
pub fn cancel_token(&self) -> CancellationToken {
|
||||
self.cancel_token.clone()
|
||||
}
|
||||
|
||||
/// Cancel the operation
|
||||
pub fn cancel(&self) {
|
||||
self.cancel_token.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_timeout_config() {
|
||||
let config = TimeoutConfig::default();
|
||||
assert!(config.default_timeout < config.max_timeout);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wrap_operation_success() {
|
||||
let manager = TimeoutManager::new(Duration::from_secs(5), Duration::from_secs(10), true);
|
||||
|
||||
let result = manager.wrap_operation(async { Ok::<_, TimeoutError>(42) }, None).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), 42);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Config Crate Instructions
|
||||
|
||||
Applies to `crates/config/`.
|
||||
|
||||
## Environment Variable Naming
|
||||
|
||||
- Global configuration variables must use flat `RUSTFS_*` names.
|
||||
- Do not introduce module-segmented names such as `RUSTFS_CONFIG_*`.
|
||||
|
||||
Canonical examples:
|
||||
|
||||
- `RUSTFS_REGION`
|
||||
- `RUSTFS_ADDRESS`
|
||||
- `RUSTFS_VOLUMES`
|
||||
- `RUSTFS_LICENSE`
|
||||
- `RUSTFS_SCANNER_ENABLED`
|
||||
- `RUSTFS_HEAL_ENABLED`
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Deprecated aliases must keep warning behavior.
|
||||
- Document aliases in `crates/config/README.md`.
|
||||
- Any alias change should include tests for both canonical and deprecated forms.
|
||||
|
||||
## Source of Truth
|
||||
|
||||
- Constants: `crates/config/src/constants/app.rs`
|
||||
- Naming conventions: `crates/config/README.md#environment-variable-naming-conventions`
|
||||
@@ -37,3 +37,6 @@ constants = ["dep:const-str"]
|
||||
notify = ["dep:const-str", "constants"]
|
||||
observability = ["constants"]
|
||||
opa = ["constants"]
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
@@ -41,6 +41,7 @@ Examples:
|
||||
- `RUSTFS_ADDRESS`
|
||||
- `RUSTFS_VOLUMES`
|
||||
- `RUSTFS_LICENSE`
|
||||
- `RUSTFS_LICENSE_PUBLIC_KEY`
|
||||
|
||||
Current guidance:
|
||||
- Prefer module-specific names only when they are not top-level product configuration.
|
||||
@@ -49,6 +50,38 @@ Current guidance:
|
||||
- Deprecated example:
|
||||
- `RUSTFS_ENABLE_SCANNER` -> `RUSTFS_SCANNER_ENABLED`
|
||||
- `RUSTFS_ENABLE_HEAL` -> `RUSTFS_HEAL_ENABLED`
|
||||
- `RUSTFS_DATA_SCANNER_START_DELAY_SECS` -> `RUSTFS_SCANNER_START_DELAY_SECS`
|
||||
|
||||
## License environment variables
|
||||
|
||||
- `RUSTFS_LICENSE` contains the signed license token.
|
||||
- `RUSTFS_LICENSE_PUBLIC_KEY` contains the RSA public key used to verify signed license tokens.
|
||||
|
||||
## CORS environment variables
|
||||
|
||||
- `RUSTFS_CORS_ALLOWED_ORIGINS` defaults to empty, so the S3 endpoint emits no generic CORS headers unless configured. Set `*` for wildcard origins without credentials, or a comma-separated allow-list for credentialed explicit origins.
|
||||
- `RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS` defaults to `*` for the console service.
|
||||
|
||||
## Scanner environment aliases
|
||||
|
||||
- `RUSTFS_SCANNER_SPEED` (canonical, also accepts `MINIO_SCANNER_SPEED`)
|
||||
- `RUSTFS_SCANNER_CYCLE` (canonical, also accepts `MINIO_SCANNER_CYCLE`)
|
||||
- `RUSTFS_SCANNER_START_DELAY_SECS` (canonical)
|
||||
- `RUSTFS_DATA_SCANNER_START_DELAY_SECS` (deprecated alias for compatibility)
|
||||
- `RUSTFS_SCANNER_IDLE_MODE` (canonical)
|
||||
- `RUSTFS_SCANNER_CACHE_SAVE_TIMEOUT_SECS` (canonical)
|
||||
|
||||
## Drive timeout environment variables
|
||||
|
||||
- `RUSTFS_DRIVE_METADATA_TIMEOUT_SECS`
|
||||
- `RUSTFS_DRIVE_DISK_INFO_TIMEOUT_SECS`
|
||||
- `RUSTFS_DRIVE_LIST_DIR_TIMEOUT_SECS`
|
||||
- `RUSTFS_DRIVE_WALKDIR_TIMEOUT_SECS`
|
||||
- `RUSTFS_DRIVE_WALKDIR_STALL_TIMEOUT_SECS`
|
||||
|
||||
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.
|
||||
|
||||
## 📄 License
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user