Compare commits

...

444 Commits

Author SHA1 Message Date
cxymds 9f07029373 fix: reload bucket metadata after lifecycle updates (#2822)
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-06 13:01:44 +00:00
Henry Guo 4b36667ba1 fix(policy): avoid logging generated access keys (#2826)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-05-06 12:56:27 +00:00
houseme 41ba34a145 fix(rpc): add issue 2815 regression and docker validation (#2828) 2026-05-06 12:22:13 +00:00
Michael Graff 3898d524fe security: same-origin console CORS, fail-closed helm creds, deny.toml, sample-config hardening (#2769)
Signed-off-by: Michael Graff <explorer@flame.org>
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-05-06 07:34:44 +00:00
Duru Can Celasun 718bec7722 feat(helm-chart): support scale to 0 in standalone mode (#2797)
Signed-off-by: Duru Can Celasun <can@dcc.im>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-05-06 06:42:56 +00:00
安正超 50ddec3ffc fix: preserve data on self metadata copy (#2819)
Co-authored-by: cxymds <Cxymds@qq.com>
2026-05-06 06:28:36 +00:00
Henry Guo 7692c0c3bd test(credentials): avoid printing default secret (#2820)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-05-06 05:19:42 +00:00
houseme 3dd0692917 refactor: unify credential env constants and deploy env usage (#2821)
Co-authored-by: Henry Guo <marshawcoco@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-05-06 04:30:29 +00:00
GatewayJ 090d60e00a fix(auth): authorize DeleteObjects per key (#2814) 2026-05-06 00:47:36 +00:00
安正超 c228fabfdb test(admin): cover orphan resync status cleanup (#2816) 2026-05-06 00:47:11 +00:00
安正超 bde569d055 test(getobject): cover buffer threshold edge cases (#2817) 2026-05-06 00:47:02 +00:00
houseme e19b0dd997 fix(notify): improve webhook target diagnostics for env setup (#2810) 2026-05-06 00:28:46 +00:00
houseme 26b3aad069 build(deps): bump the dependencies group with 3 updates (#2812) 2026-05-05 14:05:13 +00:00
安正超 bf893bcb55 fix(admin): clear removed site resync status (#2803) 2026-05-05 13:42:12 +00:00
安正超 60d4598562 test(lock): cover shared waiter abort cleanup (#2811) 2026-05-05 13:41:15 +00:00
houseme 565cbdffed fix(getobject): prevent large-download memory buffering (#2809) 2026-05-05 12:58:26 +00:00
houseme 49b2782d51 fix(lock): make slow-path waiter accounting cancellation-safe (#2805) 2026-05-05 11:19:23 +00:00
houseme 743b87014b fix(notify): validate bucket notification filter rules (#2806) 2026-05-05 10:35:36 +00:00
安正超 3208f930c5 fix(s3): advertise byte ranges on head object (#2802) 2026-05-05 04:29:25 +00:00
安正超 36b3d21c44 test: cover RPC secret trimming fallback (#2796)
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-05 04:26:36 +00:00
cxymds 935bb8a8e2 fix(admin): allow site replication removal with offline peers (#2739)
Co-authored-by: loverustfs <hello@rustfs.com>
2026-05-05 03:54:32 +00:00
安正超 9364ecba67 docs: add security advisory lessons skill (#2801)
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-05 11:59:13 +08:00
Henry Guo 0153710791 fix(server): avoid logging default credential values (#2800)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-05-05 03:44:07 +00:00
Alexander Kharkevich 995e26f5ee fix(rpc): use map-encoded msgpack for all internode RPC responses (#2771)
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-05 03:42:29 +00:00
JaySon e6fdcd1ad6 fix: Fix formatting issue in ARCHITECTURE.md (#2787)
Signed-off-by: JaySon-Huang <tshent@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-05-05 11:33:55 +08:00
安正超 f016673416 test: cover license verifier success path (#2798) 2026-05-05 02:55:56 +00:00
安正超 42ff6b4d80 test(admin): cover legacy profile auth guards (#2799) 2026-05-05 02:55:45 +00:00
安正超 3ced40f221 test(license): cover public key validation (#2793) 2026-05-04 15:12:59 +00:00
dependabot[bot] a830ab2a5d build(deps): bump the dependencies group with 4 updates (#2785)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 10:26:51 +00:00
Joey Shi 52f121ab2a Ensure CreationDate is always present in ListBuckets response (#2783) 2026-05-04 08:20:30 +00:00
安正超 eb8868397e fix: address security review follow-ups (#2781) 2026-05-03 13:53:36 +00:00
安正超 66c38b629d Harden admin and RPC security checks (#2773)
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-03 11:55:09 +00:00
安正超 eb23710d2e fix(security): harden CORS and license handling (#2774) 2026-05-03 11:39:27 +00:00
houseme 4b66155f26 chore(deps): update flake.lock (#2777) 2026-05-03 02:44:53 +00:00
Ramakrishna Chilaka 4978f60254 test(ecstore): cover ranged decode and harden offset bounds (#2758)
Co-authored-by: loverustfs <hello@rustfs.com>
2026-05-02 09:02:50 +00:00
安正超 fe59eb4952 test(admin): cover site identity merge gaps (#2767) 2026-05-02 08:43:47 +00:00
安正超 a455b4377c fix: handle empty multipart list-parts (#2765)
Co-authored-by: loverustfs <hello@rustfs.com>
2026-05-02 02:47:52 +00:00
安正超 08fab34804 test(admin): cover peer identity scheme dedupe (#2764) 2026-05-01 04:11:46 +00:00
安正超 f07fed0c49 test(filemeta): cover no-wait refresh coalescing (#2755) 2026-05-01 02:48:30 +00:00
安正超 dcfbc2612a test(admin): cover site replication scheme normalization (#2757) 2026-05-01 02:48:20 +00:00
安正超 87e1c7aeb6 test(filemeta): cover future cache timestamp refresh (#2762) 2026-05-01 02:48:08 +00:00
安正超 0d7e0a814f test(ci): cover Helm Recreate strategy rendering (#2752) 2026-04-30 12:14:38 +00:00
安正超 ef5ccc232a test(filemeta): cover shared cache reuse (#2754) 2026-04-30 12:14:15 +00:00
GatewayJ c29c8a5a1e fix(filemeta): harden and optimize metacache path (#2724)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-30 07:47:31 +00:00
安正超 403997f2f8 test(ci): cover Helm chart version mapping (#2751) 2026-04-30 06:11:14 +00:00
安正超 e34d75dfdf test(admin): cover site replication absolute URI endpoint (#2745)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-30 06:11:00 +00:00
安正超 132e72ef39 fix(ci): keep Helm chart versions valid for non-beta releases (#2749) 2026-04-30 01:51:12 +00:00
安正超 4f4c759b67 docs: simplify pull request template (#2744) 2026-04-29 20:29:23 +08:00
majinghe e331a26262 feat: helm chart version update (#2738) 2026-04-29 11:14:35 +00:00
安正超 61d2e9fbc3 test(webdav): cover decoded path parsing (#2729)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-29 07:30:40 +00:00
majinghe 946755aa89 feat: helm publish manual trigger support (#2732) 2026-04-29 06:01:22 +00:00
houseme c9622611ed fix(admin): harden site-replication identity handling and local verification for issue 2723 (#2730) 2026-04-29 03:27:55 +00:00
majinghe 7041e628b7 fix: docker image build and helm chart publish error caused by versio… (#2731) 2026-04-29 03:23:47 +00:00
Ramakrishna Chilaka 8e1bd560d8 test(get): reject range with part number (#2725) 2026-04-28 23:15:59 +00:00
majinghe d447da75c1 chore: update version from alpha to beta (#2720)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-28 23:08:10 +00:00
Rafael Peroco 2c9524e2c9 fix(helm): only render rollingUpdate when strategy type is RollingUpdate (#2728) 2026-04-28 23:06:31 +00:00
唐小鸭 e16f1ae639 fix(window): Compatible with Windows Path (#2691)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-28 14:36:59 +00:00
安正超 d79720da1d test(signer): cover header fallback helpers (#2711) 2026-04-28 14:09:56 +00:00
安正超 b747e9817e fix(policy): preserve gateway ListBucket resources (#2710) 2026-04-28 14:09:42 +00:00
giter 90ce72122b fix(webdav): decode URL-encoded filenames in path parsing (#2722)
Signed-off-by: giter <giter@users.noreply.github.com>
2026-04-28 14:06:05 +00:00
houseme c4d5c5c5ec fix(obs): disable profiling export by default and fix Helm env name (#2719) 2026-04-28 11:57:15 +00:00
cxymds c698a0f2b6 fix(policy): allow AssumeRole in system policies (#2718) 2026-04-28 11:01:46 +00:00
houseme 2953558f41 fix(lifecycle): prevent eager date-expiry deletion on config update (#2708) 2026-04-28 10:26:14 +00:00
weisd e0b8c4fd42 fix(storage): avoid faulting local drives on transient timeouts (#2714) 2026-04-28 06:38:31 +00:00
weisd a995ec0315 fix(iam): preserve portable IAM storage and derived auth (#2713) 2026-04-28 05:57:10 +00:00
houseme 946b502527 build(deps): bump the dependencies group with 2 updates (#2709) 2026-04-27 23:53:29 +00:00
GatewayJ 09be06a4d2 fix(ecstore): log walk failures in IAM listing path (#2705)
Co-authored-by: GatewayJ <8352692332qq.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-27 14:13:40 +00:00
安正超 159ddd5bac fix: honor bucket-scoped ListBucket policies with s3:prefix (#2707)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-27 14:13:22 +00:00
houseme a68fe1601f fix(ilm): harden signer failures and guard remote tier delete storms (#2706) 2026-04-27 12:39:02 +00:00
weisd 334184b005 fix(replication): prevent target state loss across buckets (#2704) 2026-04-27 09:27:33 +00:00
GatewayJ cfbd094bc4 fix(iam): propagate cache miss load failures (#2692)
Co-authored-by: GatewayJ <8352692332qq.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-27 09:21:22 +00:00
weisd 468dc3aebd fix(replication): fan out single-bucket rules to all targets (#2701) 2026-04-27 06:51:19 +00:00
dependabot[bot] fd258e877c build(deps): bump the dependencies group with 2 updates (#2694)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-27 02:42:49 +00:00
安正超 4dafb64d58 test(server): cover default module switch source (#2697) 2026-04-27 02:41:38 +00:00
houseme 50d03ef021 perf(memory): add reclaim signals and cache controls (#2689) 2026-04-26 16:42:35 +00:00
houseme 438913b84a feat: add console-managed audit and notify module switches (#2690) 2026-04-26 16:10:41 +00:00
GatewayJ 37a3cbc497 fix(admin): map IAM not found errors to 404 (#2685)
Co-authored-by: GatewayJ <8352692332qq.com>
2026-04-26 14:13:14 +00:00
安正超 a05687b900 test(utils): cover disk-check env alias precedence (#2684) 2026-04-26 07:41:50 +00:00
houseme 561d695237 chore(deps): update flake.lock (#2683) 2026-04-26 00:58:02 +00:00
houseme 59f41eb86a feat(obs): improve metrics coverage and dashboard performance (#2682) 2026-04-25 18:51:29 +00:00
houseme 81854762d4 fix(ecstore): log missing local paths during physical disk independence checks (#2680) 2026-04-25 08:01:01 +00:00
houseme 1e9c75a201 feat(ecstore): enforce local disk topology guardrails and expose device ids (#2679) 2026-04-25 06:02:02 +00:00
安正超 80413e0f8e test(ecstore): cover non-zero offset shard span reads (#2677)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-25 02:31:28 +00:00
houseme b96ccfd110 build(deps): bump the dependencies group with 7 updates (#2676) 2026-04-25 02:15:14 +00:00
Henry Guo c717195de2 fix(admin): harden STS and KMS authorization checks (#2653)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-04-25 01:34:18 +00:00
cxymds 94f64acc87 fix(site-replication): sync IAM and bucket replication (#2671) 2026-04-25 01:33:43 +00:00
唐小鸭 d949d4e794 fix: avoid sending HEAD bodies over TLS HTTP/2 (#2648)
Signed-off-by: 唐小鸭 <tangtang1251@qq.com>
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-04-25 01:33:08 +00:00
houseme 7215761784 feat(trusted-proxies): add switchable simple and legacy modes (#2674)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-04-25 01:25:32 +00:00
安正超 f833cd9cbe fix(metrics): keep S3 op counts when chains are disabled (#2675)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-25 01:24:53 +00:00
houseme 13b4500212 feat(obs): improve telemetry stack, replication metrics, and Grafana alignment (#2672)
Co-authored-by: Filipe Monteiro <a22407332@alunos.ulht.pt>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: weisd <im@weisd.in>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-04-24 13:50:17 +00:00
Henry Guo 2705e3f53b fix(audit): remove unwrap from target shutdown path (#2668)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 09:25:11 +00:00
houseme 92f812fc80 feat(rustfs): gate audit/notify by global env switches (#2669) 2026-04-24 09:14:19 +00:00
Ramakrishna Chilaka fefb308b35 feat(policy): implement BinaryEquals condition evaluation (#2626)
Signed-off-by: Ramakrishna Chilaka <49393831+RamakrishnaChilaka@users.noreply.github.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-24 07:08:11 +00:00
cxymds 8d4caeacad fix(oidc): add federated logout flow (#2667)
Co-authored-by: GatewayJ <835269233@qq.com>
2026-04-24 06:51:31 +00:00
安正超 2860c82e3c fix(console): block disabled health probes from SPA fallback (#2665) 2026-04-24 03:34:57 +00:00
houseme 572dd1264e fix(admin): harden health readiness and add health response controls (#2662) 2026-04-23 17:37:42 +00:00
houseme 47247789ad feat(obs): support OTLP headers and timeout overrides for HTTP exporters (#2661) 2026-04-23 17:02:26 +00:00
安正超 39f7de4450 test(admin): cover list service account authorization (#2650)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-23 13:40:59 +00:00
houseme de6fe816c2 build(deps): bump the dependencies group with 6 updates (#2659) 2026-04-23 12:41:37 +00:00
houseme 368ef0f16c refactor(targets): unify endpoint source/merge logic and bump rustfs-kafka-async to v1.2.0 (#2654)
Co-authored-by: Filipe Monteiro <a22407332@alunos.ulht.pt>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: weisd <im@weisd.in>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-04-23 09:14:36 +00:00
houseme bc37cc4001 refactor(logging): unify request-id propagation and fallback metrics (#2652) 2026-04-23 07:11:27 +00:00
GatewayJ ecf0db9bb7 fix(admin): enforce owner check for service account update (#2646)
Co-authored-by: GatewayJ <8352692332qq.com>
2026-04-23 01:54:43 +00:00
loverustfs fa1554be7f fix(admin): authorize cross-user ListServiceAccount with ListServiceAccount (#2640) 2026-04-22 12:16:09 +00:00
weisd f08b592c6f fix(storage): list prefix children behind marker objects (#2643) 2026-04-22 11:23:27 +00:00
安正超 8add0126f5 test(targets): cover target config redaction (#2638) 2026-04-22 02:51:41 +00:00
weisd 09a83a8f56 fix: prevent object lock retention race (#2634) 2026-04-22 02:49:38 +00:00
weisd a0f1bb4ff0 fix(lock): prevent stale distributed object locks (#2633) 2026-04-22 02:12:33 +00:00
houseme 3ac1d2ab0b fix(security): redact target debug logs and remove eval-based bench hook (#2637) 2026-04-21 21:21:01 +00:00
唐小鸭 4aafb07173 refactor: update binary field types and conversions in RPC and protofiles (#2619)
Signed-off-by: 唐小鸭 <tangtang1251@qq.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-04-21 14:49:09 +00:00
Samuel Cormier-Iijima 8c76e9838b fix(s3): return 304 Not Modified instead of dropping the connection (#2627)
Signed-off-by: Samuel Cormier-Iijima <samuel@cormier-iijima.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-21 14:28:03 +00:00
weisd d4dcb2ac9d fix(scanner): avoid stalls after abandoned child listings (#2632) 2026-04-21 09:06:26 +00:00
majinghe 41d2812861 feat: add support for external/existing certificate issuer (#2631) 2026-04-21 07:21:43 +00:00
houseme 960c13a34b feat(storage): wire capacity/object perf tuning and add batch benchmark runners (#2628) 2026-04-21 07:20:57 +00:00
Andy Teijelo Pérez 989827e3b5 feat: add OTHER_AUDIENCES config (#2605)
Co-authored-by: GatewayJ <835269233@qq.com>
2026-04-21 03:48:13 +00:00
likewu a77be8f89b fix: some expect error (#2622)
Signed-off-by: likewu <likewu@126.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-21 03:27:50 +00:00
安正超 1525143a04 test: cover NATS and Pulsar config validation (#2623) 2026-04-21 01:59:05 +00:00
houseme b7a945e453 fix(madmin): restore server_info msgpack compatibility across mixed nodes (#2621) 2026-04-21 01:09:25 +00:00
houseme 3796b684f0 feat(targets): add NATS and Pulsar target support (#2618) 2026-04-21 00:28:19 +00:00
cxymds 1511f9eacb fix(lifecycle): correct delete replication fanout (#2609)
Co-authored-by: loverustfs <hello@rustfs.com>
2026-04-20 10:02:59 +00:00
安正超 12f355a3bc test: cover S3 error XML body compatibility (#2606) 2026-04-20 04:27:22 +00:00
安正超 83bac39417 test(s3): promote passing compatibility cases (#2600) 2026-04-19 14:42:56 +00:00
安正超 177fe2ab44 fix(replication): clean targets when deleting config (#2599) 2026-04-19 11:33:01 +00:00
安正超 1d1f00470d test(s3): cover signed equals object reads (#2598) 2026-04-19 02:11:16 +00:00
安正超 457f4e0170 fix(s3): improve GitLab registry compatibility (#2596)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-19 02:10:42 +00:00
安正超 93d0606cbd fix(admin): accept trailing slash listen notification paths (#2595)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-19 02:10:30 +00:00
houseme ae7444ebb4 chore(deps): update flake.lock (#2597) 2026-04-19 00:52:49 +00:00
安正超 b8c788ffca fix: handle raw URI paths in SigV4 (#2593)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-18 17:09:00 +00:00
houseme 9677320f23 fix(scanner): stabilize data usage cache persistence under slow metadata I/O (#2594) 2026-04-18 16:36:28 +00:00
houseme 116db4f5d9 refactor(metrics): unify process sampling and split network IO (#2590) 2026-04-18 15:30:44 +00:00
安正超 f9b5ad17a9 fix(s3): ignore empty conditional ETag headers (#2592)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-18 15:27:46 +00:00
唐小鸭 fb0d096d5d fix(sse). Resolving Nonce Overwriting Issues in Multi-Package Scenarios (#2582)
Signed-off-by: 唐小鸭 <tangtang1251@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-04-18 14:00:14 +00:00
GatewayJ a5de275875 fix(oidc): prefer username for session identity (#2588)
Co-authored-by: GatewayJ <8352692332qq.com>
2026-04-18 11:13:45 +00:00
GatewayJ dc5ce7d0af test(scanner): avoid flaky noncurrent version counting (#2589)
Co-authored-by: GatewayJ <8352692332qq.com>
2026-04-18 11:13:35 +00:00
安正超 ac443a90ce test(s3): reclassify passing compatibility cases (#2586)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-18 09:15:29 +00:00
安正超 2d0b3227c5 docs: update agent contribution guidelines (#2585)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-18 16:28:07 +08:00
houseme 1cbf156559 refactor(obs): migrate metrics runtime/schema and tighten migration guards (#2584) 2026-04-18 07:51:15 +00:00
GatewayJ 03f8270a60 fix(admin): restore access key listing and guard boot-time uptime (#2580)
Co-authored-by: GatewayJ <8352692332qq.com>
2026-04-17 18:55:38 +00:00
houseme 96fb06f48e refactor(metrics): separate console stream from exports (#2583) 2026-04-17 18:21:08 +00:00
houseme ffcf18f5f3 fix(cache): wire trusted-proxy cache and remove stale cache traces (#2581) 2026-04-17 16:57:21 +00:00
LeonWang0735 38eb0781cf fix(iam): decouple IAM config encryption from root secret rotation (#2558)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-17 14:13:53 +00:00
安正超 ce291ab610 test(utils): cover scanner env alias mapping (#2574)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-17 13:24:16 +00:00
houseme f77ccd5b23 chore: move mcp crate to standalone repository (#2578) 2026-04-17 12:42:26 +00:00
weisd 8dc5ef6ef5 fix(storage): avoid startup UUID disk lookup misses (#2576) 2026-04-17 07:55:47 +00:00
GatewayJ f255b8a9f1 fix(admin): align accountinfo policy with IAM prepare_auth for OIDC console (#2568)
Co-authored-by: GatewayJ <8352692332qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-17 05:38:18 +00:00
houseme 478720d2ee Centralize lifecycle state updates and fix systemd running status (#2567) 2026-04-17 03:20:11 +00:00
majinghe 6b4172998b fix(helm): disable kms default (#2566)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-16 11:51:44 +00:00
houseme 6ce24f3b63 feat(lifecycle): improve ILM compatibility and scanner runtime config (#2534)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-04-16 10:28:03 +00:00
majinghe af93d2daba fix: update mtls configuration for standalone and distributed mode (#2565) 2026-04-16 09:26:36 +00:00
houseme 28edfd6190 fix(storage): harden offline drive fail-fast paths (#2564)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-04-16 09:21:45 +00:00
Tunglies 579b124726 lint: clippy rules or_fun_call (#2561)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-16 02:48:39 +00:00
majinghe 1ffe23e10f fix: update base image to fix security issue caused by alpine (#2563)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-16 02:44:51 +00:00
majinghe 4615791193 add kms environment variables support in helm chart (#2552)
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-16 02:43:54 +00:00
安正超 b2b92de26c test(ecstore): cover fresh tmp cleanup path (#2562) 2026-04-16 02:12:16 +00:00
Tunglies e05d07494e chore(lint): clippy rules large_futures (#2555)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-16 02:10:13 +00:00
weisd c0d3f53f7a test(filemeta): flush xl meta test fixture before reopen (#2557) 2026-04-16 00:40:46 +00:00
Tunglies 49366ee200 chore(lint): clippy rules redundant_clone (#2554) 2026-04-15 13:54:07 +00:00
Strangerxxx 73e6542eea feat(helm): add generic service and ingress annotation support (#2541)
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-15 10:16:10 +00:00
Tunglies f57dd5a3c7 chore(lint): clippy rules needless_collect (#2522) 2026-04-15 10:00:03 +00:00
安正超 1bda5ed636 test: cover stored-size range handling (#2544)
Co-authored-by: cxymds <Cxymds@qq.com>
2026-04-15 09:37:01 +00:00
weisd 27971f5c03 fix(scanner): preserve totals for compacted folder scans (#2530) 2026-04-15 07:32:13 +00:00
安正超 6506ae8c7b fix(ci): report CLA check for GitHub Merge Queue (#2551) 2026-04-15 17:20:16 +08:00
安正超 8223cda5ff fix(ci): enable merge_group trigger for GitHub Merge Queue (#2549) 2026-04-15 16:27:53 +08:00
majinghe 8152c8e084 fix: add different annotations for different pvc (#2547) 2026-04-15 15:21:38 +08:00
weisd da9dfb51e7 fix(ecstore): cleanup stale put object temp data (#2529) 2026-04-15 14:40:58 +08:00
cxymds c11efce1e4 fix(admin): replicate empty site groups (#2528)
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: GatewayJ <835269233@qq.com>
2026-04-15 14:21:40 +08:00
houseme fa793237fb build(deps): bump the dependencies group with 7 updates (#2546)
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-04-15 13:23:57 +08:00
安正超 df109680ae cleanup: remove orphaned chunk fast-path submodule files (#2545)
Co-authored-by: cxymds <Cxymds@qq.com>
2026-04-15 11:50:32 +08:00
cxymds 1eef75b0ca fix(admin): allow clearing group policy (#2527)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: GatewayJ <835269233@qq.com>
2026-04-15 11:35:19 +08:00
安正超 642d83f0e4 revert: remove #2351 chunk I/O and object-io crate (phase 7) (#2543)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-15 10:54:41 +08:00
GatewayJ 16b9189e9b feat(oidc): add roles_claim and jwt:roles policy support (#2509)
Co-authored-by: GatewayJ <8352692332qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-04-15 09:30:24 +08:00
安正超 1979fc7fb1 fix: restore bitrot stream and checksum metadata (#2542) 2026-04-15 08:36:55 +08:00
安正超 68d3dba9fc fix: revert standalone #2351 artifacts (phase 5) (#2535) 2026-04-14 22:35:05 +08:00
安正超 db8ef63674 fix: revert #2351 readers.rs behavioral changes (phase 6) (#2536) 2026-04-14 22:08:38 +08:00
安正超 6d476ae9a1 revert: remove BlockReadable trait and chunk-based I/O from rio, ecstore, disk layers (#2533) 2026-04-14 19:09:02 +08:00
安正超 2a3d127a86 revert: restore PutObjReader and remove PUT zero-copy chunk path (#2351 phase 2) (#2532) 2026-04-14 16:35:03 +08:00
安正超 8abf683178 fix(io-metrics): remove dead GET chunk fast path metrics (#2531) 2026-04-14 15:47:57 +08:00
安正超 5048ff8c69 test(credentials): cover URL-safe secret keys (#2524) 2026-04-14 13:57:23 +08:00
Liam Beckman 711aab73c3 docs: Minor formatting clean up in README.md (#2523)
Signed-off-by: Liam Beckman <lbeckman314@gmail.com>
2026-04-14 08:29:33 +08:00
安正超 315e8134eb test(get): cover reader-backed output context (#2516)
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-13 21:59:13 +08:00
houseme 979626c370 refactor(utils): decouple config deps and move sys helpers (#2520)
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-04-13 21:05:03 +08:00
Cocoon-Break 505a566c7c fix: remove dead replace() call in gen_secret_key (credentials.rs) (#2515)
Signed-off-by: cocoon <54054995+kuishou68@users.noreply.github.com>
Signed-off-by: Cocoon-Break <54054995+kuishou68@users.noreply.github.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-13 18:42:50 +08:00
安正超 8efa359a1a docs: add ARCHITECTURE.md for project-wide code navigation (#2519) 2026-04-13 18:26:58 +08:00
dependabot[bot] 522c1675d8 build(deps): bump the dependencies group with 2 updates (#2513)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 10:02:27 +08:00
houseme e160633ac4 fix(admin): enforce event and audit target auth (#2508) 2026-04-12 23:59:42 +08:00
安正超 458086dc80 fix(get): remove GET chunk fast path (#2507) 2026-04-12 23:12:23 +08:00
安正超 0e697fa7e1 test(get): cover fast path pre-commit errors (#2502)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-12 20:13:15 +08:00
安正超 30cc481731 refactor(storage): inline acl entrypoints (#2501)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-12 20:12:44 +08:00
houseme b9f2369095 build(deps): bump the dependencies group with 3 updates (#2503) 2026-04-12 12:11:40 +08:00
houseme 4ce374a181 chore(deps): update flake.lock (#2498) 2026-04-12 08:50:40 +08:00
安正超 ff7dc77b5e fix(deploy): auto-create rustfs log directory (#2500) 2026-04-12 08:50:24 +08:00
安正超 b387f6c58a refactor(storage): inline object lock config entrypoints (#2494) 2026-04-12 08:49:51 +08:00
houseme 64508ae770 fix(storage): align archive content-encoding with S3 semantics (#2496) 2026-04-12 02:14:52 +08:00
John 6a2f98b468 fix(obs): respect error log level for http warnings (#2492)
Signed-off-by: 80347547 <jianglong@oppo.com>
Co-authored-by: 80347547 <jianglong@oppo.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-12 01:32:54 +08:00
houseme eb0dc24921 fix(get): validate fast path body before response commit (#2495)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-04-12 01:04:22 +08:00
Mohamed Zenadi c8c71e34b6 fix(storage): fix critical bug in range calculation (#2493)
Signed-off-by: Mohamed Zenadi <zeapo@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-12 00:20:15 +08:00
John 11552eb722 fix(deploy): use standard rustfs log directory (#2491)
Signed-off-by: 80347547 <jianglong@oppo.com>
Co-authored-by: 80347547 <jianglong@oppo.com>
2026-04-11 21:48:05 +08:00
安正超 b0b7f56281 refactor(storage): inline object lock metadata writes (#2489) 2026-04-11 20:05:03 +08:00
安正超 120f1021b5 refactor(storage): inline object tagging entrypoints (#2486) 2026-04-11 19:01:24 +08:00
安正超 20e07519a1 refactor(storage): inline object metadata read entrypoints (#2485)
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-11 14:59:38 +08:00
安正超 5c4dadc0b7 test(bucket): cover inline list response params (#2483) 2026-04-11 14:59:17 +08:00
Henry Guo 0f7e7f35e9 test(utils): make retry timer assertions deterministic (#2468) 2026-04-11 12:02:38 +08:00
安正超 5e97377aa0 refactor(storage): drop ecfs test-only helper (#2484) 2026-04-11 11:49:10 +08:00
安正超 b49570d87a refactor(storage): inline extract put-object branch (#2482) 2026-04-11 11:00:11 +08:00
安正超 d70ca1990e refactor(storage): inline list response params (#2476) 2026-04-11 09:25:40 +08:00
Henry Guo 70fdf79297 fix(server): warn on default credentials with console enabled (#2448)
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-11 00:03:06 +08:00
安正超 4472324125 refactor(storage): trim thin s3 forwarding layers (#2474) 2026-04-10 23:17:18 +08:00
houseme b8c45fc9e3 fix(get-object): harden GET fast path against mid-stream regressions (#2472) 2026-04-10 21:38:29 +08:00
weisd a8a2aaa460 fix(ecstore): avoid duplicate keys in ListObjectsV2 (#2467) 2026-04-10 11:29:23 +08:00
Henry Guo 2bf4b14394 docs(readme): fix inconsistent startup instructions (#2465)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:28:01 +08:00
安正超 59b5e4e722 refactor(app): inline remaining tagging outputs (#2461) 2026-04-10 10:27:27 +08:00
安正超 9ec6465b26 refactor(app): inline object request context types (#2462) 2026-04-10 10:27:08 +08:00
安正超 80cb6b3939 test(filemeta): cover legacy nil UUID metadata (#2464) 2026-04-10 10:26:23 +08:00
weisd 30c8bead63 fix(filemeta): accept nil legacy pool metadata (#2459) 2026-04-10 08:27:41 +08:00
安正超 2125cffd0b refactor(app): inline trivial s3 api outputs (#2460) 2026-04-10 08:07:44 +08:00
安正超 aeef2e67f9 refactor(app): inline bucket tagging outputs (#2455) 2026-04-10 07:47:44 +08:00
安正超 8acf2a51b9 refactor(app): reuse put object request context (#2456) 2026-04-10 07:08:27 +08:00
安正超 33b50a5366 refactor(app): inline bucket replication output (#2454) 2026-04-10 07:07:29 +08:00
安正超 acdd2de21f refactor(app): inline bucket encryption output (#2452) 2026-04-10 07:07:14 +08:00
安正超 b8bfaccbc5 refactor(app): inline multipart transition helper (#2451) 2026-04-10 07:06:50 +08:00
安正超 28d9bbbdcb refactor(storage): remove unused s3 api facades (#2450) 2026-04-10 07:06:30 +08:00
安正超 a9507f8ec7 refactor(app): reuse bucket listing helpers (#2449) 2026-04-10 07:06:11 +08:00
安正超 722cab500c refactor(app): reuse multipart uploads helpers (#2447) 2026-04-10 07:05:40 +08:00
安正超 71e9178182 refactor(app): reuse object tagging validation (#2445)
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-09 23:31:02 +08:00
安正超 aee8a4df51 refactor(app): reuse list parts parsing (#2446) 2026-04-09 22:59:56 +08:00
无心戈 57138fa660 docs: Update RustFS console access URL to port 9001 (#2442)
Signed-off-by: 无心戈 <wanxger@egeeke.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-04-09 21:46:31 +08:00
GatewayJ 8db55de72c fix(iam): return policy JSON object from info_policy (#2395) (#2436)
Co-authored-by: GatewayJ <8352692332qq.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-04-09 21:45:22 +08:00
安正超 90e584af74 test(capacity): cover poisoned scope registry recovery (#2441) 2026-04-09 13:29:00 +08:00
安正超 ff77cb5540 refactor(app): simplify object usecase plumbing (#2438) 2026-04-09 12:54:01 +08:00
安正超 61c0da936b fix(capacity): ignore future write buckets (#2440) 2026-04-09 09:24:04 +08:00
houseme d62114f8d5 refactor(nix): modify git author info (#2437) 2026-04-08 22:09:20 +08:00
houseme 9899985d15 build(deps): bump the dependencies group with 4 updates (#2435)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-04-08 21:34:16 +08:00
houseme 064e21062d fix(capacity): harden scope registry, scan symlink guard, and test temp dir cleanup (#2432)
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-04-08 20:58:17 +08:00
安正超 d4ea14c2ba ci: normalize tagged release package filenames (#2425) 2026-04-08 07:25:50 +08:00
安正超 1977d19c29 ci: force Node.js 24 for JS setup actions (#2424) 2026-04-07 23:49:43 +08:00
安正超 d72e7f6f28 Fix remaining review follow-ups for #2361 (#2421)
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: jeadie <jack@spice.ai>
Co-authored-by: Jack Eadie <jack.eadie0@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-04-07 23:24:23 +08:00
houseme 79ffecbf14 refactor(storage): remove object cache plumbing (#2422)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-04-07 22:00:53 +08:00
houseme e3000f16e0 fixed: Update service status to Ready (#2423) 2026-04-07 21:14:24 +08:00
houseme ec8f059506 feat: add timing and metrics for query execution phases (#2419)
Signed-off-by: Shreyan Jain <shreyan11@duck.com>
Co-authored-by: Shreyan Jain <shreyan11@duck.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-04-07 20:32:32 +08:00
overtrue 9cd1d6b4d5 chore: merge upstream PR 2372 fixes 2026-04-07 19:38:59 +08:00
overtrue bf64bcabf8 fix(kms): restore configure and decrypt compatibility 2026-04-07 19:34:09 +08:00
overtrue 1f598105dd Merge branch 'pr-2372' into codex/merge-pr-2372-upstream 2026-04-07 19:24:18 +08:00
安正超 c4b5b00d9c Merge branch 'main' into feat/kms-vault-transit2 2026-04-07 19:16:28 +08:00
weisd 0b99e02891 feat(ecstore): add stale multipart upload cleanup (#2416) 2026-04-07 18:57:07 +08:00
weisd 15c8c7cecf chore: update s3s to 010ae03 (#2418) 2026-04-07 17:21:22 +08:00
houseme c9c2428373 fix(download): preserve archive download integrity (#2415)
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-04-07 16:25:34 +08:00
houseme d6158c0481 feat(mqtt): migrate client and harden TLS config (#2413)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-07 15:28:29 +08:00
weisd 9ed6487e2f fix(scanner): continue scans after traversal errors (#2414) 2026-04-07 14:19:26 +08:00
唐小鸭 72928a43fa Update rustfs/src/admin/handlers/kms_dynamic.rs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: 唐小鸭 <tangtang1251@qq.com>
2026-04-07 13:53:19 +08:00
唐小鸭 55f175b671 Update rustfs/src/admin/handlers/kms_dynamic.rs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: 唐小鸭 <tangtang1251@qq.com>
2026-04-07 13:53:05 +08:00
唐小鸭 3a16c25cd9 Update crates/kms/src/backends/vault_transit.rs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: 唐小鸭 <tangtang1251@qq.com>
2026-04-07 13:52:48 +08:00
majinghe 6963b898ee feat: add support for pvc customized annotations (#2412) 2026-04-07 13:34:49 +08:00
cxymds 7b8d7bdf18 Merge branch 'main' into feat/kms-vault-transit2 2026-04-07 11:11:43 +08:00
majinghe 751bc3d737 fix: move ec configuration from configmap to extraEnv (#2408) 2026-04-07 11:00:21 +08:00
weisd 898857d1c9 fix(iam): keep service account JWT expiry consistent (#2410) 2026-04-07 11:00:07 +08:00
安正超 97b06afd6c fix: enforce multipart authorization checks (#2411) 2026-04-07 10:50:20 +08:00
Alexander Kharkevich 740e4399af fix: skip missing groups in policy_db_get instead of aborting (#2393)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-04-07 09:17:39 +08:00
安正超 832909306e Merge branch 'main' into feat/kms-vault-transit2 2026-04-07 08:36:09 +08:00
houseme 32bf8f5bf3 feat(storage): add direct chunk GET fast path (#2351)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-04-07 08:33:46 +08:00
ankohuu 8d27170ce4 fix: limit pyroscope profiling to supported Unix targets (#2399)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-06 23:05:30 +08:00
安正超 dd68a419e3 test(server): cover request context layer propagation (#2398)
Co-authored-by: loverustfs <hello@rustfs.com>
2026-04-06 20:35:37 +08:00
houseme e69d1dc3e6 build(deps): bump the dependencies group with 14 updates (#2407)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-04-06 17:55:38 +08:00
安正超 97d3fb15fb test(metrics): cover Prometheus descriptor names (#2405) 2026-04-06 10:16:53 +08:00
ankohuu 0e5fc4bec1 fix(metrics): use Prometheus-compatible metric names (#2312) (#2317)
Signed-off-by: Shunchao Hu <ankohuu@gmail.com>
2026-04-05 21:39:10 +08:00
安正超 77229fe426 test(admin): cover audit target validation gaps (#2390) 2026-04-04 14:41:35 +08:00
houseme eabbea46d3 refactor(tracing): unify request-context propagation and fix tracing chain breaks (#2394)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-04-04 12:20:59 +08:00
houseme d2901fd78c feat(admin): add audit target APIs and harden target source handling (#2350)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-04-04 09:07:22 +08:00
安正超 67863630b2 fix(auth): reject ambiguous case-insensitive claim matches (#2386) 2026-04-04 08:36:14 +08:00
安正超 a9be9af094 ci: bump cla-bot to v0.0.9 (#2389) 2026-04-04 08:35:57 +08:00
houseme 372f004a8d refactor(server): unify TLS loading, optimize HTTP transport, add hot reload (#2388) 2026-04-04 06:37:24 +08:00
安正超 0ac3b5b992 docs: remind agents to clean build artifacts (#2387) 2026-04-03 22:51:29 +08:00
weisd 25512e2635 perf(ecstore): batch delete object lock acquisition (#2374)
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-04-03 21:46:51 +08:00
Logan Ye 2d91e2f580 fix(oidc): support case-insensitive claim name matching (#2362)
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-04-03 21:45:56 +08:00
GatewayJ c244943313 feat(iam): retry OIDC discovery with issuer URL slash variants (#2360)
Co-authored-by: GatewayJ <8352692332qq.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-04-03 21:10:27 +08:00
xxkeming b3f31ad694 fix(build): enable tokio_unstable in build script (#2376)
Co-authored-by: houseme <housemecn@gmail.com>
2026-04-03 21:09:30 +08:00
Andy Brown c4efb46827 fix(notify): emit delete webhooks for prefix deletes and align replication headers (#2383)
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-04-03 21:09:05 +08:00
安正超 1fe036cb70 ci: update CLA workflow for corrected comments (#2384) 2026-04-03 20:49:09 +08:00
weisd 5d302febb7 fix(rio): preserve reader capabilities and crypto safety (#2363) 2026-04-03 13:57:42 +08:00
weisd 6a114cd2e0 fix: bump s3s for presigned checksum handling (#2379) 2026-04-03 13:27:56 +08:00
安正超 6696703343 test: cover delete-group percent decoding (#2373) 2026-04-03 11:30:56 +08:00
安正超 aefb99f1da Merge branch 'main' into feat/kms-vault-transit2 2026-04-03 11:26:51 +08:00
安正超 c44309c16a ci: bump cla-bot to v0.0.6 (#2377) 2026-04-03 11:21:14 +08:00
安正超 c3361e38d6 ci: bump cla-bot to v0.0.5 (#2375) 2026-04-03 10:55:40 +08:00
reatang 30a757a9da feat(kms): add vault transit engine 2026-04-03 00:46:00 +08:00
GatewayJ 84f58af628 fix(admin): percent-decode group name in DELETE /v3/group/{group} (#2358)
Co-authored-by: GatewayJ <8352692332qq.com>
2026-04-02 23:52:04 +08:00
安正超 890837aee8 docs: update AGENTS pre-commit policy (#2370)
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-02 22:26:05 +08:00
安正超 c513275741 Update CLA Bot token usage in workflow
Signed-off-by: 安正超 <anzhengchao@gmail.com>
2026-04-02 22:07:40 +08:00
安正超 9d3191e55b Modify CLA workflow permissions and cleanup (#2369)
Signed-off-by: 安正超 <anzhengchao@gmail.com>
2026-04-02 22:02:35 +08:00
安正超 6fba01fb65 ci: use GitHub App tokens for CLA bot (#2368) 2026-04-02 20:42:48 +08:00
安正超 a8af7c9617 ci: integrate CLA bot checks (#2367) 2026-04-02 20:21:02 +08:00
安正超 bd36cf3588 test(filemeta): cover legacy delete marker decoding (#2333)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 22:08:45 +08:00
安正超 8893de1cad test(admin): cover empty kms key aliases (#2331)
Co-authored-by: cxymds <Cxymds@qq.com>
2026-03-31 22:08:21 +08:00
安正超 d3dee898ee test(object): cover zero-copy selection heuristics (#2338)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-03-31 22:07:58 +08:00
cxymds d960acb170 fix(admin): reconcile site replication peer identity (#2356) 2026-03-31 18:12:56 +08:00
weisd e86c6b726f fix(lock): split distributed read and write quorum (#2355) 2026-03-31 16:16:13 +08:00
weisd 15995aae14 feat(admin): complete site replication support (#2346) 2026-03-31 13:01:38 +08:00
lunrenyi 6bf0c542a1 docs: add x-cmd and nix installation options (#2306)
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-30 22:05:46 +08:00
majinghe d56e839f20 feat: add extra env support for helm chart (#2340) 2026-03-30 22:03:36 +08:00
weisd 0fb070e912 feat(s3): support metadata extensions for bucket listings (#2344) 2026-03-30 22:03:13 +08:00
majinghe 172086ff42 fix: change the condition for httproute (#2345)
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-30 22:03:01 +08:00
安正超 0b0c10b323 docs(agents): enforce constant reuse rules (#2348) 2026-03-30 21:50:14 +08:00
安正超 b256154f25 test(admin): cover tier, bucket metadata, and kms aliases (#2334) 2026-03-30 21:48:42 +08:00
安正超 d5f05993a3 test(ecstore): cover read offset overflow (#2341) 2026-03-30 20:48:03 +08:00
安正超 d0ea41e190 test(ecstore): cover inline bitrot offset reads (#2337) 2026-03-30 20:40:26 +08:00
安正超 74b2c70602 test(admin): cover heal alias routes (#2329) 2026-03-30 20:11:54 +08:00
houseme 16db18216d fix: populate tagging notification principalId and object metadata (#2342) 2026-03-30 14:32:39 +08:00
dependabot[bot] 387c385dfa build(deps): bump rustc-hash from 2.1.1 to 2.1.2 in the dependencies group (#2339)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 09:09:27 +08:00
houseme dd9e093dcc perf(capacity): tune default capacity settings, sync docs, and fix refresh/metrics correctness (#2336)
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-30 05:33:56 +08:00
houseme 7172e151de fix: address correctness, safety, and concurrency issues (#2327)
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-30 00:30:57 +08:00
安正超 860a37d3a8 test(admin): cover alias parsing edge cases (#2326) 2026-03-29 20:10:19 +08:00
安正超 3515615e79 test(s3): cover anonymous write-offset rejection (#2320) 2026-03-29 20:06:16 +08:00
安正超 924c4b17a6 test(storage): cover write-offset pre-auth rejection (#2319) 2026-03-29 20:06:04 +08:00
安正超 7fb405526b test(filemeta): cover legacy delete marker fallback (#2322) 2026-03-29 20:05:48 +08:00
安正超 4764b849cb fix(admin): route root heal start through heal_format (#2323) 2026-03-29 20:05:37 +08:00
安正超 0c6bb6add5 test(admin): cover compatible alias routes (#2328) 2026-03-29 20:05:20 +08:00
安正超 3578baf501 test(object-lock): cover validation gaps (#2318) 2026-03-29 19:23:30 +08:00
安正超 939a69f9c2 test(s3): complete snowball auto-extract compatibility (#2324) 2026-03-29 19:19:45 +08:00
GatewayJ 3366bd2464 feat(iam,admin): prepared IAM auth, ExistingObjectTag, admin permission checks (#2315)
Signed-off-by: GatewayJ <835269233@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: GatewayJ <8352692332qq.com>
2026-03-29 19:18:16 +08:00
houseme 263e504c0c refactor(capacity): optimize capacity management module (#2325)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-03-29 17:49:30 +08:00
安正超 f98664ea4a test(s3): complete snowball auto-extract coverage (#2313) 2026-03-28 22:56:42 +08:00
安正超 9536ed8d38 test(storage): cover write-offset rejection (#2316)
Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-28 22:56:03 +08:00
安正超 ef33e43032 feat(admin): align heal compatibility routes (#2311) 2026-03-28 19:57:22 +08:00
安正超 c20f2555eb feat(admin): add MinIO-compatible admin aliases (#2307) 2026-03-28 08:19:56 +08:00
weisd 5e21c398f5 fix(filemeta): support legacy xl.meta compatibility (#2304) 2026-03-27 13:42:06 +08:00
houseme af46a61fde build(deps): bump the dependencies group with 6 updates (#2303)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-27 12:47:48 +08:00
majinghe 14e4d94666 add ec environment variables in helm chart (#2290)
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-27 09:40:30 +08:00
weisd 0779036535 feat(s3): reject write-offset-bytes requests compatibly (#2295) 2026-03-26 15:48:12 +08:00
weisd d637c4d342 fix(object-lock): recover remaining s3 tests (#2294) 2026-03-26 12:11:34 +08:00
安正超 a236b0d01d feat(ecstore): implement decommission and rebalance (#2281)
Co-authored-by: weisd <im@weisd.in>
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-26 11:44:02 +08:00
weisd 59c437d901 feat(object-lock): complete legal hold enforcement (#2293) 2026-03-26 10:58:10 +08:00
houseme 0c42916fa9 ci(build): enable tokio_unstable flag in Build RustFS job (#2289) 2026-03-25 17:03:21 +08:00
weisd 41dcebda44 fix(tier): sweep transitioned copies from delete handlers (#2287) 2026-03-25 16:06:36 +08:00
houseme fb2ced4d27 feat(obs): integrate dial9-tokio-telemetry for runtime tracing (#2285)
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-03-25 14:23:58 +08:00
weisd 2681731443 fix(checksum): align multipart CRC64NVME with full object (#2286) 2026-03-25 12:44:46 +08:00
houseme 19b8389dc4 fix(disk): Fix Usage Report Capacity Calculation (#2274)
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-03-24 23:47:30 +08:00
安正超 8c8d157418 fix(object): always unregister deadlock-tracked get requests (#2275)
Signed-off-by: heihutu <heihutu@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-03-24 19:09:45 +08:00
weisd 28f57b228c feat(s3): advance parity coverage (#2278) 2026-03-24 17:29:33 +08:00
heihutu 8aa59b12cb refactor(auth): Improve UI access token login issue (#2277) 2026-03-24 14:48:37 +08:00
cxymds dad9a7d708 fix(ecstore): honor lifecycle tag filters (#2264)
Co-authored-by: loverustfs <hello@rustfs.com>
2026-03-24 14:25:43 +08:00
cxymds 5ea6d8a7e6 fix(ecstore): preserve transition object metadata (#2263)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-03-24 14:06:29 +08:00
cxymds 75e6902f46 feat(admin): add persisted OIDC config APIs (#2267)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-03-24 12:13:41 +08:00
majinghe 24d359a867 fix: CVE-2026-22184 fix in docker image (#2276)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-03-24 11:36:40 +08:00
Peter Olds ca62b0c163 fix(Helm): Remove duplicate imagePullSecrets block (#2260)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: cxymds <Cxymds@qq.com>
2026-03-24 09:54:33 +08:00
heihutu b7789c8e08 refactor(docker): image grafana/tempo:2.10.3 (#2273) 2026-03-23 23:25:58 +08:00
dependabot[bot] e95eb92612 build(deps): bump zip from 8.3.1 to 8.4.0 in the dependencies group (#2270)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 20:50:17 +08:00
dependabot[bot] 2583d1e49b build(deps): bump the dependencies group with 3 updates (#2259)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: wly <wlywly0735@126.com>
2026-03-23 19:56:09 +08:00
LeonWang0735 990acbcd4b fix(admin): decode compat payload in set-remote-target (#2216)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: 马登山 <Cxymds@qq.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
2026-03-23 18:22:07 +08:00
weisd 05dc131a49 perf(storage): optimize internode RPC transfer path (#2262)
Co-authored-by: momoda693 <momoda693@gmail.com>
2026-03-23 17:09:49 +08:00
cxymds 236142a682 fix(ecstore): repair lifecycle transition and restore flows (#2240)
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: weisd <im@weisd.in>
2026-03-23 12:29:13 +08:00
weisd 2e7abfbd63 fix(ecstore): preserve raw metadata read semantics (#2258)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-03-23 10:42:16 +08:00
houseme 6cb094e30a fix(object): Fix concurrent request hang issue in S3 range read workloads (#2251)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-03-23 09:55:08 +08:00
heihutu ff40e2bc79 build(deps): bump the dependencies group with 3 updates (#2257) 2026-03-22 17:24:42 +08:00
安正超 99dbe70a89 fix(madmin): handle blank service-account expiration (#2254)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-03-22 13:44:05 +08:00
weisd a42320848c fix(ecstore): invalidate xl.meta cache after writes and rename (#2255) 2026-03-22 11:30:29 +08:00
heihutu 8b4e5b2540 chore(deps): update flake.lock (#2253) 2026-03-22 09:00:17 +08:00
GatewayJ 19d3a23a13 fix(admin): console self password for STS sessions (#1923) (#2250)
Co-authored-by: GatewayJ <8352692332qq.com>
2026-03-21 22:10:38 +08:00
weisd 95850c1bcd fix(admin): accept IAM service account exports (#2249)
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-21 13:44:46 +08:00
houseme 628481be7c fix(tracing): Fix distributed tracing context linking (#2247) 2026-03-21 11:07:35 +08:00
houseme 0047bcd3ac build(deps): bump the dependencies group with 4 updates (#2243) 2026-03-20 23:22:57 +08:00
houseme fafbc4fe1d feat(info): add --json flag for JSON output and markdown table format by default (#2245) 2026-03-20 22:53:03 +08:00
马登山 f11c307aec fix(admin): avoid tier stats panic on missing tier (#2238)
Co-authored-by: heihutu <heihutu@gmail.com>
2026-03-20 20:57:33 +08:00
heihutu 3c28f0a0ba feat(metrics): migrate system monitoring from rustfs-obs to rustfs-metrics (#2242)
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-20 18:52:33 +08:00
houseme 28f86a505e feat: Add --info command and refactor config module (#2234)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-20 01:16:45 +08:00
Trent Houliston e1a278aaf8 fix(iam): preserve trailing slash in OIDC issuer URL (#2228) 2026-03-19 13:52:59 +08:00
weisd 35e1f28f23 fix: support legacy bucket metadata decoding (#2227) 2026-03-19 12:43:53 +08:00
weisd c6715259b1 fix(ecstore): keep bucket created time when metadata is epoch (#2220)
Co-authored-by: momoda693 <momoda693@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-19 01:02:34 +08:00
weisd df8f0edaea fix(ecstore): support legacy pool meta decoding (#2218)
Co-authored-by: momoda693 <momoda693@gmail.com>
2026-03-19 00:20:24 +08:00
houseme a4411a24d6 refactor(metrics): modularize collectors and add comprehensive metrics support (#2208)
Co-authored-by: overtrue <anzhengchao@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
2026-03-18 23:52:42 +08:00
houseme 4a807d80e3 build(deps): bump the dependencies group with 2 updates (#2211) 2026-03-18 21:39:01 +08:00
安正超 f095f56e20 fix(ci): revert docker alpha latest tag (#2209) 2026-03-18 21:05:52 +08:00
weisd b9b7d86ae4 feat: improve legacy metadata and admin compatibility (#2202) 2026-03-18 21:05:09 +08:00
马登山 84077adf17 fix(admin): avoid unbounded metrics sampling by default (#2203) 2026-03-18 14:43:17 +08:00
马登山 237c933f38 test(lifecycle): add prefix regression and zero-day checks (#2201) 2026-03-18 13:20:00 +08:00
安正超 c20b3c7f19 fix(ecstore): handle EODM rules without due date (#2198) 2026-03-18 12:54:41 +08:00
安正超 c9a2fd756c feat(rustfs): add optional license gating feature (#2197) 2026-03-18 08:45:05 +08:00
dependabot[bot] bddb0d0a05 build(deps): bump astral-tokio-tar from 0.5.6 to 0.6.0 (#2196)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 08:22:12 +08:00
houseme 9ce3c7742c fix(targets): pass credentials to MQTT broker availability check (#2192)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 23:53:28 +08:00
houseme b5d881f399 fix(notify): Fix XML Filter parsing and add comprehensive tests (#2191) 2026-03-17 23:08:07 +08:00
安正超 ce1f7cfdcb chore(skills): add repository-local workflow skills (#2190) 2026-03-17 22:13:46 +08:00
马登山 c66c6d97ec fix(lifecycle): respect Filter.Prefix and safe delete marker expiry (#2185)
Signed-off-by: likewu <likewu@126.com>
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: likewu <likewu@126.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-17 18:45:38 +08:00
dependabot[bot] be89b5fc6a build(deps): bump lz4_flex from 0.12.0 to 0.12.1 (#2181)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 10:10:41 +08:00
houseme 94cdb89e29 feat(obs): add init_obs_with_config API and signature guard test (#2175)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-03-16 18:17:55 +08:00
heihutu 06dff96c09 chore(deps): update flake.lock (#2173) 2026-03-16 16:01:36 +08:00
安正超 c1d5106acc feat(ci): allow selecting build platforms in build workflow (#2171) 2026-03-15 22:01:44 +08:00
heihutu 0a2411f59c chore(deps): update flake.lock (#2169)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-15 16:10:12 +08:00
houseme 1ede71b881 chore: update nix-flake-update.yml to use FLAKE_UPDATE_TOKEN for user… (#2168) 2026-03-15 14:49:38 +08:00
github-actions[bot] 4fb7059e6f chore(deps): update flake.lock (#2165) 2026-03-15 10:26:05 +08:00
安正超 2ad275ecc3 fix(helm): quote obs stdout configmap value (#2166)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: 马登山 <Cxymds@qq.com>
2026-03-15 10:11:25 +08:00
Philip Schmid 9179fd5608 fix(helm): merge customAnnotations with class-specific ingress annotations (#2161)
Signed-off-by: Philip Schmid <philip.schmid@protonmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-15 09:22:12 +08:00
LeonWang0735 7f1cdaedad feat(replication): add bandwidth-aware reporting for bucket replication metrics (#2141) 2026-03-15 09:03:10 +08:00
houseme 7f3459f5a8 fix(obs): fixed unresolved import super::local::ensure_dir_permissions (#2164) 2026-03-15 00:33:06 +08:00
yxrxy d3cff7d033 feat(webdav): add WebDAV protocol gateway (#2158)
Signed-off-by: yxrxy <1532529704@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: 马登山 <Cxymds@qq.com>
Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-03-14 23:06:53 +08:00
majinghe f66a90c1b2 fix: fix github action error caused by oltp modification (#2163)
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-14 22:15:35 +08:00
majinghe afcaaf66fc feat: add support for obs enpoint support in helm chart (#2160) 2026-03-14 21:44:44 +08:00
安正超 a1104b45f6 fix(obs): honor target-only rust_log directives (#2159)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 11:14:46 +08:00
bcdax110 82d9452736 docs: fix incorrect UID in Docker Quick Start of README_ZH (#2149)
Signed-off-by: bcdax110 <1711382287@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-14 09:26:07 +08:00
houseme 6e0f034ad1 refactor(obs): enhance log rotation robustness and refine filter logic (#2155)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 09:20:35 +08:00
houseme 593a58c161 refactor(obs): optimize logging with custom RollingAppender and improved cleanup (#2151)
Signed-off-by: houseme <housemecn@gmail.com>
Signed-off-by: heihutu <30542132+heihutu@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-13 13:20:27 +08:00
houseme f83bf95b04 feat(ecstore): Skip rustls provider install if already present (#2145)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-03-12 18:02:19 +08:00
安正超 aa88b1976a fix(ecstore): avoid warm tier init panics (#2144) 2026-03-12 13:52:49 +08:00
安正超 e2f741d41f fix(helm): use canonical scanner start delay env (#2142) 2026-03-12 10:06:42 +08:00
安正超 ad54293d7e fix(admin): propagate heal handler background errors (#2124) 2026-03-12 10:06:12 +08:00
安正超 83fb530609 refactor(config): normalize scanner env naming (#2129) 2026-03-11 22:41:41 +08:00
安正超 aa84d34bf8 fix(auth): preserve IAMAuth clone and correct missing-key error (#2123) 2026-03-11 21:59:12 +08:00
安正超 df57f0c033 fix(workers): clamp worker release count (#2122) 2026-03-11 21:59:00 +08:00
安正超 c47dec8549 fix(signer): avoid panics in v2 signing for missing data (#2121) 2026-03-11 21:58:40 +08:00
安正超 fdbe12ec95 fix(scanner): respect configured scan start delay (#2119) 2026-03-11 21:56:48 +08:00
安正超 b2e8078971 fix(policy): avoid unicode panic in variable resolver (#2115) 2026-03-11 21:56:32 +08:00
安正超 ac43a44a00 [codex] fix scanner first cycle startup delay (#2137) 2026-03-11 20:02:01 +08:00
安正超 5625f04697 fix(common): remove panic paths in runtime helpers (#2116)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
2026-03-11 18:12:37 +08:00
安正超 e1f24f764d fix(credentials): harden masked debug output (#2114)
Signed-off-by: heihutu <30542132+heihutu@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
2026-03-11 15:40:37 +08:00
安正超 7d7e0b2654 fix(utils): harden panic-prone paths (#2113)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
2026-03-11 15:16:03 +08:00
安正超 9908a44c38 fix(protocols): return errors instead of panics for sync signatures (#2120)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
2026-03-11 11:22:20 +08:00
evan slack 4b480727d6 feat(perf): Add configurable bitrot skip for reads (#2110)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-03-11 10:59:00 +08:00
simon-escapecode f00d01ec2d fix: resolve silent failure in MQTT bucket event notifications (#2112)
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-11 10:08:30 +08:00
dependabot[bot] 7e8c7fa2b2 build(deps): bump quinn-proto from 0.11.13 to 0.11.14 (#2127)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 09:30:53 +08:00
安正超 845ad1fa16 fix(obs): avoid panic in telemetry init and clamp sampler boundaries (#2118)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-11 01:32:46 +08:00
安正超 bb4fbf5ae2 fix(notify): ignore disabled targets when sending events (#2117)
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-11 00:37:30 +08:00
安正超 3df7105dae fix(server): init event notifier when partial notify configured (#2125) 2026-03-10 23:52:40 +08:00
evan slack b3da8ae269 feat(scanner): Add dynamic throttling presets (#2095)
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: GatewayJ <835269233@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: weisd <im@weisd.in>
2026-03-10 16:12:56 +08:00
majinghe 67e5f5e3c3 feat: add metrics support in helm chart (#2109)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-10 12:08:36 +08:00
majinghe 296efea42f change ghcr username and password name due to github restrict (#2108) 2026-03-09 21:44:35 +08:00
GatewayJ 16946c5a54 fix: allow root to bypass bucket policy deny for policy management APIs (#2102)
Co-authored-by: GatewayJ <8352692332qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
2026-03-09 20:36:29 +08:00
majinghe 73d29e95dd feat:add docker image support for quay.io and ghcr.io (#2107)
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-09 16:22:28 +08:00
dependabot[bot] e930c5c281 build(deps): bump libc from 0.2.182 to 0.2.183 in the dependencies group (#2106)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-09 16:03:17 +08:00
安正超 9d03029959 fix(iam): sync user cache on load-user notifications (#2104) 2026-03-09 09:36:02 +08:00
loverustfs a02c354ef5 Fix image url error
Fix image url error

Signed-off-by: loverustfs <hello@rustfs.com>
2026-03-08 23:39:50 +08:00
houseme 60aa47bf61 feat(storage): integrate S3Operation into OperationHelper for unified metrics and audit (#2103) 2026-03-08 17:57:33 +08:00
houseme 8e4a1ef917 refactor(protocols): replace tar with astral-tokio-tar for async processing (#2099)
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
2026-03-08 15:18:15 +08:00
Peter Hamilton b035d10abb fix(metrics): Remove high cardinality labels causing memory leak (#2098)
Co-authored-by: loverustfs <hello@rustfs.com>
2026-03-08 13:01:11 +08:00
github-actions[bot] 2180e9e7a1 chore(deps): update flake.lock (#2097)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-08 10:51:32 +08:00
evan slack 57e49e6737 feat(obs): Add metric to count all s3 operations (#2088)
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-08 10:19:20 +08:00
Senol Colak b07383760f Add OpenStack Swift API Support (#2066)
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot <noreply@github.com>
2026-03-08 01:11:35 +08:00
evan slack 7c94be4e8c fix(obs): Remove high cardinality label on rustfs_api_requests_total (#2087)
Co-authored-by: loverustfs <hello@rustfs.com>
2026-03-07 20:46:33 +08:00
evan slack d52a10c5fb chore(obs): Improve tracing instrumentation (#2086)
Co-authored-by: loverustfs <hello@rustfs.com>
2026-03-07 20:03:20 +08:00
安正超 8c4735ff88 docs: scope AGENTS instructions by directory (#2083) 2026-03-05 17:25:37 +08:00
LeonWang0735 a0503168d4 fix(heal):heal failed replication via must_replicate instead of check replicate_delete (#2072) 2026-03-05 15:47:36 +08:00
安正超 b73059dcf2 fix(admin): allow non-consoleAdmin self password update (#2082) 2026-03-05 15:47:21 +08:00
weisd ed18b3da75 Fix data usage cache and scanner (#2074) 2026-03-04 19:55:01 +08:00
houseme 05032cf887 chore: update dependencies and workspace resolver (#2073) 2026-03-04 19:22:54 +08:00
唐小鸭 f89cdfe5b3 update s3s 0.14.0-dev (#2070)
Co-authored-by: houseme <housemecn@gmail.com>
2026-03-04 01:07:24 +08:00
houseme f4b523c236 build(deps): bump the dependencies group with 7 updates (#2069) 2026-03-04 00:42:03 +08:00
安正超 c6209ba59d ci: optimize workflow runtime and remove redundant pipeline work (#2065) 2026-03-03 20:56:37 +08:00
houseme 5e7495a042 build(obs): restrict pyroscope dependency to unix targets (#2064) 2026-03-03 20:41:37 +08:00
evan slack ac4b13def1 feat(obs): Optional continuous CPU profiling with grafana pyroscope (#2035)
Signed-off-by: houseme <housemecn@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
2026-03-03 15:28:58 +08:00
928 changed files with 184842 additions and 28377 deletions
@@ -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 58 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: 05 uncovered lines
- 1: 620 uncovered lines
- 2: 2140 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.
+26
View File
@@ -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"]
+18
View File
@@ -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:
+11
View File
@@ -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;
# ...
+12
View File
@@ -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 |
+12
View File
@@ -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
+43 -87
View File
@@ -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]))
+8
View File
@@ -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
+286
View File
@@ -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
+15 -37
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
data/
+106
View File
@@ -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
```
+120
View File
@@ -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
+36
View File
@@ -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`
+1 -1
View File
@@ -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"
+17
View File
@@ -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
+9
View File
@@ -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:
+21 -22
View File
@@ -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
View File
@@ -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
View File
@@ -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: |
+70
View File
@@ -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 }}
+28 -24
View File
@@ -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
+37 -16
View File
@@ -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)
+54 -16
View File
@@ -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
+7 -3
View File
@@ -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
+2 -2
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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/
+14 -6
View File
@@ -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"
+97 -87
View File
@@ -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
View File
@@ -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).*
+3
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+121 -98
View File
@@ -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
View File
@@ -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 \
+65
View File
@@ -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
View File
@@ -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
View File
@@ -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" \
+85 -12
View File
@@ -1,4 +1,4 @@
[![RustFS](https://github.com/user-attachments/assets/3ba82e75-2f2d-4415-a4aa-1e4ffe9f22fd)](https://rustfs.com)
[![RustFS](https://repository-images.githubusercontent.com/722597620/0fa936a2-8164-4f53-867f-def4beb64b21)](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
View File
@@ -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)
## 贡献者
+3
View File
@@ -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
View File
@@ -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
+20
View File
@@ -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.
+4 -1
View File
@@ -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
View File
@@ -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());
}
+8 -2
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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()
}
}
+10 -1
View File
@@ -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() {
+1 -1
View File
@@ -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
View File
@@ -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(&section_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(&section)
&& !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(&section) {
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(&section);
} 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
View File
@@ -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
+29 -20
View File
@@ -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]
+8 -5
View File
@@ -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
+6 -12
View File
@@ -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}");
+3
View File
@@ -36,3 +36,6 @@ sha2 = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
[lib]
doctest = false
+1 -1
View File
@@ -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
});
+3
View File
@@ -41,3 +41,6 @@ rmp-serde = { workspace = true }
async-trait = { workspace = true }
s3s = { workspace = true }
tracing = { workspace = true }
[lib]
doctest = false
+308
View File
@@ -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();
}
}
+96 -38
View File
@@ -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);
}
}
+85 -10
View File
@@ -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();
+166
View File
@@ -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();
}
}
+2 -8
View File
@@ -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);
+2
View File
@@ -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
View File
@@ -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.
}
}
+17
View File
@@ -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)]
+43
View File
@@ -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"]
+224
View File
@@ -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);
}
}
+256
View File
@@ -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());
}
}
+207
View File
@@ -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");
}
}
+171
View File
@@ -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};
}
+219
View File
@@ -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());
}
}
+315
View File
@@ -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();
}
}
+225
View File
@@ -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);
}
}
+150
View File
@@ -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);
}
}
+28
View File
@@ -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`
+3
View File
@@ -37,3 +37,6 @@ constants = ["dep:const-str"]
notify = ["dep:const-str", "constants"]
observability = ["constants"]
opa = ["constants"]
[lib]
doctest = false
+33
View File
@@ -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